feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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
|
||||
}
|
||||
Reference in New Issue
Block a user