test_hypergeometric.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import pytest
  2. import numpy as np
  3. from numpy.testing import assert_allclose, assert_equal
  4. import scipy.special as sc
  5. class TestHyperu:
  6. def test_negative_x(self):
  7. a, b, x = np.meshgrid(
  8. [-1, -0.5, 0, 0.5, 1],
  9. [-1, -0.5, 0, 0.5, 1],
  10. np.linspace(-100, -1, 10),
  11. )
  12. assert np.all(np.isnan(sc.hyperu(a, b, x)))
  13. def test_special_cases(self):
  14. assert sc.hyperu(0, 1, 1) == 1.0
  15. @pytest.mark.parametrize('a', [0.5, 1, np.nan])
  16. @pytest.mark.parametrize('b', [1, 2, np.nan])
  17. @pytest.mark.parametrize('x', [0.25, 3, np.nan])
  18. def test_nan_inputs(self, a, b, x):
  19. assert np.isnan(sc.hyperu(a, b, x)) == np.any(np.isnan([a, b, x]))
  20. class TestHyp1f1:
  21. @pytest.mark.parametrize('a, b, x', [
  22. (np.nan, 1, 1),
  23. (1, np.nan, 1),
  24. (1, 1, np.nan)
  25. ])
  26. def test_nan_inputs(self, a, b, x):
  27. assert np.isnan(sc.hyp1f1(a, b, x))
  28. def test_poles(self):
  29. assert_equal(sc.hyp1f1(1, [0, -1, -2, -3, -4], 0.5), np.infty)
  30. @pytest.mark.parametrize('a, b, x, result', [
  31. (-1, 1, 0.5, 0.5),
  32. (1, 1, 0.5, 1.6487212707001281468),
  33. (2, 1, 0.5, 2.4730819060501922203),
  34. (1, 2, 0.5, 1.2974425414002562937),
  35. (-10, 1, 0.5, -0.38937441413785204475)
  36. ])
  37. def test_special_cases(self, a, b, x, result):
  38. # Hit all the special case branches at the beginning of the
  39. # function. Desired answers computed using Mpmath.
  40. assert_allclose(sc.hyp1f1(a, b, x), result, atol=0, rtol=1e-15)
  41. @pytest.mark.parametrize('a, b, x, result', [
  42. (1, 1, 0.44, 1.5527072185113360455),
  43. (-1, 1, 0.44, 0.55999999999999999778),
  44. (100, 100, 0.89, 2.4351296512898745592),
  45. (-100, 100, 0.89, 0.40739062490768104667),
  46. (1.5, 100, 59.99, 3.8073513625965598107),
  47. (-1.5, 100, 59.99, 0.25099240047125826943)
  48. ])
  49. def test_geometric_convergence(self, a, b, x, result):
  50. # Test the region where we are relying on the ratio of
  51. #
  52. # (|a| + 1) * |x| / |b|
  53. #
  54. # being small. Desired answers computed using Mpmath
  55. assert_allclose(sc.hyp1f1(a, b, x), result, atol=0, rtol=1e-15)
  56. @pytest.mark.parametrize('a, b, x, result', [
  57. (-1, 1, 1.5, -0.5),
  58. (-10, 1, 1.5, 0.41801777430943080357),
  59. (-25, 1, 1.5, 0.25114491646037839809),
  60. (-50, 1, 1.5, -0.25683643975194756115),
  61. (-80, 1, 1.5, -0.24554329325751503601),
  62. (-150, 1, 1.5, -0.173364795515420454496),
  63. ])
  64. def test_a_negative_integer(self, a, b, x, result):
  65. # Desired answers computed using Mpmath.
  66. assert_allclose(sc.hyp1f1(a, b, x), result, atol=0, rtol=1e-14)
  67. @pytest.mark.parametrize('a, b, x, expected', [
  68. (0.01, 150, -4, 0.99973683897677527773), # gh-3492
  69. (1, 5, 0.01, 1.0020033381011970966), # gh-3593
  70. (50, 100, 0.01, 1.0050126452421463411), # gh-3593
  71. (1, 0.3, -1e3, -7.011932249442947651455e-04), # gh-14149
  72. (1, 0.3, -1e4, -7.001190321418937164734e-05), # gh-14149
  73. (9, 8.5, -350, -5.224090831922378361082e-20), # gh-17120
  74. (9, 8.5, -355, -4.595407159813368193322e-20), # gh-17120
  75. (75, -123.5, 15, 3.425753920814889017493e+06),
  76. ])
  77. def test_assorted_cases(self, a, b, x, expected):
  78. # Expected values were computed with mpmath.hyp1f1(a, b, x).
  79. assert_allclose(sc.hyp1f1(a, b, x), expected, atol=0, rtol=1e-14)
  80. def test_a_neg_int_and_b_equal_x(self):
  81. # This is a case where the Boost wrapper will call hypergeometric_pFq
  82. # instead of hypergeometric_1F1. When we use a version of Boost in
  83. # which https://github.com/boostorg/math/issues/833 is fixed, this
  84. # test case can probably be moved into test_assorted_cases.
  85. # The expected value was computed with mpmath.hyp1f1(a, b, x).
  86. a = -10.0
  87. b = 2.5
  88. x = 2.5
  89. expected = 0.0365323664364104338721
  90. computed = sc.hyp1f1(a, b, x)
  91. assert_allclose(computed, expected, atol=0, rtol=1e-13)
  92. @pytest.mark.parametrize('a, b, x, desired', [
  93. (-1, -2, 2, 2),
  94. (-1, -4, 10, 3.5),
  95. (-2, -2, 1, 2.5)
  96. ])
  97. def test_gh_11099(self, a, b, x, desired):
  98. # All desired results computed using Mpmath
  99. assert sc.hyp1f1(a, b, x) == desired
  100. @pytest.mark.parametrize('a', [-3, -2])
  101. def test_x_zero_a_and_b_neg_ints_and_a_ge_b(self, a):
  102. assert sc.hyp1f1(a, -3, 0) == 1
  103. # The "legacy edge cases" mentioned in the comments in the following
  104. # tests refers to the behavior of hyp1f1(a, b, x) when b is a nonpositive
  105. # integer. In some subcases, the behavior of SciPy does not match that
  106. # of Boost (1.81+), mpmath and Mathematica (via Wolfram Alpha online).
  107. # If the handling of these edges cases is changed to agree with those
  108. # libraries, these test will have to be updated.
  109. @pytest.mark.parametrize('b', [0, -1, -5])
  110. def test_legacy_case1(self, b):
  111. # Test results of hyp1f1(0, n, x) for n <= 0.
  112. # This is a legacy edge case.
  113. # Boost (versions greater than 1.80), Mathematica (via Wolfram Alpha
  114. # online) and mpmath all return 1 in this case, but SciPy's hyp1f1
  115. # returns inf.
  116. assert_equal(sc.hyp1f1(0, b, [-1.5, 0, 1.5]), [np.inf, np.inf, np.inf])
  117. def test_legacy_case2(self):
  118. # This is a legacy edge case.
  119. # In software such as boost (1.81+), mpmath and Mathematica,
  120. # the value is 1.
  121. assert sc.hyp1f1(-4, -3, 0) == np.inf