511 lines
17 KiB
Go
511 lines
17 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package core
|
||
|
||
import (
|
||
"math"
|
||
|
||
"sourcedock.dev/petrbalvin/tensor/internal/engine"
|
||
)
|
||
|
||
// Elliptic integrals, Jacobi elliptic functions and the Gauss
|
||
// hypergeometric function. The complete integrals use the
|
||
// arithmetic-geometric mean where it is exact (K directly, E through
|
||
// the companion series) and a high-order Gauss-Legendre product where
|
||
// it is not (Pi); the Jacobi functions invert the incomplete integral
|
||
// F by Newton iteration, which makes sn, cn and dn correct by
|
||
// construction: they are the sin, cos and dn of the amplitude whose
|
||
// integral is the argument.
|
||
|
||
// ellipticF evaluates the incomplete integral F(φ, m) = ∫₀^φ dθ / √(1
|
||
// − m·sin²θ) through Carlson's symmetric form
|
||
//
|
||
// F(φ, m) = sin φ · R_F(cos²φ, 1 − m·sin²φ, 1),
|
||
//
|
||
// which is exact for every m ∈ [0, 1], φ ∈ [−π/2, π/2]: the integrand's
|
||
// endpoint singularity at m tending to 1, φ = π/2 is a zero argument
|
||
// of R_F, a regular point of the duplication algorithm. The
|
||
// Gauss-Legendre rule this replaces lost accuracy there (2e-16 at
|
||
// m = 0.99, 3e-3 at m = 1−1e-6), because a product rule cannot follow a
|
||
// square-root singularity.
|
||
func ellipticF(phi, m float64) float64 {
|
||
if m == 0 {
|
||
return phi
|
||
}
|
||
// The integrand has period π and a full period integrates to 2K,
|
||
// so any φ reduces to the principal range: F(φ, m) = F(φ̂, m) +
|
||
// 2n·K(m) with φ̂ = φ − nπ the nearest point of [−π/2, π/2].
|
||
n := math.Round(phi / math.Pi)
|
||
phi -= n * math.Pi
|
||
s, c := math.Sincos(phi)
|
||
f := s * carlsonRF(c*c, 1-m*s*s, 1)
|
||
if n != 0 {
|
||
f += 2 * n * EllipticKScalar(m)
|
||
}
|
||
return f
|
||
}
|
||
|
||
// carlsonRF returns Carlson's symmetric elliptic integral of the first
|
||
// kind, R_F(x, y, z) = ½∫₀^∞ dt/√((t+x)(t+y)(t+z)), by the
|
||
// duplication algorithm (Numerical Recipes, 6.11): each pass averages
|
||
// the three arguments, and the series in the final deviations
|
||
// converges to double precision in a handful of passes. The arguments
|
||
// must be non-negative with at most one zero, which is the case for
|
||
// the calls made here.
|
||
func carlsonRF(x, y, z float64) float64 {
|
||
// The textbook cut-off is 0.0025, which leaves the third-order
|
||
// series in the deviations good to about 1e-10 relative; halving the
|
||
// deviations quadratically costs two extra passes and buys six
|
||
// digits, so the cut is pushed to 1e-9 here.
|
||
const (
|
||
errtol = 1e-9
|
||
c1 = 1.0 / 24
|
||
c2 = 0.1
|
||
c3 = 3.0 / 44
|
||
c4 = 1.0 / 14
|
||
)
|
||
for range 100 {
|
||
sx, sy, sz := math.Sqrt(x), math.Sqrt(y), math.Sqrt(z)
|
||
lambda := sx*(sy+sz) + sy*sz
|
||
x = 0.25 * (x + lambda)
|
||
y = 0.25 * (y + lambda)
|
||
z = 0.25 * (z + lambda)
|
||
ave := (x + y + z) / 3
|
||
delx := (ave - x) / ave
|
||
dely := (ave - y) / ave
|
||
delz := (ave - z) / ave
|
||
if math.Abs(delx) <= errtol && math.Abs(dely) <= errtol && math.Abs(delz) <= errtol {
|
||
e2 := delx*dely - delz*delz
|
||
e3 := delx * dely * delz
|
||
return (1 + (c1*e2-c2-c3*e3)*e2 + c4*e3) / math.Sqrt(ave)
|
||
}
|
||
}
|
||
// The loop above converges in under ten passes for every admissible
|
||
// input; the fallback keeps the contract of never looping for ever.
|
||
return math.NaN()
|
||
}
|
||
|
||
// EllipticK returns the complete elliptic integral of the first kind
|
||
// K(m) = F(π/2, m), evaluated by the arithmetic-geometric mean:
|
||
// K(m) = π / (2·AGM(1, √(1−m))). The parameter convention is m = k².
|
||
// m must be below 1; as m tends to 1 it diverges
|
||
// and K returns +Inf there, m = 1 included as the limit only through
|
||
// the caller's rounding.
|
||
func EllipticK(m *Array) (*Array, error) {
|
||
return m.realFunc("EllipticK", func(v float64) float64 {
|
||
if math.IsNaN(v) {
|
||
return math.NaN()
|
||
}
|
||
if v >= 1 {
|
||
if v == 1 {
|
||
return math.Inf(1)
|
||
}
|
||
return math.NaN()
|
||
}
|
||
if v < 0 {
|
||
// Negative parameter: transform to a positive one,
|
||
// K(−s) = K(s/(1+s))/√(1+s). The complement 1 − s/(1+s) =
|
||
// 1/(1+s) is handed to the AGM directly: for s above 2^53
|
||
// the float64 sum 1+s rounds to s, so the transformed
|
||
// parameter would round to exactly 1 and the AGM would
|
||
// return its round-off floor (~1.8e15) instead of the true
|
||
// K, which stays finite, and decays to 0, for every finite
|
||
// s.
|
||
s := -v
|
||
return agmKFromComplement(1/(1+s)) / math.Sqrt(1+s)
|
||
}
|
||
return EllipticKScalar(v)
|
||
})
|
||
}
|
||
|
||
// EllipticKScalar is K(m) at one point (0 ≤ m < 1) by the AGM.
|
||
func EllipticKScalar(m float64) float64 {
|
||
return agmKFromComplement(1 - m)
|
||
}
|
||
|
||
// agmKFromComplement is K(1−ε) = π/(2·AGM(1, √ε)) evaluated from the
|
||
// complementary parameter ε = 1−m, the form the negative-parameter
|
||
// transform needs: there ε = 1/(1+s) stays exact where 1−ε would round
|
||
// to 1.
|
||
func agmKFromComplement(eps float64) float64 {
|
||
a, b := 1.0, math.Sqrt(eps)
|
||
// The stop test sits a few ulps above machine zero: rounding can
|
||
// pin a and b one ulp apart forever, and any threshold below that
|
||
// is an infinite loop, not extra accuracy.
|
||
for math.Abs(a-b) > 4*epsF*math.Max(1, a) {
|
||
a, b = 0.5*(a+b), math.Sqrt(a*b)
|
||
}
|
||
return math.Pi / (2 * a)
|
||
}
|
||
|
||
// EllipticE returns the complete elliptic integral of the second kind
|
||
// E(m) = ∫₀^{π/2} √(1 − m·sin²θ) dθ through the AGM companion series
|
||
// E = K·(1 − Σ 2^{n−1} c_n²), with c_n² = a_n² − b_n² the AGM
|
||
// remainders. m = 1 gives 1; m > 1 is NaN; negative m transforms like
|
||
// K's does.
|
||
func EllipticE(m *Array) (*Array, error) {
|
||
return m.realFunc("EllipticE", ellipticEScalar)
|
||
}
|
||
|
||
// ellipticEScalar is E(m) at one point.
|
||
func ellipticEScalar(m float64) float64 {
|
||
if math.IsNaN(m) {
|
||
return math.NaN()
|
||
}
|
||
if m == 1 {
|
||
return 1
|
||
}
|
||
if m > 1 {
|
||
return math.NaN()
|
||
}
|
||
if m < 0 {
|
||
// E(−s) = √(1+s)·E(s/(1+s)).
|
||
s := -m
|
||
return math.Sqrt(1+s) * ellipticEScalar(s/(1+s))
|
||
}
|
||
a, b := 1.0, math.Sqrt(1-m)
|
||
k := EllipticKScalar(m)
|
||
sum := 0.0
|
||
pow2 := 0.5 // 2^{n-1} starting at n = 1
|
||
for math.Abs(a-b) > 4*epsF*math.Max(1, a) {
|
||
c2 := a*a - b*b
|
||
sum += pow2 * c2
|
||
pow2 *= 2
|
||
a, b = 0.5*(a+b), math.Sqrt(a*b)
|
||
}
|
||
return k * (1 - sum)
|
||
}
|
||
|
||
// EllipticPi returns the complete elliptic integral of the third kind
|
||
// Π(n, m) = ∫₀^{π/2} dθ / ((1 − n·sin²θ)√(1 − m·sin²θ)) element-wise
|
||
// over paired arrays, through Carlson's symmetric forms
|
||
//
|
||
// Π(n, m) = R_F(0, 1−m, 1) + (n/3)·R_J(0, 1−m, 1, 1−n),
|
||
//
|
||
// whose duplication algorithms are exact at the singular ends of the
|
||
// parameter square: full double precision for every n < 1 and m < 1,
|
||
// where the 64-point product rule this replaces lost seven digits at
|
||
// m = 1−1e-6 and more as n approached 1 (measured against mpmath: the
|
||
// worst relative error over the pinned table is 6e-16). At n = 1 and
|
||
// at m = 1 the integral diverges: the value there is +Inf, and beyond
|
||
// it NaN.
|
||
func EllipticPi(n, m *Array) (*Array, error) {
|
||
if !sameShape(n.shape, m.shape) {
|
||
return nil, errf("EllipticPi: shape mismatch %s vs %s", shapeText(n.shape), shapeText(m.shape))
|
||
}
|
||
if n.dt == Complex || m.dt == Complex {
|
||
return nil, errf("EllipticPi: complex arrays are not supported")
|
||
}
|
||
out := &Array{shape: append([]int{}, n.shape...), dt: Float}
|
||
out.alloc(n.Len())
|
||
engine.Parallel(n.Len(), func(s, e int) {
|
||
for i := s; i < e; i++ {
|
||
out.floats[i] = ellipticPiScalar(n.floatAt(i), m.floatAt(i))
|
||
}
|
||
})
|
||
return out, nil
|
||
}
|
||
|
||
// ellipticPiScalar is Π(n, m) at one point, through Carlson's
|
||
// symmetric forms:
|
||
//
|
||
// Π(n, m) = R_F(0, 1−m, 1) + (n/3)·R_J(0, 1−m, 1, 1−n).
|
||
//
|
||
// Both forms are exact at the singular ends of the parameter square,
|
||
// which is where the product rule this replaces lost its digits.
|
||
func ellipticPiScalar(n, m float64) float64 {
|
||
if math.IsNaN(n) || math.IsNaN(m) || m >= 1 || n >= 1 {
|
||
if m == 1 || n == 1 {
|
||
return math.Inf(1)
|
||
}
|
||
return math.NaN()
|
||
}
|
||
rf := carlsonRF(0, 1-m, 1)
|
||
if n == 0 {
|
||
return rf
|
||
}
|
||
return rf + n/3*carlsonRJ(0, 1-m, 1, 1-n)
|
||
}
|
||
|
||
// carlsonRC returns Carlson's degenerate symmetric integral
|
||
// R_C(x, y) = R_F(x, y, y) for x ≥ 0 and y ≠ 0, by the duplication
|
||
// algorithm with the single-deviation series (Carlson 1995,
|
||
// "Numerical computation of real or complex elliptic integrals",
|
||
// (16)-(20)). A negative y is its Cauchy principal value, through the
|
||
// paper's (21); that case never arises for the calls made here, and is
|
||
// implemented so the function stands on its own.
|
||
func carlsonRC(x, y float64) float64 {
|
||
// r is the target relative truncation error; 1e-16 asks for double
|
||
// precision, and the seven-term series is good to that.
|
||
const r = 1e-16
|
||
if y < 0 {
|
||
// (21) with the positive magnitude: R_C(x, −Y) =
|
||
// sqrt(x/(x+Y))·R_C(x+Y, Y).
|
||
return math.Sqrt(x/(x-y)) * carlsonRC(x-y, -y)
|
||
}
|
||
a0 := (x + 2*y) / 3
|
||
q := math.Pow(3*r, -0.125) * math.Abs(a0-x)
|
||
a, xc, yc := a0, x, y
|
||
pow4 := 1.0 // 4^{−m}
|
||
for range 200 {
|
||
sx, sy := math.Sqrt(xc), math.Sqrt(yc)
|
||
lambda := 2*sx*sy + yc
|
||
an := (a + lambda) / 4
|
||
pow4n := pow4 / 4
|
||
if pow4n*q < math.Abs(an) {
|
||
s := (y - a0) / (an / pow4n)
|
||
s2 := s * s
|
||
series := 1 + (3.0/10)*s2 + (1.0/7)*s2*s + (3.0/8)*s2*s2 +
|
||
(9.0/22)*s2*s2*s + (159.0/208)*s2*s2*s2 + (9.0/8)*s2*s2*s2*s
|
||
return series / math.Sqrt(an)
|
||
}
|
||
xc, yc = (xc+lambda)/4, (yc+lambda)/4
|
||
a, pow4 = an, pow4n
|
||
}
|
||
return math.NaN()
|
||
}
|
||
|
||
// carlsonRJ returns Carlson's symmetric integral
|
||
// R_J(x, y, z, p) = (3/2)∫₀^∞ dt/((t+p)√((t+x)(t+y)(t+z))) for x, y,
|
||
// z ≥ 0 with at most one zero and p > 0, by the duplication theorem
|
||
// with the five-variable series (Carlson 1995, (24)-(32)): each pass
|
||
// averages the variables through lambda and accumulates the
|
||
// R_C(1, 1+e) correction the theorem contributes, then a sixth-order
|
||
// series in the deviations from the mean finishes the job.
|
||
func carlsonRJ(x, y, z, p float64) float64 {
|
||
const r = 1e-16
|
||
if p <= 0 {
|
||
// The principal value for negative p is the paper's (33),
|
||
// which needs a permutation of x, y, z; none of the callers
|
||
// reaches it, so it is refused rather than approximated.
|
||
return math.NaN()
|
||
}
|
||
a0 := (x + y + z + 2*p) / 5
|
||
delta := (p - x) * (p - y) * (p - z)
|
||
q := math.Pow(r/4, -1.0/6) * math.Max(math.Max(math.Abs(a0-x), math.Abs(a0-y)),
|
||
math.Max(math.Abs(a0-z), math.Abs(a0-p)))
|
||
a, xc, yc, zc, pc := a0, x, y, z, p
|
||
pow4 := 1.0
|
||
total := 0.0
|
||
for range 200 {
|
||
sx, sy, sz, sp := math.Sqrt(xc), math.Sqrt(yc), math.Sqrt(zc), math.Sqrt(pc)
|
||
lambda := sx*sy + sx*sz + sy*sz
|
||
an := (a + lambda) / 4
|
||
pow4n := pow4 / 4
|
||
// The m-th correction carries 4^{−3m}; pow4 is 4^{−m} already.
|
||
d := (sp + sx) * (sp + sy) * (sp + sz)
|
||
e := pow4 * pow4 * pow4 * delta / (d * d)
|
||
total += 6 * pow4 / d * carlsonRC(1, 1+e)
|
||
if pow4n*q < math.Abs(an) {
|
||
scaled := an / pow4n // 4^n·A_n
|
||
xx := (a0 - x) / scaled
|
||
yy := (a0 - y) / scaled
|
||
zz := (a0 - z) / scaled
|
||
pp := (-xx - yy - zz) / 2
|
||
e2 := xx*yy + xx*zz + yy*zz - 3*pp*pp
|
||
e3 := xx*yy*zz + 2*e2*pp + 4*pp*pp*pp
|
||
e4 := (2*xx*yy*zz + e2*pp + 3*pp*pp*pp) * pp
|
||
e5 := xx * yy * zz * pp * pp
|
||
series := 1 - (3.0/14)*e2 + (1.0/6)*e3 + (9.0/88)*e2*e2 -
|
||
(3.0/22)*e4 - (9.0/52)*e2*e3 + (3.0/26)*e5
|
||
return pow4n*series/(an*math.Sqrt(an)) + total
|
||
}
|
||
xc, yc, zc, pc = (xc+lambda)/4, (yc+lambda)/4, (zc+lambda)/4, (pc+lambda)/4
|
||
a, pow4 = an, pow4n
|
||
}
|
||
return math.NaN()
|
||
}
|
||
|
||
// EllipticFScalar is the incomplete elliptic integral of the first
|
||
// kind F(φ, m) = ∫₀^φ dθ/√(1−m·sin²θ) at one point, through Carlson's
|
||
// symmetric form; valid for every m ∈ [0, 1) and any real φ. The
|
||
// inverse view is the amplitude: u = F(φ, m) means φ = am(u, m), the
|
||
// reading behind the Jacobi functions.
|
||
func EllipticFScalar(phi, m float64) float64 {
|
||
return ellipticF(phi, m)
|
||
}
|
||
|
||
// JacobiSN returns sn(u, m), the Jacobi elliptic sine, element-wise
|
||
// over u with the parameter m shared: sn inverts the incomplete
|
||
// integral u = F(φ, m) through φ, giving sn = sin φ. The same
|
||
// inversion serves cn and dn, which are the cos and the sqrt factor
|
||
// of the same amplitude. Valid for m ∈ [0, 1) and any real u, where
|
||
// m = 0 degenerates to the circular functions; m = 1 is refused
|
||
// because the amplitude would have to travel through the singular
|
||
// complete integral.
|
||
func JacobiSN(u *Array, m float64) (*Array, error) {
|
||
return jacobi(u, m, true, func(phi, mm float64) float64 { return math.Sin(phi) })
|
||
}
|
||
|
||
// JacobiCN returns cn(u, m) element-wise.
|
||
func JacobiCN(u *Array, m float64) (*Array, error) {
|
||
return jacobi(u, m, false, func(phi, mm float64) float64 { return math.Cos(phi) })
|
||
}
|
||
|
||
// JacobiDN returns dn(u, m) element-wise.
|
||
func JacobiDN(u *Array, m float64) (*Array, error) {
|
||
return jacobi(u, m, false, func(phi, mm float64) float64 {
|
||
return math.Sqrt(1 - mm*math.Sin(phi)*math.Sin(phi))
|
||
})
|
||
}
|
||
|
||
// jacobi inverts u = F(φ, m) by a bracketed Newton iteration and maps
|
||
// the amplitude through f.
|
||
func jacobi(u *Array, m float64, odd bool, f func(phi, mm float64) float64) (*Array, error) {
|
||
if u.dt == Complex {
|
||
return nil, errf("Jacobi: complex arrays are not supported")
|
||
}
|
||
if math.IsNaN(m) || m < 0 || m >= 1 {
|
||
return nil, errf("Jacobi: the parameter m must lie in [0, 1), got %g", m)
|
||
}
|
||
// K scales the period; the amplitude search brackets φ in [0, hi]
|
||
// by doubling until F covers u.
|
||
k := EllipticKScalar(m)
|
||
out := &Array{shape: append([]int{}, u.shape...), dt: Float}
|
||
out.alloc(u.Len())
|
||
engine.Parallel(u.Len(), func(s, e int) {
|
||
for i := s; i < e; i++ {
|
||
uu := u.floatAt(i)
|
||
// Sign symmetry first: work with |u|.
|
||
sign := 1.0
|
||
if uu < 0 {
|
||
sign = -1
|
||
uu = -uu
|
||
}
|
||
// Bracket: F(φ) ≥ φ·(1−m)^... the integrand is at least
|
||
// 1 on [0, π/2] and periodic beyond; hi = u + K covers
|
||
// every u ≥ 0 because F(u + K-margin)... doubling is the
|
||
// safe route.
|
||
hi := math.Max(uu, k)
|
||
for ellipticF(hi, m) < uu {
|
||
hi *= 2
|
||
}
|
||
lo := 0.0
|
||
phi := 0.5 * (lo + hi)
|
||
for range 200 {
|
||
// Newton from the midpoint of the current bracket,
|
||
// re-bracketed every step: F is strictly increasing.
|
||
val := ellipticF(phi, m)
|
||
if val < uu {
|
||
lo = phi
|
||
} else {
|
||
hi = phi
|
||
}
|
||
den := math.Sqrt(1 - m*math.Sin(phi)*math.Sin(phi))
|
||
// F'(φ) = 1/den, so the Newton step multiplies by den.
|
||
next := phi + (uu-val)*den
|
||
if next <= lo || next >= hi {
|
||
next = 0.5 * (lo + hi)
|
||
}
|
||
if math.Abs(next-phi) < 1e-16*math.Max(1, math.Abs(phi)) {
|
||
phi = next
|
||
break
|
||
}
|
||
phi = next
|
||
}
|
||
if odd {
|
||
out.floats[i] = sign * f(phi, m)
|
||
} else {
|
||
out.floats[i] = f(phi, m) // dn is an even function
|
||
}
|
||
}
|
||
})
|
||
return out, nil
|
||
}
|
||
|
||
// JacobiCDScalar is cd(u, m) = cn(u, m)/dn(u, m) at one point, by the
|
||
// Gauss AGM: the descending Landen sequence converges quadratically
|
||
// and the amplitude folds back down the chain, so a handful of passes
|
||
// covers any u. The parameter convention is m = k². Valid for m ∈ [0,
|
||
// 1) and any real u, where m = 0 degenerates cd to cos. This is the
|
||
// function the elliptic rational function needs for the zeroes of a
|
||
// Cauer filter design.
|
||
func JacobiCDScalar(u, m float64) float64 {
|
||
if math.IsNaN(m) || m < 0 || m >= 1 || math.IsNaN(u) {
|
||
return math.NaN()
|
||
}
|
||
if m == 0 {
|
||
return math.Cos(u)
|
||
}
|
||
// The AGM chain: a ascends to the mean, b descends, c is half
|
||
// their gap and vanishes quadratically.
|
||
k := EllipticKScalar(m)
|
||
// cd has period 4K and antisymmetry about 2K: reduce to [0, 2K)
|
||
// and carry the sign.
|
||
sign := 1.0
|
||
u = math.Mod(u, 4*k)
|
||
if u < 0 {
|
||
u += 4 * k
|
||
}
|
||
if u >= 2*k {
|
||
u -= 2 * k
|
||
sign = -1
|
||
}
|
||
var a, b, c [64]float64
|
||
a[0], b[0], c[0] = 1, math.Sqrt(1-m), math.Sqrt(m)
|
||
n := 0
|
||
for n < 63 {
|
||
n++
|
||
a[n] = 0.5 * (a[n-1] + b[n-1])
|
||
b[n] = math.Sqrt(a[n-1] * b[n-1])
|
||
c[n] = 0.5 * (a[n-1] - b[n-1])
|
||
// The gap between a and b stops shrinking a few ulps above
|
||
// machine zero, so the stop test carries the same floor the
|
||
// complete integral's AGM uses; below it more passes would be
|
||
// an infinite loop, not extra accuracy.
|
||
if math.Abs(c[n]) <= 4*epsF*math.Abs(a[n]) {
|
||
break
|
||
}
|
||
}
|
||
// The amplitude climbs the chain, then folds back down it.
|
||
phi := float64(int(1)<<uint(n)) * a[n] * u
|
||
for i := n; i > 0; i-- {
|
||
phi = 0.5 * (phi + math.Asin(c[i]/a[i]*math.Sin(phi)))
|
||
}
|
||
return sign * math.Cos(phi) / math.Sqrt(1-m*math.Sin(phi)*math.Sin(phi))
|
||
}
|
||
|
||
// Hypergeometric2F1 returns the Gauss hypergeometric function
|
||
// ₂F₁(a, b; c; x) = Σ (a)_k (b)_k / (c)_k · x^k / k! element-wise
|
||
// over x, with a, b, c scalar parameters. The series converges for
|
||
// |x| < 1; a or b a non-positive integer terminates it as a
|
||
// polynomial. A non-positive integer c is an error. Outside the disc
|
||
// of convergence the result is the Gauss value at x = 1 when c > a+b
|
||
// (finite), the divergence limit +Inf when x = 1 and c ≤ a+b, and NaN
|
||
// for every other |x| ≥ 1.
|
||
func Hypergeometric2F1(a, b, c float64, x *Array) (*Array, error) {
|
||
if c == math.Trunc(c) && c <= 0 {
|
||
return nil, errf("Hypergeometric2F1: c must not be a non-positive integer, got %g", c)
|
||
}
|
||
terminated := (a == math.Trunc(a) && a <= 0) || (b == math.Trunc(b) && b <= 0)
|
||
return x.realFunc("Hypergeometric2F1", func(v float64) float64 {
|
||
if math.IsNaN(v) {
|
||
return math.NaN()
|
||
}
|
||
if math.Abs(v) >= 1 && !terminated {
|
||
if v == 1 {
|
||
// The Gauss value at x = 1 exists when c > a+b.
|
||
if c > a+b {
|
||
return math.Gamma(c) * math.Gamma(c-a-b) / (math.Gamma(c-a) * math.Gamma(c-b))
|
||
}
|
||
return math.Inf(1)
|
||
}
|
||
return math.NaN()
|
||
}
|
||
term := 1.0
|
||
sum := 1.0
|
||
for k := 1; k <= 100000; k++ {
|
||
term *= (a + float64(k-1)) * (b + float64(k-1)) / (c + float64(k-1)) * v / float64(k)
|
||
sum += term
|
||
if math.Abs(term) < 1e-18*math.Abs(sum) {
|
||
break
|
||
}
|
||
if math.IsInf(sum, 0) {
|
||
return sum
|
||
}
|
||
}
|
||
return sum
|
||
})
|
||
}
|