Files
tensor/internal/core/fresnel.go
T

200 lines
6.1 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// 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
}