Files
tensor/internal/core/special2.go
T

450 lines
16 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"
// Ordinary Bessel functions of the first and second kind at one real
// point, the scalar companions of the modified pair in besselmod.go.
// J grows out of a convergent power series below the crossover and a
// downward Miller recurrence above it, the direction that amplifies
// no rounding once the order passes the argument. Y climbs the
// upward recurrence from the seeds Y₀ and Y₁, the stable direction
// for the second kind, with the seeds themselves from the Frobenius
// series below the crossover and from the asymptotic expansion above
// it, where the ascending series would start paying for cancellation.
// besselCrossover splits the power-series and recurrence regimes. At
// the crossover both sides still carry twelve significant digits, so
// the exact split point is a matter of taste rather than accuracy.
const besselCrossover = 15.0
// BesselJ returns the Bessel function of the first kind of integer
// order n at the real point x, Jₙ(x). Arguments with |x| below the
// crossover are served by the convergent power series
// Σ (−1)^k (x/2)^{2k+n}/(k!·Γ(k+n+1)); above it the recurrence runs in
// its stable direction, orders at or below the argument climbing
// upward from the large-argument asymptotic J₀ and J₁ at O(n) cost and
// higher orders running the downward Miller walk anchored on the same
// asymptotic J₀, whose start sits above the turning point at order
// n + |x|. A negative argument follows the parity law
// Jₙ(−x) = (−1)ⁿ Jₙ(x) and a negative order the law
// J₋ₙ(x) = (−1)ⁿ Jₙ(x); Jₙ is finite for every real x, so nothing here
// can fail.
func BesselJ(n int, x float64) float64 {
if x == 0 {
if n == 0 {
return 1
}
return 0
}
// Fold both parity laws into one sign: (−1)ⁿ only cares about the
// order's parity, which |n| preserves.
order, ax, flip := n, math.Abs(x), 1.0
if order < 0 {
if order%2 != 0 {
flip = -1
}
order = -order
}
if x < 0 && order%2 != 0 {
flip = -flip
}
if ax < besselCrossover {
return flip * besselJSeries(order, ax)
}
if float64(order) <= ax {
return flip * besselJUpward(order, ax)
}
return flip * besselJMiller(order, ax)
}
// BesselY returns the Bessel function of the second kind of integer
// order n at the real point x, Yₙ(x), defined for x > 0; Y diverges
// at the origin and a non-positive argument is an error, not a NaN.
// Y₀ and Y₁ are seeded below the crossover by the Frobenius series
// (Abramowitz & Stegun 9.1.11 with ψ(k+1) = H_k − γ written out
// through the harmonic numbers H_k) and above it by the
// large-argument asymptotic expansion; higher orders climb the upward
// recurrence Yₙ₊₁ = 2n/x·Yₙ − Yₙ₋₁, stable for the second kind
// because the parasitic J component the seeds carry decays relative
// to Y at every step. A negative order follows the parity law
// Y₋ₙ(x) = (−1)ⁿ Yₙ(x).
//
// Errors: x ≤ 0 or NaN.
func BesselY(n int, x float64) (float64, error) {
if math.IsNaN(x) || x <= 0 {
return 0, errf("BesselY: the argument must be positive, got %g", x)
}
flip, order := 1.0, n
if order < 0 {
if order%2 != 0 {
flip = -1
}
order = -order
}
var v float64
switch {
case order == 0:
v = besselY0(x)
case order == 1:
v = besselY1(x)
default:
ym, y := besselY0(x), besselY1(x)
for k := 1; k < order; k++ {
ym, y = y, 2*float64(k)/x*y-ym
}
v = y
}
return flip * v, nil
}
// besselJSeries evaluates Jₙ(x) by the convergent power series,
// stepping the summand along tₖ = tₖ₋₁·(−(x/2)²)/(k(k+n)). The first
// term (x/2)ⁿ/n! goes through logarithms so a large order never
// overflows the factorial on the way to a small answer.
func besselJSeries(n int, x float64) float64 {
half := 0.5 * x
// The k = 0 term (x/2)ⁿ/n! is positive; the (−1)^k alternation
// enters through the recursion step below.
t := math.Exp(float64(n)*math.Log(half) - LnFactorial(n))
sum := t
q := half * half
for k := 1; k <= 400; k++ {
t *= -q / (float64(k) * float64(k+n))
sum += t
if math.Abs(t) <= 1e-17*math.Abs(sum) {
break
}
}
return sum
}
// besselJUpward climbs J₀ and J₁ from the large-argument asymptotic to
// order n by the upward recurrence Jₖ₊₁ = (2k/x)·Jₖ − Jₖ₋₁. The
// asymptotic seeds carry the absolute scale, so nothing needs
// renormalising, and the climb is the stable direction while the order
// stays at or below the argument: there the parasitic Y component the
// seeds carry stays bounded relative to J, while the downward walk
// would start above the turning point at order x and pay O(x) steps
// for an answer of order n. The caller guarantees x ≥ besselCrossover
// and 0 ≤ n ≤ x.
func besselJUpward(n int, x float64) float64 {
j0, _ := besselAsymptotic(0, x)
if n == 0 {
return j0
}
j1, _ := besselAsymptotic(1, x)
for k := 1; k < n; k++ {
j0, j1 = j1, 2*float64(k)/x*j1-j0
}
return j1
}
// besselJMiller evaluates Jₙ(x) for |x| above the crossover by the
// downward Miller recurrence, the same stable scheme
// besselInMiller uses for the modified kind: start well above n with
// an arbitrary scale, recurse down through J_{k−1} = (2k/x)·J_k −
// J_{k+1}, then renormalise the arbitrary seed scale against an
// independent J₀, here the asymptotic value the way besselInMiller
// leans on its quadrature I₀. The caller guarantees x ≠ 0.
func besselJMiller(n int, x float64) float64 {
start := n + int(x) + 40
jp, j := 0.0, 1.0 // J_{k+1}, J_k, seeded at k = start
jn := 0.0
for k := start; k >= 1; k-- {
if k == n {
jn = j
}
jp, j = j, 2*float64(k)/x*j-jp
if aj := math.Abs(j); aj > 1e200 {
// The unscaled seed grows like k!/x^k on the way down and
// would overflow for high orders whose true value is
// representable; the renormalisation cancels any common
// factor, so rescaling the running pair (and the captured
// order-n value) is exact up to rounding.
jp /= aj
j /= aj
jn /= aj
}
}
if n == 0 {
jn = j
}
anchor, _ := besselAsymptotic(0, x)
return anchor * jn / j
}
// besselY0 evaluates Y₀(x) for x > 0. Below the crossover the
// Frobenius series in the form the digamma reduction gives,
//
// Y₀ = (2/π)·[(ln(x/2) + γ)·J₀(x) + Σ (−1)^{k+1} H_k (x/2)^{2k}/(k!)²],
//
// above it the asymptotic expansion, whose terms still fall fast
// enough at the crossover to keep twelve significant digits.
func besselY0(x float64) float64 {
if x >= besselCrossover {
_, y := besselAsymptotic(0, x)
return y
}
half := 0.5 * x
q := half * half
u := q // u_k = (x/2)^{2k}/(k!)², starting at k = 1
h := 1.0 // H_k
sum := u // the k = 1 term carries the + sign
for k := 2; k <= 200; k++ {
u *= q / float64(k*k)
h += 1 / float64(k)
term := h * u
if k%2 == 1 {
sum += term
} else {
sum -= term
}
if math.Abs(term) <= 1e-17*math.Abs(sum) {
break
}
}
return (2 / math.Pi) * ((math.Log(half)+eulerGamma)*besselJSeries(0, x) + sum)
}
// besselY1 evaluates Y₁(x) for x > 0, the order-one twin of
// besselY0's series branch:
//
// Y₁ = (2/π)(ln(x/2) + γ)·J₁(x) − 2/(πx)
// − (1/π)·Σ (−1)^k (H_k + H_{k+1})·(x/2)^{2k+1}/(k!(k+1)!),
//
// where the 2/(πx) term is the one-entry finite sum of A&S 9.1.11
// and the ascending series converges for every x, paying only the
// cancellation that caps its usable range at the crossover.
func besselY1(x float64) float64 {
if x >= besselCrossover {
_, y := besselAsymptotic(1, x)
return y
}
half := 0.5 * x
q := half * half
u := half // u_k = (x/2)^{2k+1}/(k!(k+1)!), starting at k = 0
h := 1.0 // H_{k+1}, starting at H_1 = 1 (H_0 = 0)
sum := u // the k = 0 term: (H_0 + H_1)·u₀ with the + sign
for k := 1; k <= 200; k++ {
hk := h // H_k, before the update below
u *= q / float64(k*(k+1))
h += 1 / float64(k+1)
term := (hk + h) * u
if k%2 == 1 {
sum -= term
} else {
sum += term
}
if math.Abs(term) <= 1e-17*math.Abs(sum) {
break
}
}
return (2/math.Pi)*((math.Log(half)+eulerGamma)*besselJSeries(1, x)) -
2/(math.Pi*x) - sum/math.Pi
}
// besselAsymptotic evaluates the pair J_ν, Y_ν above the crossover
// from the large-argument expansion (Abramowitz & Stegun 9.2.5 through
// 9.2.6):
//
// J_ν ~ sqrt(2/πx)·[cos ω·Σ(−1)^k a_{2k} − sin ω·Σ(−1)^k a_{2k+1}]
// Y_ν ~ sqrt(2/πx)·[sin ω·Σ(−1)^k a_{2k} + cos ω·Σ(−1)^k a_{2k+1}],
//
// with ω = x − νπ/2 − π/4 and aₘ = ∏(μ − (2s−1)²)/(m!·(8x)^m),
// μ = 4ν². The order is a float64: the integer callers pass exact
// integers, whose float64 arithmetic reproduces the int path bit for
// bit, and the real-order BesselJRealOrder passes the fractional seed
// orders. Both kinds share the one coefficient sweep, and the sum
// stops at the optimal truncation where the terms turn around and
// start growing again, which at the crossover still leaves about
// twelve significant digits.
func besselAsymptotic(nu float64, x float64) (j, y float64) {
mu := 4 * nu * nu
omega := besselPhase(nu, x)
eighth := 8 * x
a, prev := 1.0, math.Inf(1)
even, odd := 1.0, 0.0 // Σ(−1)^k a_{2k}, Σ(−1)^k a_{2k+1}
for m := 1; m <= 200; m++ {
a *= (mu - float64((2*m-1)*(2*m-1))) / (float64(m) * eighth)
// m = 2k and m = 2k+1 share the integer k = m/2 and the sign
// (−1)^k of their sum's k-th term.
if (m/2)%2 == 0 {
if m%2 == 0 {
even += a
} else {
odd += a
}
} else {
if m%2 == 0 {
even -= a
} else {
odd -= a
}
}
if math.Abs(a) > prev {
break
}
prev = math.Abs(a)
}
factor := math.Sqrt(2 / (math.Pi * x))
sin, cos := math.Sin(omega), math.Cos(omega)
j = factor * (cos*even - sin*odd)
y = factor * (sin*even + cos*odd)
return j, y
}
// 2π split into two float64 parts, the sum of which is 2π to about
// 1e-32: the phase reduction below needs the low part to hold the
// fraction of a large argument.
const (
twoPiHi = 6.283185307179586 // fl(2π)
twoPiLo = 2.4492935982947064e-16 // 2π − fl(2π)
)
// besselPhase returns ω = x − νπ/2 − π/4 reduced modulo 2π for the
// large-argument expansion. The reduction is what keeps the phase of a
// large argument meaningful: the plain float64 difference rounds the
// fraction of x away, at x = 1e9 to about 1e-7 absolute, which the
// amplitude √(2/πx) turns into a relative error of 1e-8, and at
// x = 1e12 into 1e-4. The remainder is taken with the two-part 2π:
// x − hi is exact by Sterbenz's lemma, math.FMA gives the exact
// residual of n·2π, and the rest is a handful of flops on quantities
// below π, so the phase keeps its own last ulp while |ω| < 2^53·2π
// (beyond that the float64 grid of x is coarser than a radian and the
// plain difference is as good as anything). The caller passes |x|,
// which is at least the crossover.
func besselPhase(nu float64, x float64) float64 {
nuPi2 := nu * math.Pi / 2
quarterPi := math.Pi / 4
omega := x - nuPi2 - quarterPi
n := math.Round(omega / twoPiHi)
if math.Abs(n) >= 1<<53 {
return omega
}
hi := n * twoPiHi
lo := math.FMA(n, twoPiHi, -hi)
return x - hi - lo - n*twoPiLo - nuPi2 - quarterPi
}
// BesselJRealOrder returns the Bessel function of the first kind of
// real order ν at the real point x, J_ν(x). The order must be ≥ 0 and
// the argument positive; J diverges at the origin for ν > 0 and a
// non-positive argument or a NaN order is an error, not a NaN.
//
// The regimes follow the integer BesselJ's, with the fractional part
// of the order taking the role of the seeds: below the crossover the
// ascending Frobenius series carries the answer, above it the order
// resolves into its integer and fractional parts, the fractional pair
// (ν₀, ν₀+1) is seeded from the large-argument expansion and the
// three-term recurrence runs in its stable direction, upward while the
// order stays at or below the argument and by the downward Miller walk
// anchored on the ν₀ seed when the order passes it. Orders within 1e-8
// of a non-negative integer are served by the integer algorithm
// itself, which is the continuous limit there: the general route's
// normalisation would cancel against sin(πν) and lose every digit.
func BesselJRealOrder(nu, x float64) (float64, error) {
if math.IsNaN(nu) || math.IsNaN(x) {
return 0, errf("BesselJRealOrder: the order and the argument must be finite, got %g and %g", nu, x)
}
if nu < 0 {
return 0, errf("BesselJRealOrder: the order must be zero or greater, got %g", nu)
}
if x <= 0 {
return 0, errf("BesselJRealOrder: the argument must be positive, got %g", x)
}
if n := math.Round(nu); math.Abs(nu-n) <= 1e-8 {
return BesselJ(int(n), x), nil
}
if x < besselRealCrossover {
return besselJSeriesReal(nu, x), nil
}
whole := math.Floor(nu)
frac := nu - whole
if nu <= x {
// The climb: seed the fractional pair from the expansion and
// run the upward recurrence, the stable direction while the
// order stays at or below the argument. whole = 0 means the
// answer is the first seed itself.
jm, _ := besselAsymptotic(frac, x)
if whole == 0 {
return jm, nil
}
j, _ := besselAsymptotic(frac+1, x)
for k := 1; k < int(whole); k++ {
jm, j = j, 2*(frac+float64(k))/x*j-jm
}
return j, nil
}
// The downward Miller walk through one fractional residue class,
// anchored on the fractional seed the same way besselJMiller
// anchors its integer walk on J₀. The walk counts integer steps
// from the fractional floor instead of testing k against nu and
// frac: start descends in floats, and a capture missed by one ulp
// of the running k would answer from an unseeded slot.
steps := int(math.Floor(x)) + int(whole) + 40
jp, j := 0.0, 1.0 // J_{k+1}, J_k, seeded at k = frac + steps
uNu := 0.0
uFrac := 0.0
for i := steps; i >= 0; i-- {
k := frac + float64(i)
if i == int(whole) {
uNu = j
}
if i == 0 {
uFrac = j
}
jp, j = j, 2*k/x*j-jp
if aj := math.Abs(j); aj > 1e200 {
jp /= aj
j /= aj
uNu /= aj
uFrac /= aj
}
}
anchor, _ := besselAsymptotic(frac, x)
return anchor * uNu / uFrac, nil
}
// lnGammaReal returns lnΓ(z) for z > 0 at one point, the scalar
// companion the real-order series needs; the sign of Γ is positive on
// that domain, so only the logarithm comes back.
func lnGammaReal(z float64) float64 {
lg, _ := math.Lgamma(z)
return lg
}
// besselRealCrossover splits the real-order series and recurrence
// regimes, lower than the integer one because the two sides' quality
// decides differently here: the series pays the same cancellation
// (about x·ln10/2 digits at x = 12, still leaving better than nine),
// while the expansion the seeds come from truncates to full precision
// for the half-integer orders and to ten-plus digits for the small
// fractional seeds the climb and the Miller walk lean on.
const besselRealCrossover = 12.0
// besselJSeriesReal evaluates J_ν(x) by the convergent Frobenius
// series for real ν, stepping the summand along
// tₖ = tₖ₋₁·(−(x/2)²)/(k(k+ν)). It is the real-order companion of
// besselJSeries, kept separate so the integer series keeps its
// exact LnFactorial opening and its recorded bits.
func besselJSeriesReal(nu, x float64) float64 {
half := 0.5 * x
// The opening term never overflows below the crossover: with
// half < 7.5 the exponent ν·log(half) − lnΓ(ν+1) turns downward
// past ν ≈ 20 and stays negative.
t := math.Exp(nu*math.Log(half) - lnGammaReal(nu+1))
sum := t
q := half * half
for k := 1; k <= 400; k++ {
t *= -q / (float64(k) * (float64(k) + nu))
sum += t
if math.Abs(t) <= 1e-17*math.Abs(sum) {
break
}
}
return sum
}