Sam Chaudry
Upload folder using huggingface_hub
7885a28 verified
raw
history blame
34.7 kB
/* Implementation of Gauss's hypergeometric function for complex values.
*
* This implementation is based on the Fortran implementation by Shanjie Zhang and
* Jianming Jin included in specfun.f [1]_. Computation of Gauss's hypergeometric
* function involves handling a patchwork of special cases. By default the Zhang and
* Jin implementation has been followed as closely as possible except for situations where
* an improvement was obvious. We've attempted to document the reasons behind decisions
* made by Zhang and Jin and to document the reasons for deviating from their implementation
* when this has been done. References to the NIST Digital Library of Mathematical
* Functions [2]_ have been added where they are appropriate. The review paper by
* Pearson et al [3]_ is an excellent resource for best practices for numerical
* computation of hypergeometric functions. We have followed this review paper
* when making improvements to and correcting defects in Zhang and Jin's
* implementation. When Pearson et al propose several competing alternatives for a
* given case, we've used our best judgment to decide on the method to use.
*
* Author: Albert Steppi
*
* Distributed under the same license as Scipy.
*
* References
* ----------
* .. [1] S. Zhang and J.M. Jin, "Computation of Special Functions", Wiley 1996
* .. [2] NIST Digital Library of Mathematical Functions. http://dlmf.nist.gov/,
* Release 1.1.1 of 2021-03-15. F. W. J. Olver, A. B. Olde Daalhuis,
* D. W. Lozier, B. I. Schneider, R. F. Boisvert, C. W. Clark, B. R. Miller,
* B. V. Saunders, H. S. Cohl, and M. A. McClain, eds.
* .. [3] Pearson, J.W., Olver, S. & Porter, M.A.
* "Numerical methods for the computation of the confluent and Gauss
* hypergeometric functions."
* Numer Algor 74, 821-866 (2017). https://doi.org/10.1007/s11075-016-0173-0
* .. [4] Raimundas Vidunas, "Degenerate Gauss Hypergeometric Functions",
* Kyushu Journal of Mathematics, 2007, Volume 61, Issue 1, Pages 109-135,
* .. [5] López, J.L., Temme, N.M. New series expansions of the Gauss hypergeometric
* function. Adv Comput Math 39, 349-365 (2013).
* https://doi.org/10.1007/s10444-012-9283-y
* """
*/
#pragma once
#include "config.h"
#include "error.h"
#include "tools.h"
#include "binom.h"
#include "cephes/gamma.h"
#include "cephes/lanczos.h"
#include "cephes/poch.h"
#include "cephes/hyp2f1.h"
#include "digamma.h"
namespace xsf {
namespace detail {
constexpr double hyp2f1_EPS = 1e-15;
/* The original implementation in SciPy from Zhang and Jin used 1500 for the
* maximum number of series iterations in some cases and 500 in others.
* Through the empirical results on the test cases in
* scipy/special/_precompute/hyp2f1_data.py, it was determined that these values
* can lead to early termination of series which would have eventually converged
* at a reasonable level of accuracy. We've bumped the iteration limit to 3000,
* and may adjust it again based on further analysis. */
constexpr std::uint64_t hyp2f1_MAXITER = 3000;
XSF_HOST_DEVICE inline double four_gammas_lanczos(double u, double v, double w, double x) {
/* Compute ratio of gamma functions using lanczos approximation.
*
* Computes gamma(u)*gamma(v)/(gamma(w)*gamma(x))
*
* It is assumed that x = u + v - w, but it is left to the user to
* ensure this.
*
* The lanczos approximation takes the form
*
* gamma(x) = factor(x) * lanczos_sum_expg_scaled(x)
*
* where factor(x) = ((x + lanczos_g - 0.5)/e)**(x - 0.5).
*
* The formula above is only valid for x >= 0.5, but can be extended to
* x < 0.5 with the reflection principle.
*
* Using the lanczos approximation when computing this ratio of gamma functions
* allows factors to be combined analytically to avoid underflow and overflow
* and produce a more accurate result. The condition x = u + v - w makes it
* possible to cancel the factors in the expression
*
* factor(u) * factor(v) / (factor(w) * factor(x))
*
* by taking one factor and absorbing it into the others. Currently, this
* implementation takes the factor corresponding to the argument with largest
* absolute value and absorbs it into the others.
*
* Since this is only called internally by four_gammas. It is assumed that
* |u| >= |v| and |w| >= |x|.
*/
/* The below implementation may incorrectly return finite results
* at poles of the gamma function. Handle these cases explicitly. */
if ((u == std::trunc(u) && u <= 0) || (v == std::trunc(v) && v <= 0)) {
/* Return nan if numerator has pole. Diverges to +- infinity
* depending on direction so value is undefined. */
return std::numeric_limits<double>::quiet_NaN();
}
if ((w == std::trunc(w) && w <= 0) || (x == std::trunc(x) && x <= 0)) {
// Return 0 if denominator has pole but not numerator.
return 0.0;
}
double result = 1.0;
double ugh, vgh, wgh, xgh, u_prime, v_prime, w_prime, x_prime;
if (u >= 0.5) {
result *= cephes::lanczos_sum_expg_scaled(u);
ugh = u + cephes::lanczos_g - 0.5;
u_prime = u;
} else {
result /= cephes::lanczos_sum_expg_scaled(1 - u) * std::sin(M_PI * u) * M_1_PI;
ugh = 0.5 - u + cephes::lanczos_g;
u_prime = 1 - u;
}
if (v >= 0.5) {
result *= cephes::lanczos_sum_expg_scaled(v);
vgh = v + cephes::lanczos_g - 0.5;
v_prime = v;
} else {
result /= cephes::lanczos_sum_expg_scaled(1 - v) * std::sin(M_PI * v) * M_1_PI;
vgh = 0.5 - v + cephes::lanczos_g;
v_prime = 1 - v;
}
if (w >= 0.5) {
result /= cephes::lanczos_sum_expg_scaled(w);
wgh = w + cephes::lanczos_g - 0.5;
w_prime = w;
} else {
result *= cephes::lanczos_sum_expg_scaled(1 - w) * std::sin(M_PI * w) * M_1_PI;
wgh = 0.5 - w + cephes::lanczos_g;
w_prime = 1 - w;
}
if (x >= 0.5) {
result /= cephes::lanczos_sum_expg_scaled(x);
xgh = x + cephes::lanczos_g - 0.5;
x_prime = x;
} else {
result *= cephes::lanczos_sum_expg_scaled(1 - x) * std::sin(M_PI * x) * M_1_PI;
xgh = 0.5 - x + cephes::lanczos_g;
x_prime = 1 - x;
}
if (std::abs(u) >= std::abs(w)) {
// u has greatest absolute value. Absorb ugh into the others.
if (std::abs((v_prime - u_prime) * (v - 0.5)) < 100 * ugh and v > 100) {
/* Special case where base is close to 1. Condition taken from
* Boost's beta function implementation. */
result *= std::exp((v - 0.5) * std::log1p((v_prime - u_prime) / ugh));
} else {
result *= std::pow(vgh / ugh, v - 0.5);
}
if (std::abs((u_prime - w_prime) * (w - 0.5)) < 100 * wgh and u > 100) {
result *= std::exp((w - 0.5) * std::log1p((u_prime - w_prime) / wgh));
} else {
result *= std::pow(ugh / wgh, w - 0.5);
}
if (std::abs((u_prime - x_prime) * (x - 0.5)) < 100 * xgh and u > 100) {
result *= std::exp((x - 0.5) * std::log1p((u_prime - x_prime) / xgh));
} else {
result *= std::pow(ugh / xgh, x - 0.5);
}
} else {
// w has greatest absolute value. Absorb wgh into the others.
if (std::abs((u_prime - w_prime) * (u - 0.5)) < 100 * wgh and u > 100) {
result *= std::exp((u - 0.5) * std::log1p((u_prime - w_prime) / wgh));
} else {
result *= pow(ugh / wgh, u - 0.5);
}
if (std::abs((v_prime - w_prime) * (v - 0.5)) < 100 * wgh and v > 100) {
result *= std::exp((v - 0.5) * std::log1p((v_prime - w_prime) / wgh));
} else {
result *= std::pow(vgh / wgh, v - 0.5);
}
if (std::abs((w_prime - x_prime) * (x - 0.5)) < 100 * xgh and x > 100) {
result *= std::exp((x - 0.5) * std::log1p((w_prime - x_prime) / xgh));
} else {
result *= std::pow(wgh / xgh, x - 0.5);
}
}
// This exhausts all cases because we assume |u| >= |v| and |w| >= |x|.
return result;
}
XSF_HOST_DEVICE inline double four_gammas(double u, double v, double w, double x) {
double result;
// Without loss of generality, ensure |u| >= |v| and |w| >= |x|.
if (std::abs(v) > std::abs(u)) {
std::swap(u, v);
}
if (std::abs(x) > std::abs(w)) {
std::swap(x, w);
}
/* Direct ratio tends to be more accurate for arguments in this range. Range
* chosen empirically based on the relevant benchmarks in
* scipy/special/_precompute/hyp2f1_data.py */
if (std::abs(u) <= 100 && std::abs(v) <= 100 && std::abs(w) <= 100 && std::abs(x) <= 100) {
result = cephes::Gamma(u) * cephes::Gamma(v) * (cephes::rgamma(w) * cephes::rgamma(x));
if (std::isfinite(result) && result != 0.0) {
return result;
}
}
result = four_gammas_lanczos(u, v, w, x);
if (std::isfinite(result) && result != 0.0) {
return result;
}
// If overflow or underflow, try again with logs.
result = std::exp(cephes::lgam(v) - cephes::lgam(x) + cephes::lgam(u) - cephes::lgam(w));
result *= cephes::gammasgn(u) * cephes::gammasgn(w) * cephes::gammasgn(v) * cephes::gammasgn(x);
return result;
}
class HypergeometricSeriesGenerator {
/* Maclaurin series for hyp2f1.
*
* Series is convergent for |z| < 1 but is only practical for numerical
* computation when |z| < 0.9.
*/
public:
XSF_HOST_DEVICE HypergeometricSeriesGenerator(double a, double b, double c, std::complex<double> z)
: a_(a), b_(b), c_(c), z_(z), term_(1.0), k_(0) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
std::complex<double> output = term_;
term_ = term_ * (a_ + k_) * (b_ + k_) / ((k_ + 1) * (c_ + k_)) * z_;
++k_;
return output;
}
private:
double a_, b_, c_;
std::complex<double> z_, term_;
std::uint64_t k_;
};
class Hyp2f1Transform1Generator {
/* 1 -z transformation of standard series.*/
public:
XSF_HOST_DEVICE Hyp2f1Transform1Generator(double a, double b, double c, std::complex<double> z)
: factor1_(four_gammas(c, c - a - b, c - a, c - b)),
factor2_(four_gammas(c, a + b - c, a, b) * std::pow(1.0 - z, c - a - b)),
generator1_(HypergeometricSeriesGenerator(a, b, a + b - c + 1, 1.0 - z)),
generator2_(HypergeometricSeriesGenerator(c - a, c - b, c - a - b + 1, 1.0 - z)) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
return factor1_ * generator1_() + factor2_ * generator2_();
}
private:
std::complex<double> factor1_, factor2_;
HypergeometricSeriesGenerator generator1_, generator2_;
};
class Hyp2f1Transform1LimitSeriesGenerator {
/* 1 - z transform in limit as c - a - b approaches an integer m. */
public:
XSF_HOST_DEVICE Hyp2f1Transform1LimitSeriesGenerator(double a, double b, double m, std::complex<double> z)
: d1_(xsf::digamma(a)), d2_(xsf::digamma(b)), d3_(xsf::digamma(1 + m)),
d4_(xsf::digamma(1.0)), a_(a), b_(b), m_(m), z_(z), log_1_z_(std::log(1.0 - z)),
factor_(cephes::rgamma(m + 1)), k_(0) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
std::complex<double> term_ = (d1_ + d2_ - d3_ - d4_ + log_1_z_) * factor_;
// Use digamma(x + 1) = digamma(x) + 1/x
d1_ += 1 / (a_ + k_); // d1 = digamma(a + k)
d2_ += 1 / (b_ + k_); // d2 = digamma(b + k)
d3_ += 1 / (1.0 + m_ + k_); // d3 = digamma(1 + m + k)
d4_ += 1 / (1.0 + k_); // d4 = digamma(1 + k)
factor_ *= (a_ + k_) * (b_ + k_) / ((k_ + 1.0) * (m_ + k_ + 1)) * (1.0 - z_);
++k_;
return term_;
}
private:
double d1_, d2_, d3_, d4_, a_, b_, m_;
std::complex<double> z_, log_1_z_, factor_;
int k_;
};
class Hyp2f1Transform2Generator {
/* 1/z transformation of standard series.*/
public:
XSF_HOST_DEVICE Hyp2f1Transform2Generator(double a, double b, double c, std::complex<double> z)
: factor1_(four_gammas(c, b - a, b, c - a) * std::pow(-z, -a)),
factor2_(four_gammas(c, a - b, a, c - b) * std::pow(-z, -b)),
generator1_(HypergeometricSeriesGenerator(a, a - c + 1, a - b + 1, 1.0 / z)),
generator2_(HypergeometricSeriesGenerator(b, b - c + 1, b - a + 1, 1.0 / z)) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
return factor1_ * generator1_() + factor2_ * generator2_();
}
private:
std::complex<double> factor1_, factor2_;
HypergeometricSeriesGenerator generator1_, generator2_;
};
class Hyp2f1Transform2LimitSeriesGenerator {
/* 1/z transform in limit as a - b approaches a non-negative integer m. (Can swap a and b to
* handle the m a negative integer case. */
public:
XSF_HOST_DEVICE Hyp2f1Transform2LimitSeriesGenerator(double a, double b, double c, double m,
std::complex<double> z)
: d1_(xsf::digamma(1.0)), d2_(xsf::digamma(1 + m)), d3_(xsf::digamma(a)),
d4_(xsf::digamma(c - a)), a_(a), b_(b), c_(c), m_(m), z_(z), log_neg_z_(std::log(-z)),
factor_(xsf::cephes::poch(b, m) * xsf::cephes::poch(1 - c + b, m) *
xsf::cephes::rgamma(m + 1)),
k_(0) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
std::complex<double> term = (d1_ + d2_ - d3_ - d4_ + log_neg_z_) * factor_;
// Use digamma(x + 1) = digamma(x) + 1/x
d1_ += 1 / (1.0 + k_); // d1 = digamma(1 + k)
d2_ += 1 / (1.0 + m_ + k_); // d2 = digamma(1 + m + k)
d3_ += 1 / (a_ + k_); // d3 = digamma(a + k)
d4_ -= 1 / (c_ - a_ - k_ - 1); // d4 = digamma(c - a - k)
factor_ *= (b_ + m_ + k_) * (1 - c_ + b_ + m_ + k_) / ((k_ + 1) * (m_ + k_ + 1)) / z_;
++k_;
return term;
}
private:
double d1_, d2_, d3_, d4_, a_, b_, c_, m_;
std::complex<double> z_, log_neg_z_, factor_;
std::uint64_t k_;
};
class Hyp2f1Transform2LimitSeriesCminusAIntGenerator {
/* 1/z transform in limit as a - b approaches a non-negative integer m, and c - a approaches
* a positive integer n. */
public:
XSF_HOST_DEVICE Hyp2f1Transform2LimitSeriesCminusAIntGenerator(double a, double b, double c, double m,
double n, std::complex<double> z)
: d1_(xsf::digamma(1.0)), d2_(xsf::digamma(1 + m)), d3_(xsf::digamma(a)),
d4_(xsf::digamma(n)), a_(a), b_(b), c_(c), m_(m), n_(n), z_(z), log_neg_z_(std::log(-z)),
factor_(xsf::cephes::poch(b, m) * xsf::cephes::poch(1 - c + b, m) *
xsf::cephes::rgamma(m + 1)),
k_(0) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
std::complex<double> term;
if (k_ < n_) {
term = (d1_ + d2_ - d3_ - d4_ + log_neg_z_) * factor_;
// Use digamma(x + 1) = digamma(x) + 1/x
d1_ += 1 / (1.0 + k_); // d1 = digamma(1 + k)
d2_ += 1 / (1 + m_ + k_); // d2 = digamma(1 + m + k)
d3_ += 1 / (a_ + k_); // d3 = digamma(a + k)
d4_ -= 1 / (n_ - k_ - 1); // d4 = digamma(c - a - k)
factor_ *= (b_ + m_ + k_) * (1 - c_ + b_ + m_ + k_) / ((k_ + 1) * (m_ + k_ + 1)) / z_;
++k_;
return term;
}
if (k_ == n_) {
/* When c - a approaches a positive integer and k_ >= c - a = n then
* poch(1 - c + b + m + k) = poch(1 - c + a + k) = approaches zero and
* digamma(c - a - k) approaches a pole. However we can use the limit
* digamma(-n + epsilon) / gamma(-n + epsilon) -> (-1)**(n + 1) * (n+1)! as epsilon -> 0
* to continue the series.
*
* poch(1 - c + b, m + k) = gamma(1 - c + b + m + k)/gamma(1 - c + b)
*
* If a - b is an integer and c - a is an integer, then a and b must both be integers, so assume
* a and b are integers and take the limit as c approaches an integer.
*
* gamma(1 - c + epsilon + a + k)/gamma(1 - c - epsilon + b) =
* (gamma(c + epsilon - b) / gamma(c + epsilon - a - k)) *
* (sin(pi * (c + epsilon - b)) / sin(pi * (c + epsilon - a - k))) (reflection principle)
*
* In the limit as epsilon goes to zero, the ratio of sines will approach
* (-1)**(a - b + k) = (-1)**(m + k)
*
* We may then replace
*
* poch(1 - c - epsilon + b, m + k)*digamma(c + epsilon - a - k)
*
* with
*
* (-1)**(a - b + k)*gamma(c + epsilon - b) * digamma(c + epsilon - a - k) / gamma(c + epsilon - a - k)
*
* and taking the limit epsilon -> 0 gives
*
* (-1)**(a - b + k) * gamma(c - b) * (-1)**(k + a - c + 1)(k + a - c)!
* = (-1)**(c - b - 1)*Gamma(k + a - c + 1)
*/
factor_ = std::pow(-1, m_ + n_) * xsf::binom(c_ - 1, b_ - 1) *
xsf::cephes::poch(c_ - a_ + 1, m_ - 1) / std::pow(z_, static_cast<double>(k_));
}
term = factor_;
factor_ *= (b_ + m_ + k_) * (k_ + a_ - c_ + 1) / ((k_ + 1) * (m_ + k_ + 1)) / z_;
++k_;
return term;
}
private:
double d1_, d2_, d3_, d4_, a_, b_, c_, m_, n_;
std::complex<double> z_, log_neg_z_, factor_;
std::uint64_t k_;
};
class Hyp2f1Transform2LimitFinitePartGenerator {
/* Initial finite sum in limit as a - b approaches a non-negative integer m. The limiting series
* for the 1 - z transform also has an initial finite sum, but it is a standard hypergeometric
* series. */
public:
XSF_HOST_DEVICE Hyp2f1Transform2LimitFinitePartGenerator(double b, double c, double m,
std::complex<double> z)
: b_(b), c_(c), m_(m), z_(z), term_(cephes::Gamma(m) * cephes::rgamma(c - b)), k_(0) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
std::complex<double> output = term_;
term_ = term_ * (b_ + k_) * (c_ - b_ - k_ - 1) / ((k_ + 1) * (m_ - k_ - 1)) / z_;
++k_;
return output;
}
private:
double b_, c_, m_;
std::complex<double> z_, term_;
std::uint64_t k_;
};
class LopezTemmeSeriesGenerator {
/* Lopez-Temme Series for Gaussian hypergeometric function [4].
*
* Converges for all z with real(z) < 1, including in the regions surrounding
* the points exp(+- i*pi/3) that are not covered by any of the standard
* transformations.
*/
public:
XSF_HOST_DEVICE LopezTemmeSeriesGenerator(double a, double b, double c, std::complex<double> z)
: n_(0), a_(a), b_(b), c_(c), phi_previous_(1.0), phi_(1 - 2 * b / c), z_(z), Z_(a * z / (z - 2.0)) {}
XSF_HOST_DEVICE std::complex<double> operator()() {
if (n_ == 0) {
++n_;
return 1.0;
}
if (n_ > 1) { // Update phi and Z for n>=2
double new_phi = ((n_ - 1) * phi_previous_ - (2.0 * b_ - c_) * phi_) / (c_ + (n_ - 1));
phi_previous_ = phi_;
phi_ = new_phi;
Z_ = Z_ * z_ / (z_ - 2.0) * ((a_ + (n_ - 1)) / n_);
}
++n_;
return Z_ * phi_;
}
private:
std::uint64_t n_;
double a_, b_, c_, phi_previous_, phi_;
std::complex<double> z_, Z_;
};
XSF_HOST_DEVICE std::complex<double> hyp2f1_transform1_limiting_case(double a, double b, double c, double m,
std::complex<double> z) {
/* 1 - z transform in limiting case where c - a - b approaches an integer m. */
std::complex<double> result = 0.0;
if (m >= 0) {
if (m != 0) {
auto series_generator = HypergeometricSeriesGenerator(a, b, 1 - m, 1.0 - z);
result += four_gammas(m, c, a + m, b + m) * series_eval_fixed_length(series_generator,
std::complex<double>{0.0, 0.0},
static_cast<std::uint64_t>(m));
}
std::complex<double> prefactor = std::pow(-1.0, m + 1) * xsf::cephes::Gamma(c) /
(xsf::cephes::Gamma(a) * xsf::cephes::Gamma(b)) *
std::pow(1.0 - z, m);
auto series_generator = Hyp2f1Transform1LimitSeriesGenerator(a + m, b + m, m, z);
result += prefactor * series_eval(series_generator, std::complex<double>{0.0, 0.0}, hyp2f1_EPS,
hyp2f1_MAXITER, "hyp2f1");
return result;
} else {
result = four_gammas(-m, c, a, b) * std::pow(1.0 - z, m);
auto series_generator1 = HypergeometricSeriesGenerator(a + m, b + m, 1 + m, 1.0 - z);
result *= series_eval_fixed_length(series_generator1, std::complex<double>{0.0, 0.0},
static_cast<std::uint64_t>(-m));
double prefactor = std::pow(-1.0, m + 1) * xsf::cephes::Gamma(c) *
(xsf::cephes::rgamma(a + m) * xsf::cephes::rgamma(b + m));
auto series_generator2 = Hyp2f1Transform1LimitSeriesGenerator(a, b, -m, z);
result += prefactor * series_eval(series_generator2, std::complex<double>{0.0, 0.0}, hyp2f1_EPS,
hyp2f1_MAXITER, "hyp2f1");
return result;
}
}
XSF_HOST_DEVICE std::complex<double> hyp2f1_transform2_limiting_case(double a, double b, double c, double m,
std::complex<double> z) {
/* 1 / z transform in limiting case where a - b approaches a non-negative integer m. Negative integer case
* can be handled by swapping a and b. */
auto series_generator1 = Hyp2f1Transform2LimitFinitePartGenerator(b, c, m, z);
std::complex<double> result = cephes::Gamma(c) * cephes::rgamma(a) * std::pow(-z, -b);
result *=
series_eval_fixed_length(series_generator1, std::complex<double>{0.0, 0.0}, static_cast<std::uint64_t>(m));
std::complex<double> prefactor = cephes::Gamma(c) * (cephes::rgamma(a) * cephes::rgamma(c - b) * std::pow(-z, -a));
double n = c - a;
if (abs(n - std::round(n)) < hyp2f1_EPS) {
auto series_generator2 = Hyp2f1Transform2LimitSeriesCminusAIntGenerator(a, b, c, m, n, z);
result += prefactor * series_eval(series_generator2, std::complex<double>{0.0, 0.0}, hyp2f1_EPS,
hyp2f1_MAXITER, "hyp2f1");
return result;
}
auto series_generator2 = Hyp2f1Transform2LimitSeriesGenerator(a, b, c, m, z);
result += prefactor *
series_eval(series_generator2, std::complex<double>{0.0, 0.0}, hyp2f1_EPS, hyp2f1_MAXITER, "hyp2f1");
return result;
}
} // namespace detail
XSF_HOST_DEVICE inline std::complex<double> hyp2f1(double a, double b, double c, std::complex<double> z) {
/* Special Cases
* -----------------------------------------------------------------------
* Takes constant value 1 when a = 0 or b = 0, even if c is a non-positive
* integer. This follows mpmath. */
if (a == 0 || b == 0) {
return 1.0;
}
double z_abs = std::abs(z);
// Equals 1 when z i 0, unless c is 0.
if (z_abs == 0) {
if (c != 0) {
return 1.0;
} else {
// Returning real part NAN and imaginary part 0 follows mpmath.
return std::complex<double>{std::numeric_limits<double>::quiet_NaN(), 0};
}
}
bool a_neg_int = a == std::trunc(a) && a < 0;
bool b_neg_int = b == std::trunc(b) && b < 0;
bool c_non_pos_int = c == std::trunc(c) and c <= 0;
/* Diverges when c is a non-positive integer unless a is an integer with
* c <= a <= 0 or b is an integer with c <= b <= 0, (or z equals 0 with
* c != 0) Cases z = 0, a = 0, or b = 0 have already been handled. We follow
* mpmath in handling the degenerate cases where any of a, b, c are
* non-positive integers. See [3] for a treatment of degenerate cases. */
if (c_non_pos_int && !((a_neg_int && c <= a && a < 0) || (b_neg_int && c <= b && b < 0))) {
return std::complex<double>{std::numeric_limits<double>::infinity(), 0};
}
/* Reduces to a polynomial when a or b is a negative integer.
* If a and b are both negative integers, we take care to terminate
* the series at a or b of smaller magnitude. This is to ensure proper
* handling of situations like a < c < b <= 0, a, b, c all non-positive
* integers, where terminating at a would lead to a term of the form 0 / 0. */
std::uint64_t max_degree;
if (a_neg_int || b_neg_int) {
if (a_neg_int && b_neg_int) {
max_degree = a > b ? std::abs(a) : std::abs(b);
} else if (a_neg_int) {
max_degree = std::abs(a);
} else {
max_degree = std::abs(b);
}
if (max_degree <= UINT64_MAX) {
auto series_generator = detail::HypergeometricSeriesGenerator(a, b, c, z);
return detail::series_eval_fixed_length(series_generator, std::complex<double>{0.0, 0.0}, max_degree + 1);
} else {
set_error("hyp2f1", SF_ERROR_NO_RESULT, NULL);
return std::complex<double>{std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::quiet_NaN()};
}
}
// Kummer's Theorem for z = -1; c = 1 + a - b (DLMF 15.4.26)
if (std::abs(z + 1.0) < detail::hyp2f1_EPS && std::abs(1 + a - b - c) < detail::hyp2f1_EPS && !c_non_pos_int) {
return detail::four_gammas(a - b + 1, 0.5 * a + 1, a + 1, 0.5 * a - b + 1);
}
std::complex<double> result;
bool c_minus_a_neg_int = c - a == std::trunc(c - a) && c - a < 0;
bool c_minus_b_neg_int = c - b == std::trunc(c - b) && c - b < 0;
/* If one of c - a or c - b is a negative integer, reduces to evaluating
* a polynomial through an Euler hypergeometric transformation.
* (DLMF 15.8.1) */
if (c_minus_a_neg_int || c_minus_b_neg_int) {
max_degree = c_minus_b_neg_int ? std::abs(c - b) : std::abs(c - a);
if (max_degree <= UINT64_MAX) {
result = std::pow(1.0 - z, c - a - b);
auto series_generator = detail::HypergeometricSeriesGenerator(c - a, c - b, c, z);
result *=
detail::series_eval_fixed_length(series_generator, std::complex<double>{0.0, 0.0}, max_degree + 2);
return result;
} else {
set_error("hyp2f1", SF_ERROR_NO_RESULT, NULL);
return std::complex<double>{std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::quiet_NaN()};
}
}
/* Diverges as real(z) -> 1 when c <= a + b.
* Todo: Actually check for overflow instead of using a fixed tolerance for
* all parameter combinations like in the Fortran original. */
if (std::abs(1 - z.real()) < detail::hyp2f1_EPS && z.imag() == 0 && c - a - b <= 0 && !c_non_pos_int) {
return std::complex<double>{std::numeric_limits<double>::infinity(), 0};
}
// Gauss's Summation Theorem for z = 1; c - a - b > 0 (DLMF 15.4.20).
if (z == 1.0 && c - a - b > 0 && !c_non_pos_int) {
return detail::four_gammas(c, c - a - b, c - a, c - b);
}
/* |z| < 0, z.real() >= 0. Use the Maclaurin Series.
* -----------------------------------------------------------------------
* Apply Euler Hypergeometric Transformation (DLMF 15.8.1) to reduce
* size of a and b if possible. We follow Zhang and Jin's
* implementation [1] although there is very likely a better heuristic
* to determine when this transformation should be applied. As it
* stands, this hurts precision in some cases. */
if (z_abs < 0.9 && z.real() >= 0) {
if (c - a < a && c - b < b) {
result = std::pow(1.0 - z, c - a - b);
auto series_generator = detail::HypergeometricSeriesGenerator(c - a, c - b, c, z);
result *= detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
detail::hyp2f1_MAXITER, "hyp2f1");
return result;
}
auto series_generator = detail::HypergeometricSeriesGenerator(a, b, c, z);
return detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
detail::hyp2f1_MAXITER, "hyp2f1");
}
/* Points near exp(iπ/3), exp(-iπ/3) not handled by any of the standard
* transformations. Use series of López and Temme [5]. These regions
* were not correctly handled by Zhang and Jin's implementation.
* -------------------------------------------------------------------------*/
if (0.9 <= z_abs && z_abs < 1.1 && std::abs(1.0 - z) >= 0.9 && z.real() >= 0) {
/* This condition for applying Euler Transformation (DLMF 15.8.1)
* was determined empirically to work better for this case than that
* used in Zhang and Jin's implementation for |z| < 0.9,
* real(z) >= 0. */
if ((c - a <= a && c - b < b) || (c - a < a && c - b <= b)) {
auto series_generator = detail::LopezTemmeSeriesGenerator(c - a, c - b, c, z);
result = std::pow(1.0 - 0.5 * z, a - c); // Lopez-Temme prefactor
result *= detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
detail::hyp2f1_MAXITER, "hyp2f1");
return std::pow(1.0 - z, c - a - b) * result; // Euler transform prefactor.
}
auto series_generator = detail::LopezTemmeSeriesGenerator(a, b, c, z);
result = detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
detail::hyp2f1_MAXITER, "hyp2f1");
return std::pow(1.0 - 0.5 * z, -a) * result; // Lopez-Temme prefactor.
}
/* z/(z - 1) transformation (DLMF 15.8.1). Avoids cancellation issues that
* occur with Maclaurin series for real(z) < 0.
* -------------------------------------------------------------------------*/
if (z_abs < 1.1 && z.real() < 0) {
if (0 < b && b < a && a < c) {
std::swap(a, b);
}
auto series_generator = detail::HypergeometricSeriesGenerator(a, c - b, c, z / (z - 1.0));
return std::pow(1.0 - z, -a) * detail::series_eval(series_generator, std::complex<double>{0.0, 0.0},
detail::hyp2f1_EPS, detail::hyp2f1_MAXITER, "hyp2f1");
}
/* 1 - z transformation (DLMF 15.8.4). */
if (0.9 <= z_abs && z_abs < 1.1) {
if (std::abs(c - a - b - std::round(c - a - b)) < detail::hyp2f1_EPS) {
// Removable singularity when c - a - b is an integer. Need to use limiting formula.
double m = std::round(c - a - b);
return detail::hyp2f1_transform1_limiting_case(a, b, c, m, z);
}
auto series_generator = detail::Hyp2f1Transform1Generator(a, b, c, z);
return detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
detail::hyp2f1_MAXITER, "hyp2f1");
}
/* 1/z transformation (DLMF 15.8.2). */
if (std::abs(a - b - std::round(a - b)) < detail::hyp2f1_EPS) {
if (b > a) {
std::swap(a, b);
}
double m = std::round(a - b);
return detail::hyp2f1_transform2_limiting_case(a, b, c, m, z);
}
auto series_generator = detail::Hyp2f1Transform2Generator(a, b, c, z);
return detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
detail::hyp2f1_MAXITER, "hyp2f1");
}
XSF_HOST_DEVICE inline std::complex<float> hyp2f1(float a, float b, float c, std::complex<float> x) {
return static_cast<std::complex<float>>(hyp2f1(static_cast<double>(a), static_cast<double>(b),
static_cast<double>(c), static_cast<std::complex<double>>(x)));
}
XSF_HOST_DEVICE inline double hyp2f1(double a, double b, double c, double x) { return cephes::hyp2f1(a, b, c, x); }
XSF_HOST_DEVICE inline float hyp2f1(float a, float b, float c, float x) {
return hyp2f1(static_cast<double>(a), static_cast<double>(b), static_cast<double>(c), static_cast<double>(x));
}
} // namespace xsf