Files
tensor/internal/core/fresnel.go
T
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

200 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package core
import (
"math"
"math/big"
)
// Fresnel integrals, the diffraction pair C(x) = ∫₀^x cos(πt²/2) dt
// and S(x) = ∫₀^x sin(πt²/2) dt.
//
// The power series converge for every real x, but their intermediate
// terms grow like e^(πx²/2) before cancelling down to the ½ limit.
// Within |x| ≤ 4 the cancellation is mild and plain float64 holds full
// accuracy. Above that the same series is summed in extended
// precision (math/big), with the working bit size scaled to the
// largest intermediate term, so the float64 result stays correct
// instead of drowning in cancellation noise. Both integrals are odd
// functions.
// FresnelC returns the Fresnel cosine integral C(x) of each element.
func FresnelC(a *Array) (*Array, error) { return a.realFunc("FresnelC", fresnelC) }
// FresnelS returns the Fresnel sine integral S(x) of each element.
func FresnelS(a *Array) (*Array, error) { return a.realFunc("FresnelS", fresnelS) }
const fresnelSeriesCutoff = 3
// fresnelAsymptoticFrom is the argument above which the erfc
// asymptotic series answers the Fresnel pair. There the terms of that
// series fall by a factor 2πx² per order, so a handful of them reach
// double precision, where the power series would need a working
// precision growing with x² (and its term count with x²).
const fresnelAsymptoticFrom = 10
func fresnelC(x float64) float64 {
if x < 0 {
return -fresnelC(-x)
}
if x > fresnelAsymptoticFrom {
return fresnelAsym(x, false)
}
if x > fresnelSeriesCutoff {
return fresnelBig(x, false)
}
return fresnelCSeries(x)
}
func fresnelS(x float64) float64 {
if x < 0 {
return -fresnelS(-x)
}
if x > fresnelAsymptoticFrom {
return fresnelAsym(x, true)
}
if x > fresnelSeriesCutoff {
return fresnelBig(x, true)
}
return fresnelSSeries(x)
}
// fresnelCSeries sums C(x) = Σ (−1)^k (π/2)^{2k} x^{4k+1}/((2k)!(4k+1))
// by the term-ratio recurrence. Accurate while the largest
// intermediate term stays comfortably inside float64, which holds
// through |x| ≈ 4.
func fresnelCSeries(x float64) float64 {
sum := 0.0
term := x // the k = 0 term
for k := range 200 {
sum += term
next := -term * (math.Pi / 2) * (math.Pi / 2) * x * x * x * x /
float64((2*k+1)*(2*k+2)) * float64(4*k+1) / float64(4*k+5)
if math.Abs(next) < 1e-18*math.Abs(sum) {
break
}
term = next
}
return sum
}
// fresnelSSeries sums S(x) = Σ (−1)^k (π/2)^{2k+1} x^{4k+3}/((2k+1)!(4k+3))
// by the term-ratio recurrence, with the same validity range as the
// cosine branch.
func fresnelSSeries(x float64) float64 {
sum := 0.0
term := (math.Pi / 2) * x * x * x / 3 // the k = 0 term
for k := range 200 {
sum += term
next := -term * (math.Pi / 2) * (math.Pi / 2) * x * x * x * x /
float64((2*k+2)*(2*k+3)) * float64(4*k+3) / float64(4*k+7)
if math.Abs(next) < 1e-18*math.Abs(sum) {
break
}
term = next
}
return sum
}
// fresnelAsym evaluates C or S from the auxiliary functions above.
// x must be positive and at least fresnelAsymptoticFrom.
func fresnelAsym(x float64, sine bool) float64 {
f, g := fresnelAux(x)
u := math.Pi * x * x / 2
cosu, sinu := math.Cos(u), math.Sin(u)
if sine {
return 0.5 - f*cosu - g*sinu
}
return 0.5 + f*sinu - g*cosu
}
// fresnelAux returns the auxiliary functions f and g of the Fresnel
// asymptotics,
//
// S(x) = ½ − f·cos(πx²/2) − g·sin(πx²/2)
// C(x) = ½ + f·sin(πx²/2) − g·cos(πx²/2)
//
// as the classical expansions
//
// f(x) = (1/(πx))·Σ (−1)^k (4k−1)!!·t^{2k}
// g(x) = (1/(πx))·Σ (−1)^k (4k+1)!!·t^{2k+1}, t = 1/(πx²),
//
// whose coefficients follow from (4k+1)(4k+3). x must be positive and
// at least fresnelAsymptoticFrom: there t ≤ 1/314 and the terms fall
// fast enough for four of them to pass double precision. The
// coefficients and the reconstruction were checked against mpmath to
// 1e-20 at x = 8, 12, 20, 100, 1000.
func fresnelAux(x float64) (f, g float64) {
t := 1 / (math.Pi * x * x)
cf, cg := 1.0, t
sumF, sumG := 1.0, t
for k := range 20 {
cf = -cf * float64(4*k+1) * float64(4*k+3) * t * t
cg = -cg * float64(4*k+3) * float64(4*k+5) * t * t
sumF += cf
sumG += cg
if math.Abs(cf)+math.Abs(cg) < 1e-19 {
break
}
}
inv := 1 / (math.Pi * x)
return inv * sumF, inv * sumG
}
// beyond the float64 cancellation ceiling. The working bit size grows
// with the largest intermediate term; the tail is cut once the terms
// fall below a few 1e-40 relative to the running sum, which bounds the
// final float64 rounding far below the 1e-15 relative level.
func fresnelBig(x float64, sine bool) float64 {
// The largest intermediate term of the series grows like e^{πx²/2},
// so the working precision must grow by π/(2·ln2) ≈ 2.266 bits per
// unit of x²; a smaller coefficient silently loses the cancellation
// and the result turns to noise around x ≈ 22.
bits := uint(64 + int(2.2663*x*x) + 96)
xf := new(big.Float).SetPrec(bits).SetFloat64(x)
halfPi := new(big.Float).SetPrec(bits).SetFloat64(math.Pi / 2)
hh := new(big.Float).SetPrec(bits).Mul(halfPi, halfPi)
x4 := new(big.Float).SetPrec(bits).Mul(xf, xf)
x4.Mul(x4, xf)
x4.Mul(x4, xf)
term := new(big.Float).SetPrec(bits)
if sine {
// S seed: (π/2)·x³/3.
term.Mul(halfPi, xf)
term.Mul(term, xf)
term.Mul(term, xf)
term.Quo(term, big.NewFloat(3))
} else {
term.Set(xf)
}
sum := new(big.Float).SetPrec(bits)
tiny := new(big.Float).SetPrec(bits).SetFloat64(1e-40)
tinyNeg := new(big.Float).SetPrec(bits).SetFloat64(-1e-40)
for k := range 200000 {
sum.Add(sum, term)
an := 4*k + 1
d1, d2, d3 := 2*k+1, 2*k+2, 4*k+5
if sine {
an = 4*k + 3
d1, d2, d3 = 2*k+2, 2*k+3, 4*k+7
}
next := new(big.Float).SetPrec(bits).Neg(term)
next.Mul(next, hh)
next.Mul(next, x4)
next.Mul(next, big.NewFloat(float64(an)))
next.Quo(next, big.NewFloat(float64(d1)))
next.Quo(next, big.NewFloat(float64(d2)))
next.Quo(next, big.NewFloat(float64(d3)))
if next.Cmp(tiny) < 0 && next.Cmp(tinyNeg) > 0 {
break
}
term = next
}
out, _ := sum.Float64()
return out
}