mirror of
https://github.com/gonum/gonum.git
synced 2025-09-27 03:26:04 +08:00
234 lines
5.6 KiB
Go
234 lines
5.6 KiB
Go
// Copyright ©2025 The Gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package mathext
|
|
|
|
import (
|
|
"math"
|
|
|
|
"gonum.org/v1/gonum/mathext/internal/cephes"
|
|
)
|
|
|
|
// The hypergeometric relations used here are from:
|
|
//
|
|
// M. Abramowitz and I. A. Stegun 1965. Handbook of Mathematical Functions, New York: Dover
|
|
//
|
|
// It is available online via https://personal.math.ubc.ca/~cbm/aands/frameindex.htm
|
|
|
|
// Hypergeo returns the value of the Gaussian Hypergeometric function at z.
|
|
// For |z| < 1, this implementation follows the Cephes library.
|
|
// For |z| ≥ 1, this implementation performs analytic continuation via relevant Hypergeometric identities.
|
|
//
|
|
// See https://en.wikipedia.org/wiki/Hypergeometric_function for more details.
|
|
func Hypergeo(a, b, c, z float64) float64 {
|
|
// Fix numerical issues for large |a|, |b| or |c| using equations 15.2.10 and 15.2.12.
|
|
// Simplify discussion by ensuring |a| > |b|.
|
|
aa, ab, ac := math.Abs(a), math.Abs(b), math.Abs(c)
|
|
if ab > aa {
|
|
return Hypergeo(b, a, c, z)
|
|
}
|
|
acFar := (aa/ac < 0.9 || ac/aa < 0.9)
|
|
if a < -50 && ac > 2 && acFar {
|
|
return eqn15_2_10(a, b, c, z)
|
|
}
|
|
if c < -50 && aa > 2 && acFar {
|
|
return eqn15_2_12(a, b, c, z)
|
|
}
|
|
|
|
// Canonical case |z| < 1 supported by Cephes.
|
|
if math.Abs(z) < 1 {
|
|
if y, ok := isCephesBug(a, b, c, z); ok {
|
|
return y
|
|
}
|
|
return cephes.Hyp2f1(a, b, c, z)
|
|
}
|
|
|
|
// Function undefined between the 1 and Inf branch points.
|
|
if z >= 1 {
|
|
return math.NaN()
|
|
}
|
|
|
|
// Special case for z=-1, use equation 15.3.4 in Abramowitz.
|
|
if z == -1 {
|
|
return eqn15_3_4(a, b, c, z)
|
|
}
|
|
|
|
// When a or b is a non-positive integer, Hypergeo reduces to
|
|
// the finite polynomial described in equation 15.4.1.
|
|
if isNonPosInt(a) {
|
|
return eqn15_4_1(int(-a), b, c, z)
|
|
}
|
|
if isNonPosInt(b) {
|
|
return eqn15_4_1(int(-b), a, c, z)
|
|
}
|
|
|
|
// Analytic continuation formula contains NaNs from Gamma(a-b) when a-b is a non-positive integer.
|
|
// Fix this by making a and b different via equations 15.3.3 and 15.3.4.
|
|
if isNonPosInt(c-a) || isNonPosInt(c-b) {
|
|
return eqn15_3_3(a, b, c, z)
|
|
}
|
|
if isNonPosInt(a - b) {
|
|
return eqn15_3_4(a, b, c, z)
|
|
}
|
|
|
|
// Analytic continuation based on equation 15.3.7:
|
|
// https://www.johndcook.com/blog/2021/11/03/escaping-the-unit-disk/
|
|
y1 := cephes.Hyp2f1(a, 1-c+a, 1-b+a, 1/z)
|
|
y1 *= math.Gamma(c) / math.Gamma(b) * math.Gamma(b-a) / math.Gamma(c-a) * math.Pow(-z, -a)
|
|
y2 := cephes.Hyp2f1(b, 1-c+b, 1-a+b, 1/z)
|
|
y2 *= math.Gamma(c) / math.Gamma(a) * math.Gamma(a-b) / math.Gamma(c-b) * math.Pow(-z, -b)
|
|
return y1 + y2
|
|
}
|
|
|
|
func eqn15_2_10(a, b, c, x float64) float64 {
|
|
t := a - math.Round(a)
|
|
var f2 float64
|
|
f1 := Hypergeo(t, b, c, x)
|
|
var f0 float64
|
|
mai := int(math.Round(math.Abs(a)))
|
|
if a < 0 {
|
|
f0 = Hypergeo(t-1, b, c, x)
|
|
t--
|
|
for n := 1; n < mai; n++ {
|
|
f2 = f1
|
|
f1 = f0
|
|
f0 = -(2*t-c-t*x+b*x)/(c-t)*f1 - t*(x-1)/(c-t)*f2
|
|
t--
|
|
}
|
|
} else {
|
|
f0 = Hypergeo(t+1, b, c, x)
|
|
t++
|
|
for n := 1; n < mai; n++ {
|
|
f2 = f1
|
|
f1 = f0
|
|
f0 = -((2*t-c-t*x+b*x)*f1 + (c-t)*f2) / (t * (x - 1))
|
|
t++
|
|
}
|
|
}
|
|
return f0
|
|
}
|
|
|
|
func eqn15_2_12(a, b, c, x float64) float64 {
|
|
t := c - math.Round(c)
|
|
var f2 float64
|
|
f1 := Hypergeo(a, b, t, x)
|
|
var f0 float64
|
|
mci := int(math.Round(math.Abs(c)))
|
|
if c < 0 {
|
|
f0 = Hypergeo(a, b, t-1, x)
|
|
t--
|
|
for n := 1; n < mci; n++ {
|
|
f2 = f1
|
|
f1 = f0
|
|
f0 = -t*(t-1-(2*t-a-b-1)*x)/t/(t-1)/(x-1)*f1 - (t-a)*(t-b)*x/t/(t-1)/(x-1)*f2
|
|
t--
|
|
}
|
|
} else {
|
|
f0 = Hypergeo(a, b, t+1, x)
|
|
t++
|
|
for n := 1; n < mci; n++ {
|
|
f2 = f1
|
|
f1 = f0
|
|
f0 = -(t*(t-1-(2*t-a-b-1)*x)*f1 + t*(t-1)*(x-1)*f2) / ((t - a) * (t - b) * x)
|
|
t++
|
|
}
|
|
}
|
|
return f0
|
|
}
|
|
|
|
func eqn15_3_3(a, b, c, z float64) float64 {
|
|
y := Hypergeo(c-a, c-b, c, z)
|
|
return math.Pow(1-z, c-a-b) * y
|
|
}
|
|
|
|
func eqn15_3_4(a, b, c, z float64) float64 {
|
|
y := Hypergeo(a, c-b, c, z/(z-1))
|
|
return math.Pow(1-z, -a) * y
|
|
}
|
|
|
|
func eqn15_4_1(m int, b, c, z float64) float64 {
|
|
// lzn is log(z^n / n!).
|
|
var lzn float64
|
|
var lznSign int = 1
|
|
// sum is the sum of the evaluated polynomial.
|
|
var sum float64 = 1
|
|
for n := 1; n <= m; n++ {
|
|
lzn += math.Log(math.Abs(z)) - math.Log(float64(n))
|
|
if z < 0 {
|
|
lznSign *= -1
|
|
}
|
|
|
|
var w float64
|
|
var sign int = 1
|
|
lphC, sn := lpochhammer(c, n)
|
|
if sn == 0 { // divide by zero
|
|
return math.NaN()
|
|
}
|
|
w -= lphC
|
|
sign *= sn
|
|
|
|
lphM, sn := lpochhammer(float64(-m), n)
|
|
if sn == 0 {
|
|
continue
|
|
}
|
|
w += lphM
|
|
sign *= sn
|
|
|
|
lphB, sn := lpochhammer(b, n)
|
|
if sn == 0 {
|
|
continue
|
|
}
|
|
w += lphB
|
|
sign *= sn
|
|
|
|
sum += float64(sign) * float64(lznSign) * math.Exp(w+lzn)
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func lpochhammer(x float64, n int) (float64, int) {
|
|
var y float64
|
|
var sign int = 1
|
|
for k := range n {
|
|
xk := x + float64(k)
|
|
if xk == 0 {
|
|
return math.NaN(), 0
|
|
}
|
|
|
|
y += math.Log(math.Abs(xk))
|
|
if xk < 0 {
|
|
sign *= -1
|
|
}
|
|
}
|
|
return y, sign
|
|
}
|
|
|
|
// isCephesBug checks whether the received arguments hit a Cephes bug,
|
|
// occurring when z ≈ 1 and |c-a-b| is a negative integer.
|
|
// If the bug is hit, isCephesBug returns the remedied value.
|
|
//
|
|
// This bug concerns Cephes' evaluation of mathext.Digamma on negative integers,
|
|
// where it is mathematically undefined.
|
|
//
|
|
// For reference, scipy also found this workaround necessary in:
|
|
// https://github.com/scipy/scipy/commit/1dfea85b34e8cbd868f9fc43345898084ba73aab
|
|
func isCephesBug(a, b, c, z float64) (float64, bool) {
|
|
const cephesEPS = 1e-13
|
|
d := c - a - b
|
|
id := math.Round(d)
|
|
criterion := z > 0.9 && math.Abs(d-id) < cephesEPS && id < 0
|
|
if !criterion {
|
|
return math.NaN(), false
|
|
}
|
|
|
|
// We hit the bug, so use power series straight away.
|
|
y, _ := cephes.Hys2f1(a, b, c, z)
|
|
return y, true
|
|
}
|
|
|
|
func isNonPosInt(x float64) bool {
|
|
_, frac := math.Modf(x)
|
|
return x <= 0 && frac == 0
|
|
}
|