test_ndtri_exp.py 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import pytest
  2. import numpy as np
  3. from numpy.testing import assert_equal, assert_allclose
  4. from scipy.special import log_ndtr, ndtri_exp
  5. from scipy.special._testutils import assert_func_equal
  6. def log_ndtr_ndtri_exp(y):
  7. return log_ndtr(ndtri_exp(y))
  8. @pytest.fixture(scope="class")
  9. def uniform_random_points():
  10. random_state = np.random.RandomState(1234)
  11. points = random_state.random_sample(1000)
  12. return points
  13. class TestNdtriExp:
  14. """Tests that ndtri_exp is sufficiently close to an inverse of log_ndtr.
  15. We have separate tests for the five intervals (-inf, -10),
  16. [-10, -2), [-2, -0.14542), [-0.14542, -1e-6), and [-1e-6, 0).
  17. ndtri_exp(y) is computed in three different ways depending on if y
  18. is in (-inf, -2), [-2, log(1 - exp(-2))], or [log(1 - exp(-2), 0).
  19. Each of these intervals is given its own test with two additional tests
  20. for handling very small values and values very close to zero.
  21. """
  22. @pytest.mark.parametrize(
  23. "test_input", [-1e1, -1e2, -1e10, -1e20, -np.finfo(float).max]
  24. )
  25. def test_very_small_arg(self, test_input, uniform_random_points):
  26. scale = test_input
  27. points = scale * (0.5 * uniform_random_points + 0.5)
  28. assert_func_equal(
  29. log_ndtr_ndtri_exp,
  30. lambda y: y, points,
  31. rtol=1e-14,
  32. nan_ok=True
  33. )
  34. @pytest.mark.parametrize(
  35. "interval,expected_rtol",
  36. [
  37. ((-10, -2), 1e-14),
  38. ((-2, -0.14542), 1e-12),
  39. ((-0.14542, -1e-6), 1e-10),
  40. ((-1e-6, 0), 1e-6),
  41. ],
  42. )
  43. def test_in_interval(self, interval, expected_rtol, uniform_random_points):
  44. left, right = interval
  45. points = (right - left) * uniform_random_points + left
  46. assert_func_equal(
  47. log_ndtr_ndtri_exp,
  48. lambda y: y, points,
  49. rtol=expected_rtol,
  50. nan_ok=True
  51. )
  52. def test_extreme(self):
  53. # bigneg is not quite the largest negative double precision value.
  54. # Here's why:
  55. # The round-trip calculation
  56. # y = ndtri_exp(bigneg)
  57. # bigneg2 = log_ndtr(y)
  58. # where bigneg is a very large negative value, would--with infinite
  59. # precision--result in bigneg2 == bigneg. When bigneg is large enough,
  60. # y is effectively equal to -sqrt(2)*sqrt(-bigneg), and log_ndtr(y) is
  61. # effectively -(y/sqrt(2))**2. If we use bigneg = np.finfo(float).min,
  62. # then by construction, the theoretical value is the most negative
  63. # finite value that can be represented with 64 bit float point. This
  64. # means tiny changes in how the computation proceeds can result in the
  65. # return value being -inf. (E.g. changing the constant representation
  66. # of 1/sqrt(2) from 0.7071067811865475--which is the value returned by
  67. # 1/np.sqrt(2)--to 0.7071067811865476--which is the most accurate 64
  68. # bit floating point representation of 1/sqrt(2)--results in the
  69. # round-trip that starts with np.finfo(float).min returning -inf. So
  70. # we'll move the bigneg value a few ULPs towards 0 to avoid this
  71. # sensitivity.
  72. # Use the reduce method to apply nextafter four times.
  73. bigneg = np.nextafter.reduce([np.finfo(float).min, 0, 0, 0, 0])
  74. # tinyneg is approx. -2.225e-308.
  75. tinyneg = -np.finfo(float).tiny
  76. x = np.array([tinyneg, bigneg])
  77. result = log_ndtr_ndtri_exp(x)
  78. assert_allclose(result, x, rtol=1e-12)
  79. def test_asymptotes(self):
  80. assert_equal(ndtri_exp([-np.inf, 0.0]), [-np.inf, np.inf])
  81. def test_outside_domain(self):
  82. assert np.isnan(ndtri_exp(1.0))