200 lines
6.1 KiB
Go
200 lines
6.1 KiB
Go
// 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
|
|||
|
|
}
|