// Copyright (c) 2026 Petr Balvín (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 }