test_iv_ratio.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # This file contains unit tests for iv_ratio() and related functions.
  2. import pytest
  3. import numpy as np
  4. from numpy.testing import assert_equal, assert_allclose
  5. from scipy.special._ufuncs import ( # type: ignore[attr-defined]
  6. _iv_ratio as iv_ratio,
  7. _iv_ratio_c as iv_ratio_c,
  8. )
  9. class TestIvRatio:
  10. @pytest.mark.parametrize('v,x,r', [
  11. (0.5, 0.16666666666666666, 0.16514041292462933),
  12. (0.5, 0.3333333333333333, 0.32151273753163434),
  13. (0.5, 0.5, 0.46211715726000974),
  14. (0.5, 0.6666666666666666, 0.5827829453479101),
  15. (0.5, 0.8333333333333335, 0.6822617902381698),
  16. (1, 0.3380952380952381, 0.1666773049170313),
  17. (1, 0.7083333333333333, 0.33366443586989925),
  18. (1, 1.1666666666666667, 0.5023355231537423),
  19. (1, 1.8666666666666665, 0.674616572252164),
  20. (1, 3.560606060606061, 0.844207659503163),
  21. (2.34, 0.7975238095238094, 0.16704903081553285),
  22. (2.34, 1.7133333333333334, 0.3360215931268845),
  23. (2.34, 2.953333333333333, 0.50681909317803),
  24. (2.34, 5.0826666666666656, 0.6755252698800679),
  25. (2.34, 10.869696969696973, 0.8379351104498762),
  26. (56.789, 19.46575238095238, 0.1667020505391409),
  27. (56.789, 42.55008333333333, 0.33353809996933026),
  28. (56.789, 75.552, 0.5003932381177826),
  29. (56.789, 135.76026666666667, 0.6670528221946127),
  30. (56.789, 307.8642424242425, 0.8334999441460798),
  31. ])
  32. def test_against_reference_values(self, v, x, r):
  33. """The reference values are computed using mpmath as follows.
  34. from mpmath import mp
  35. mp.dps = 100
  36. def iv_ratio_mp(v, x):
  37. return mp.besseli(v, x) / mp.besseli(v - 1, x)
  38. def _sample(n, *, v):
  39. '''Return n positive real numbers x such that iv_ratio(v, x) are
  40. roughly evenly spaced over (0, 1). The formula is taken from [1].
  41. [1] Banerjee A., Dhillon, I. S., Ghosh, J., Sra, S. (2005).
  42. "Clustering on the Unit Hypersphere using von Mises-Fisher
  43. Distributions." Journal of Machine Learning Research,
  44. 6(46):1345-1382.
  45. '''
  46. r = np.arange(1, n+1) / (n+1)
  47. return r * (2*v-r*r) / (1-r*r)
  48. for v in (0.5, 1, 2.34, 56.789):
  49. xs = _sample(5, v=v)
  50. for x in xs:
  51. print(f"({v}, {x}, {float(iv_ratio_mp(v,x))}),")
  52. """
  53. assert_allclose(iv_ratio(v, x), r, rtol=4e-16, atol=0)
  54. @pytest.mark.parametrize('v,x,r', [
  55. (1, np.inf, 1),
  56. (np.inf, 1, 0),
  57. ])
  58. def test_inf(self, v, x, r):
  59. """If exactly one of v or x is inf and the other is within domain,
  60. should return 0 or 1 accordingly."""
  61. assert_equal(iv_ratio(v, x), r)
  62. @pytest.mark.parametrize('v', [0.49, -np.inf, np.nan, np.inf])
  63. @pytest.mark.parametrize('x', [-np.finfo(float).smallest_normal,
  64. -np.finfo(float).smallest_subnormal,
  65. -np.inf, np.nan, np.inf])
  66. def test_nan(self, v, x):
  67. """If at least one argument is out of domain, or if v = x = inf,
  68. the function should return nan."""
  69. assert_equal(iv_ratio(v, x), np.nan)
  70. @pytest.mark.parametrize('v', [0.5, 1, np.finfo(float).max, np.inf])
  71. def test_zero_x(self, v):
  72. """If x is +/-0.0, return x to ensure iv_ratio is an odd function."""
  73. assert_equal(iv_ratio(v, 0.0), 0.0)
  74. assert_equal(iv_ratio(v, -0.0), -0.0)
  75. @pytest.mark.parametrize('v,x', [
  76. (1, np.finfo(float).smallest_normal),
  77. (1, np.finfo(float).smallest_subnormal),
  78. (1, np.finfo(float).smallest_subnormal*2),
  79. (1e20, 123),
  80. (np.finfo(float).max, 1),
  81. (np.finfo(float).max, np.sqrt(np.finfo(float).max)),
  82. ])
  83. def test_tiny_x(self, v, x):
  84. """If x is much less than v, the bounds
  85. x x
  86. --------------------------- <= R <= -----------------------
  87. v-0.5+sqrt(x**2+(v+0.5)**2) v-1+sqrt(x**2+(v+1)**2)
  88. collapses to R ~= x/2v. Test against this asymptotic expression.
  89. """
  90. assert_equal(iv_ratio(v, x), (0.5*x)/v)
  91. @pytest.mark.parametrize('v,x', [
  92. (1, 1e16),
  93. (1e20, 1e40),
  94. (np.sqrt(np.finfo(float).max), np.finfo(float).max),
  95. ])
  96. def test_huge_x(self, v, x):
  97. """If x is much greater than v, the bounds
  98. x x
  99. --------------------------- <= R <= ---------------------------
  100. v-0.5+sqrt(x**2+(v+0.5)**2) v-0.5+sqrt(x**2+(v-0.5)**2)
  101. collapses to R ~= 1. Test against this asymptotic expression.
  102. """
  103. assert_equal(iv_ratio(v, x), 1.0)
  104. @pytest.mark.parametrize('v,x', [
  105. (np.finfo(float).max, np.finfo(float).max),
  106. (np.finfo(float).max / 3, np.finfo(float).max),
  107. (np.finfo(float).max, np.finfo(float).max / 3),
  108. ])
  109. def test_huge_v_x(self, v, x):
  110. """If both x and v are very large, the bounds
  111. x x
  112. --------------------------- <= R <= -----------------------
  113. v-0.5+sqrt(x**2+(v+0.5)**2) v-1+sqrt(x**2+(v+1)**2)
  114. collapses to R ~= x/(v+sqrt(x**2+v**2). Test against this asymptotic
  115. expression, and in particular that no numerical overflow occurs during
  116. intermediate calculations.
  117. """
  118. t = x / v
  119. expected = t / (1 + np.hypot(1, t))
  120. assert_allclose(iv_ratio(v, x), expected, rtol=4e-16, atol=0)
  121. class TestIvRatioC:
  122. @pytest.mark.parametrize('v,x,r', [
  123. (0.5, 0.16666666666666666, 0.8348595870753707),
  124. (0.5, 0.3333333333333333, 0.6784872624683657),
  125. (0.5, 0.5, 0.5378828427399902),
  126. (0.5, 0.6666666666666666, 0.4172170546520899),
  127. (0.5, 0.8333333333333335, 0.3177382097618302),
  128. (1, 0.3380952380952381, 0.8333226950829686),
  129. (1, 0.7083333333333333, 0.6663355641301008),
  130. (1, 1.1666666666666667, 0.4976644768462577),
  131. (1, 1.8666666666666665, 0.325383427747836),
  132. (1, 3.560606060606061, 0.155792340496837),
  133. (2.34, 0.7975238095238094, 0.8329509691844672),
  134. (2.34, 1.7133333333333334, 0.6639784068731155),
  135. (2.34, 2.953333333333333, 0.49318090682197),
  136. (2.34, 5.0826666666666656, 0.3244747301199321),
  137. (2.34, 10.869696969696973, 0.16206488955012377),
  138. (56.789, 19.46575238095238, 0.8332979494608591),
  139. (56.789, 42.55008333333333, 0.6664619000306697),
  140. (56.789, 75.552, 0.4996067618822174),
  141. (56.789, 135.76026666666667, 0.3329471778053873),
  142. (56.789, 307.8642424242425, 0.16650005585392025),
  143. ])
  144. def test_against_reference_values(self, v, x, r):
  145. """The reference values are one minus those of TestIvRatio."""
  146. assert_allclose(iv_ratio_c(v, x), r, rtol=1e-15, atol=0)
  147. @pytest.mark.parametrize('v,x,r', [
  148. (1, np.inf, 0),
  149. (np.inf, 1, 1),
  150. ])
  151. def test_inf(self, v, x, r):
  152. """If exactly one of v or x is inf and the other is within domain,
  153. should return 0 or 1 accordingly."""
  154. assert_equal(iv_ratio_c(v, x), r)
  155. @pytest.mark.parametrize('v', [0.49, -np.inf, np.nan, np.inf])
  156. @pytest.mark.parametrize('x', [-np.finfo(float).smallest_normal,
  157. -np.finfo(float).smallest_subnormal,
  158. -np.inf, np.nan, np.inf])
  159. def test_nan(self, v, x):
  160. """If at least one argument is out of domain, or if v = x = inf,
  161. the function should return nan."""
  162. assert_equal(iv_ratio_c(v, x), np.nan)
  163. @pytest.mark.parametrize('v', [0.5, 1, np.finfo(float).max, np.inf])
  164. def test_zero_x(self, v):
  165. """If x is +/-0.0, return 1."""
  166. assert_equal(iv_ratio_c(v, 0.0), 1.0)
  167. assert_equal(iv_ratio_c(v, -0.0), 1.0)
  168. @pytest.mark.parametrize('v,x', [
  169. (1, np.finfo(float).smallest_normal),
  170. (1, np.finfo(float).smallest_subnormal),
  171. (1, np.finfo(float).smallest_subnormal*2),
  172. (1e20, 123),
  173. (np.finfo(float).max, 1),
  174. (np.finfo(float).max, np.sqrt(np.finfo(float).max)),
  175. ])
  176. def test_tiny_x(self, v, x):
  177. """If x is much less than v, the bounds
  178. x x
  179. --------------------------- <= R <= -----------------------
  180. v-0.5+sqrt(x**2+(v+0.5)**2) v-1+sqrt(x**2+(v+1)**2)
  181. collapses to 1-R ~= 1-x/2v. Test against this asymptotic expression.
  182. """
  183. assert_equal(iv_ratio_c(v, x), 1.0-(0.5*x)/v)
  184. @pytest.mark.parametrize('v,x', [
  185. (1, 1e16),
  186. (1e20, 1e40),
  187. (np.sqrt(np.finfo(float).max), np.finfo(float).max),
  188. ])
  189. def test_huge_x(self, v, x):
  190. """If x is much greater than v, the bounds
  191. x x
  192. --------------------------- <= R <= ---------------------------
  193. v-0.5+sqrt(x**2+(v+0.5)**2) v-0.5+sqrt(x**2+(v-0.5)**2)
  194. collapses to 1-R ~= (v-0.5)/x. Test against this asymptotic expression.
  195. """
  196. assert_allclose(iv_ratio_c(v, x), (v-0.5)/x, rtol=1e-15, atol=0)
  197. @pytest.mark.parametrize('v,x', [
  198. (np.finfo(float).max, np.finfo(float).max),
  199. (np.finfo(float).max / 3, np.finfo(float).max),
  200. (np.finfo(float).max, np.finfo(float).max / 3),
  201. ])
  202. def test_huge_v_x(self, v, x):
  203. """If both x and v are very large, the bounds
  204. x x
  205. --------------------------- <= R <= -----------------------
  206. v-0.5+sqrt(x**2+(v+0.5)**2) v-1+sqrt(x**2+(v+1)**2)
  207. collapses to 1 - R ~= 1 - x/(v+sqrt(x**2+v**2). Test against this
  208. asymptotic expression, and in particular that no numerical overflow
  209. occurs during intermediate calculations.
  210. """
  211. t = x / v
  212. expected = 1 - t / (1 + np.hypot(1, t))
  213. assert_allclose(iv_ratio_c(v, x), expected, rtol=4e-16, atol=0)