// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package core import "math" // Modified Bessel functions of the first and second kind, the workhorses // of heat conduction, waveguides and filtered noise. I₀ and I₁ use the // polynomial fits below 3.75 (Abramowitz & Stegun 9.8.1 and 9.8.2) and // their integral representations beyond, K₀ and K₁ integrate their // kernels directly, and integer orders follow by the stable direction // of the recurrence: upward for I, downward for K. // All are element-wise over real arrays; ints and float32 promote. // BesselI0 returns the modified Bessel function of the first kind of // order zero, I₀(x), of each element. I₀ is even with I₀(0) = 1. func BesselI0(a *Array) (*Array, error) { return a.realFunc("BesselI0", besselI0) } // BesselI1 returns the modified Bessel function of the first kind of // order one, I₁(x), of each element. I₁ is odd with I₁(0) = 0. func BesselI1(a *Array) (*Array, error) { return a.realFunc("BesselI1", besselI1) } // BesselK0 returns the modified Bessel function of the second kind of // order zero, K₀(x), of each element, defined for x > 0. K₀ diverges // logarithmically at 0 and returns +Inf there. func BesselK0(a *Array) (*Array, error) { return a.realFunc("BesselK0", besselK0) } // BesselK1 returns the modified Bessel function of the second kind of // order one, K₁(x), of each element, defined for x > 0. K₁ diverges // like 1/x at 0 and returns +Inf there. func BesselK1(a *Array) (*Array, error) { return a.realFunc("BesselK1", besselK1) } // BesselIn returns the modified Bessel function of the first kind of // integer order n, Iₙ(x), of each element. The recurrence runs in its // stable direction: orders well below |x| climb upward from the // quadrature I₀ and I₁ at O(n) cost, and orders comparable to or above // it run Miller's downward algorithm, which is where the upward climb // would amplify the parasitic K component the seeds carry. The // downward walk starts above the turning point at order |x|, so it // costs O(n + |x|) steps: for an order far below a large argument that // is the difference between a microsecond and minutes, which is why // the climb exists. func BesselIn(n int, a *Array) (*Array, error) { if n < 0 { return nil, errf("BesselIn: order must be ≥ 0, got %d", n) } return a.realFunc("BesselIn", func(x float64) float64 { if n == 0 { return besselI0(x) } if n == 1 { return besselI1(x) } // The relative contamination the climb suffers grows roughly // like e^{n²/x}, and the seeds it climbs from are only as good // as besselI0 and besselI1 are there: below 3.75 those are // polynomial fits worth about 2.5e-8, so a short climb over a // small argument multiplies a weak seed by the condition number // and lands coarser than the downward walk, which renormalises // through the same seed once. The climb is therefore taken only // where its seeds are strong, from besselIUpwardMinX up, and the // split follows n² ≤ 4x inside that: above it the downward walk // is both accurate and affordable, since the order is then // comparable to the argument and O(n + |x|) steps is the // answer's own price. ax := math.Abs(x) if ax >= besselIUpwardMinX && float64(n)*float64(n) <= 4*ax { return besselIUpward(n, x) } return besselInMiller(n, x) }) } // besselIUpwardMinX is the argument from which the upward climb beats // Miller's downward walk on accuracy as well as on cost: the asymptotic // I₀ and I₁ it climbs from are near full precision there, while the // polynomial pair below 3.75 is not. Miller's own cost is O(n + |x|), so // staying with it under this bound costs nothing that matters. const besselIUpwardMinX = 12 // besselIUpward climbs I₀ and I₁ to order n by the upward recurrence // Iₖ₊₁ = Iₖ₋₁ − (2k/x)·Iₖ (the modified kind carries the minus sign // where J carries a plus), the stable direction while the order stays // well below |x|: the parasitic K component the seeds carry decays // relative to I there, whereas near and above the argument the same // recurrence amplifies it, which is what besselInMiller's downward walk // avoids. The quadrature seeds carry the absolute scale and the parity, // so a negative argument needs no extra handling. The caller guarantees // x ≠ 0, 2 ≤ n, n² ≤ 4|x| and |x| ≥ besselIUpwardMinX. func besselIUpward(n int, x float64) float64 { i0, i1 := besselI0(x), besselI1(x) // The quadrature overflows to Inf once |x| passes about 714, where // the true value overflows too: report that instead of letting the // recurrence fold Inf - Inf into NaN. The sign follows the parity // Iₙ(−x) = (−1)ⁿ Iₙ(x). if math.IsInf(i0, 0) || math.IsInf(i1, 0) { if x < 0 && n%2 == 1 { return math.Inf(-1) } return math.Inf(1) } for k := 1; k < n; k++ { i0, i1 = i1, i0-2*float64(k)/x*i1 } return i1 } // besselInMiller evaluates I_n by the downward Miller recurrence. The // upward recurrence for I is unstable once the order passes the // argument (the terms subtract nearly equal neighbours), while the // downward direction amplifies no rounding. The arbitrary seed scale // is removed against the exact I_0 at the end. func besselInMiller(n int, x float64) float64 { if x == 0 { if n == 0 { return 1 } return 0 } // |x| keeps the start above n for negative arguments too: a start // at or below n would never pass the order on the way down and // renormalise against a garbage seed. start := n + int(math.Abs(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 } // Downward recurrence: j_{k−1} = j_{k+1} + 2k/x·j_k. jp, j = j, jp+2*float64(k)/x*j if aj := math.Abs(j); aj > 1e200 { // I grows monotonically down the walk, and the unscaled // seed overflows for tiny x or high orders whose true // value is representable; a common rescale cancels in the // final ratio. jp /= aj j /= aj jn /= aj } } // j now holds the unscaled I_0. The quotient is formed before the // multiplication: above about x = 300 both walk values pass 1e100, // and the product I₀·jₙ overflows even though the answer, which is // the quotient times the seed scale, is an ordinary number. Taking // the product first turned every such evaluation into +Inf. return besselI0(x) * (jn / j) } // BesselKn returns the modified Bessel function of the second kind of // integer order n, Kₙ(x), of each element, by the upward recurrence // Kₙ₊₁ = Kₙ₋₁ + 2n/x·Kₙ from K₀ and K₁, which is the stable direction // for the second-kind functions. K is defined for x > 0 at every // order: outside that domain the answer is not order-dependent but // undefined, so any element that is NaN or non-positive is an error // naming the element, the same contract BesselY enforces at one // point, never a silent NaN or a spurious +Inf. func BesselKn(n int, a *Array) (*Array, error) { if n < 0 { return nil, errf("BesselKn: order must be ≥ 0, got %d", n) } if a.dt == Complex { return nil, errf("BesselKn: complex arrays are not supported") } for i := range a.Len() { if x := a.floatAt(i); math.IsNaN(x) || x <= 0 { return nil, errf("BesselKn: the argument must be positive, got %g at element %d", x, i) } } return a.realFunc("BesselKn", func(x float64) float64 { k0, k1 := besselK0(x), besselK1(x) if n == 0 { return k0 } if n == 1 { return k1 } km := k0 k := k1 for j := 1; j < n; j++ { km, k = k, km+2*float64(j)/x*k } return k }) } // besselI0 evaluates I₀: the standard 3.75-piecewise polynomial fit // below, and the exact integral representation I₀ = (1/π)∫₀^π // e^(x·cos θ) dθ above, where the polynomial's accuracy degrades. func besselI0(x float64) float64 { ax := math.Abs(x) if ax < 3.75 { t := x / 3.75 t2 := t * t return 1 + t2*(3.5156229+t2*(3.0899424+t2*(1.2067492+t2*(0.2659732+t2*(0.0360768+t2*0.0045813))))) } return quadratureBesselI(x, false) } // besselI1 evaluates I₁: the polynomial fit below 3.75, the integral // representation I₁ = (1/π)∫₀^π e^(x·cos θ)·cos θ dθ above. The odd // parity comes out of the quadrature by itself; the small branch // applies the sign of x explicitly. func besselI1(x float64) float64 { ax := math.Abs(x) var r float64 if ax < 3.75 { t := x / 3.75 t2 := t * t r = ax * (0.5 + t2*(0.87890594+t2*(0.51498869+t2*(0.15084934+t2*(0.02658733+t2*(0.00301532+t2*0.00032411)))))) if x < 0 { r = -r } return r } return quadratureBesselI(x, true) } // quadratureBesselI evaluates I₀ (weight false) or I₁ (weight true) // through the exact integral representations over θ ∈ [0, π] by // composite Simpson with 2048 intervals, which resolves the peak at // θ = 0 for any practical |x|. func quadratureBesselI(x float64, weightCos bool) float64 { const intervals = 2048 h := math.Pi / float64(intervals) kern := func(th float64) float64 { v := math.Exp(x * math.Cos(th)) if weightCos { v *= math.Cos(th) } return v } sum := kern(0) + kern(math.Pi) for i := 1; i < intervals; i++ { w := 2.0 if i%2 == 1 { w = 4.0 } sum += w * kern(float64(i)*h) } return sum * h / (3 * math.Pi) } // besselK0 evaluates K₀(x) = ∫₀^∞ e^(−x·cosh t) dt by composite // Simpson quadrature over a truncated domain. The tail beyond the // cutoff falls below 1e-100 for every x > 0, and the integrand is // smooth, so the quadrature carries roughly ten significant digits. // That is the deliberate trade: a provably correct entry-level K // without hand-typed approximation coefficients. func besselK0(x float64) float64 { if x <= 0 { return math.Inf(1) } return integrateCoshKernel(x, func(_ float64, coshU float64) float64 { return math.Exp(-x * coshU) }) } // besselK1 evaluates K₁(x) = ∫₀^∞ e^(−x·cosh t)·cosh t dt, the // derivative partner of K₀, by the same quadrature. func besselK1(x float64) float64 { if x <= 0 { return math.Inf(1) } return integrateCoshKernel(x, func(_ float64, coshU float64) float64 { return math.Exp(-x*coshU) * coshU }) } // integrateCoshKernel integrates ∫₀^∞ kernel(u; x) du where the // kernel carries the factor e^(−x·cosh u). The cutoff follows from // requiring the tail e^(−x·e^U/2) below 1e-100; for large x the // result legitimately underflows to zero like the true value. func integrateCoshKernel(x float64, kernel func(float64, float64) float64) float64 { const intervals = 2001 u := math.Log(200/x) + 2 if u < 2 { u = 2 } h := u / float64(intervals) sum := kernel(0, 1) + kernel(u, math.Cosh(u)) for i := 1; i < intervals; i++ { tu := float64(i) * h w := 2.0 if i%2 == 1 { w = 4.0 } sum += w * kernel(tu, math.Cosh(tu)) } return sum * h / 3 }