165 lines
4.6 KiB
Go
165 lines
4.6 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package core
|
||
|
||
import "math"
|
||
|
||
// The exponential-integral and polygamma family. Element-wise over
|
||
// real arrays; ints and float32 promote, complex inputs are refused.
|
||
|
||
// ExpIntegralE1 returns the exponential integral E1(x) =
|
||
// ∫ₓ^∞ e^(−t)/t dt of each element, defined for x > 0. A power series
|
||
// runs for x ≤ 2 and a continued fraction above, both accurate to
|
||
// near machine precision.
|
||
func ExpIntegralE1(a *Array) (*Array, error) {
|
||
return a.realFunc("ExpIntegralE1", expIntegralE1)
|
||
}
|
||
|
||
func expIntegralE1(x float64) float64 {
|
||
if x <= 0 {
|
||
return math.NaN()
|
||
}
|
||
if x <= 2 {
|
||
// E1(x) = −γ − ln x − Σ_{k≥1} (−x)^k/(k·k!).
|
||
sum := 0.0
|
||
term := 1.0
|
||
for k := 1; k < 200; k++ {
|
||
term = -term * x / float64(k)
|
||
add := term / float64(k)
|
||
sum += add
|
||
if math.Abs(add) < 1e-18*math.Abs(sum) {
|
||
break
|
||
}
|
||
}
|
||
return -eulerGamma - math.Log(x) - sum
|
||
}
|
||
// Gauss continued fraction of the incomplete gamma function at
|
||
// a = 0, evaluated bottom-up from a generous depth: E1 = e^(-x)/f
|
||
// with f = x+1 - 1^2/(x+3 - 2^2/(x+5 - 3^2/(x+7 - ...))). The
|
||
// numerators grow quadratically, so a hundred levels leave the tail
|
||
// far below double rounding for every x in this branch.
|
||
const cfDepth = 100
|
||
f := x + 2*float64(cfDepth) + 1
|
||
for i := cfDepth; i >= 1; i-- {
|
||
f = (x + 2*float64(i) - 1) - float64(i*i)/f
|
||
}
|
||
return math.Exp(-x) / f
|
||
}
|
||
|
||
// ExpIntegralEi returns the exponential integral Ei(x) of each
|
||
// element, defined for real x ≠ 0 by the Cauchy principal value.
|
||
// Negative x routes through E1; positive x uses the convergent series
|
||
// with the logarithmic singularity subtracted.
|
||
func ExpIntegralEi(a *Array) (*Array, error) {
|
||
return a.realFunc("ExpIntegralEi", func(x float64) float64 {
|
||
if x < 0 {
|
||
return -expIntegralE1(-x)
|
||
}
|
||
if x == 0 {
|
||
return math.Inf(-1)
|
||
}
|
||
if x >= 40 {
|
||
// Asymptotic expansion summed to its smallest term at
|
||
// k ≈ x. The convergent series needs k proportional to x
|
||
// (already past 300 terms at x = 250), so the old fixed
|
||
// cap silently truncated it; from x = 40 on, the omitted
|
||
// asymptotic tail sits below double rounding. The exp/log
|
||
// shift evaluates e^x/x without overflowing before the
|
||
// true value does.
|
||
t, sum := 1.0, 1.0
|
||
for k := 1; k <= int(x); k++ {
|
||
t *= float64(k) / x
|
||
sum += t
|
||
}
|
||
return math.Exp(x-math.Log(x)) * sum
|
||
}
|
||
sum := 0.0
|
||
term := 1.0
|
||
for k := 1; k < 300; k++ {
|
||
term *= x / float64(k)
|
||
add := term / float64(k)
|
||
sum += add
|
||
if math.Abs(add) < 1e-18*math.Abs(sum) {
|
||
break
|
||
}
|
||
}
|
||
return eulerGamma + math.Log(x) + sum
|
||
})
|
||
}
|
||
|
||
// Digamma returns the digamma function ψ(x) = d/dx ln Γ(x) of each
|
||
// element. A recurrence lifts the argument above 20, then the
|
||
// asymptotic series applies; the reflection formula covers x < 0.
|
||
func Digamma(a *Array) (*Array, error) {
|
||
return a.realFunc("Digamma", digamma)
|
||
}
|
||
|
||
func digamma(x float64) float64 {
|
||
if math.IsNaN(x) || math.IsInf(x, 0) {
|
||
return math.NaN()
|
||
}
|
||
result := 0.0
|
||
if x <= 0 && x == math.Floor(x) {
|
||
return math.NaN()
|
||
}
|
||
if x < 0 {
|
||
// Reflection: ψ(1−x) = ψ(x) + π·cot(πx).
|
||
result = -math.Pi / math.Tan(math.Pi*x)
|
||
x = 1 - x
|
||
}
|
||
for x < 20 {
|
||
result -= 1 / x
|
||
x++
|
||
}
|
||
inv := 1 / x
|
||
inv2 := inv * inv
|
||
result += math.Log(x) - 0.5*inv - inv2*(1.0/12-inv2*(1.0/120-inv2*(1.0/252-inv2*(1.0/240))))
|
||
return result
|
||
}
|
||
|
||
// Trigamma returns the trigamma function ψ′(x), the derivative of the
|
||
// digamma function, of each element. The same recurrence and
|
||
// asymptotic strategy as Digamma applies.
|
||
func Trigamma(a *Array) (*Array, error) {
|
||
return a.realFunc("Trigamma", trigamma)
|
||
}
|
||
|
||
func trigamma(x float64) float64 {
|
||
if math.IsNaN(x) || math.IsInf(x, 0) {
|
||
return math.NaN()
|
||
}
|
||
result := 0.0
|
||
reflected := false
|
||
if x <= 0 && x == math.Floor(x) {
|
||
return math.NaN()
|
||
}
|
||
if x < 0 {
|
||
// Reflection: ψ′(x) + ψ′(1−x) = π²/sin²(πx), so ψ′(x) is the
|
||
// constant minus the lifted value ψ′(1−x): the recurrence and
|
||
// the asymptotic below subtract instead of add, mirroring the
|
||
// digamma structure above.
|
||
result = math.Pi * math.Pi / (math.Sin(math.Pi*x) * math.Sin(math.Pi*x))
|
||
x = 1 - x
|
||
reflected = true
|
||
}
|
||
for x < 20 {
|
||
if reflected {
|
||
result -= 1 / (x * x)
|
||
} else {
|
||
result += 1 / (x * x)
|
||
}
|
||
x++
|
||
}
|
||
inv := 1 / x
|
||
inv2 := inv * inv
|
||
tail := inv*(1+0.5*inv) + inv*inv2*(1.0/6-inv2*(1.0/30-inv2*(1.0/42-inv2*(1.0/30))))
|
||
if reflected {
|
||
return result - tail
|
||
}
|
||
return result + tail
|
||
}
|
||
|
||
// eulerGamma is the Euler-Mascheroni constant.
|
||
const eulerGamma = 0.57721566490153286060651209008240243
|