test_fftlog.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import warnings
  2. import math
  3. import numpy as np
  4. import pytest
  5. from scipy.fft._fftlog import fht, ifht, fhtoffset
  6. from scipy.special import poch
  7. from scipy._lib._array_api import xp_assert_close, xp_assert_less
  8. skip_xp_backends = pytest.mark.skip_xp_backends
  9. def test_fht_agrees_with_fftlog(xp):
  10. # check that fht numerically agrees with the output from Fortran FFTLog,
  11. # the results were generated with the provided `fftlogtest` program,
  12. # after fixing how the k array is generated (divide range by n-1, not n)
  13. # test function, analytical Hankel transform is of the same form
  14. def f(r, mu):
  15. return r**(mu+1)*np.exp(-r**2/2)
  16. r = np.logspace(-4, 4, 16)
  17. dln = math.log(r[1]/r[0])
  18. mu = 0.3
  19. offset = 0.0
  20. bias = 0.0
  21. a = xp.asarray(f(r, mu))
  22. # test 1: compute as given
  23. ours = fht(a, dln, mu, offset=offset, bias=bias)
  24. theirs = [-0.1159922613593045E-02, +0.1625822618458832E-02,
  25. -0.1949518286432330E-02, +0.3789220182554077E-02,
  26. +0.5093959119952945E-03, +0.2785387803618774E-01,
  27. +0.9944952700848897E-01, +0.4599202164586588E+00,
  28. +0.3157462160881342E+00, -0.8201236844404755E-03,
  29. -0.7834031308271878E-03, +0.3931444945110708E-03,
  30. -0.2697710625194777E-03, +0.3568398050238820E-03,
  31. -0.5554454827797206E-03, +0.8286331026468585E-03]
  32. theirs = xp.asarray(theirs, dtype=xp.float64)
  33. xp_assert_close(ours, theirs)
  34. # test 2: change to optimal offset
  35. offset = fhtoffset(dln, mu, bias=bias)
  36. ours = fht(a, dln, mu, offset=offset, bias=bias)
  37. theirs = [+0.4353768523152057E-04, -0.9197045663594285E-05,
  38. +0.3150140927838524E-03, +0.9149121960963704E-03,
  39. +0.5808089753959363E-02, +0.2548065256377240E-01,
  40. +0.1339477692089897E+00, +0.4821530509479356E+00,
  41. +0.2659899781579785E+00, -0.1116475278448113E-01,
  42. +0.1791441617592385E-02, -0.4181810476548056E-03,
  43. +0.1314963536765343E-03, -0.5422057743066297E-04,
  44. +0.3208681804170443E-04, -0.2696849476008234E-04]
  45. theirs = xp.asarray(theirs, dtype=xp.float64)
  46. xp_assert_close(ours, theirs)
  47. # test 3: positive bias
  48. bias = 0.8
  49. offset = fhtoffset(dln, mu, bias=bias)
  50. # offset is a np.float64, which array-api-strict disallows
  51. # even if it's technically a subclass of float
  52. offset = float(offset)
  53. ours = fht(a, dln, mu, offset=offset, bias=bias)
  54. theirs = [-7.3436673558316850E+00, +0.1710271207817100E+00,
  55. +0.1065374386206564E+00, -0.5121739602708132E-01,
  56. +0.2636649319269470E-01, +0.1697209218849693E-01,
  57. +0.1250215614723183E+00, +0.4739583261486729E+00,
  58. +0.2841149874912028E+00, -0.8312764741645729E-02,
  59. +0.1024233505508988E-02, -0.1644902767389120E-03,
  60. +0.3305775476926270E-04, -0.7786993194882709E-05,
  61. +0.1962258449520547E-05, -0.8977895734909250E-06]
  62. theirs = xp.asarray(theirs, dtype=xp.float64)
  63. xp_assert_close(ours, theirs)
  64. # test 4: negative bias
  65. bias = -0.8
  66. offset = fhtoffset(dln, mu, bias=bias)
  67. offset = float(offset)
  68. ours = fht(a, dln, mu, offset=offset, bias=bias)
  69. theirs = [+0.8985777068568745E-05, +0.4074898209936099E-04,
  70. +0.2123969254700955E-03, +0.1009558244834628E-02,
  71. +0.5131386375222176E-02, +0.2461678673516286E-01,
  72. +0.1235812845384476E+00, +0.4719570096404403E+00,
  73. +0.2893487490631317E+00, -0.1686570611318716E-01,
  74. +0.2231398155172505E-01, -0.1480742256379873E-01,
  75. +0.1692387813500801E+00, +0.3097490354365797E+00,
  76. +2.7593607182401860E+00, 10.5251075070045800E+00]
  77. theirs = xp.asarray(theirs, dtype=xp.float64)
  78. xp_assert_close(ours, theirs)
  79. @pytest.mark.parametrize('optimal', [True, False])
  80. @pytest.mark.parametrize('offset', [0.0, 1.0, -1.0])
  81. @pytest.mark.parametrize('bias', [0, 0.1, -0.1])
  82. @pytest.mark.parametrize('n', [64, 63])
  83. def test_fht_identity(n, bias, offset, optimal, xp):
  84. rng = np.random.RandomState(3491349965)
  85. a = xp.asarray(rng.standard_normal(n))
  86. dln = rng.uniform(-1, 1)
  87. mu = rng.uniform(-2, 2)
  88. if optimal:
  89. offset = fhtoffset(dln, mu, initial=offset, bias=bias)
  90. # offset is a np.float64, which array-api-strict disallows
  91. # even if it's technically a subclass of float
  92. offset = float(offset)
  93. A = fht(a, dln, mu, offset=offset, bias=bias)
  94. a_ = ifht(A, dln, mu, offset=offset, bias=bias)
  95. xp_assert_close(a_, a, rtol=1.5e-7)
  96. def test_fht_special_cases(xp):
  97. rng = np.random.RandomState(3491349965)
  98. a = xp.asarray(rng.standard_normal(64))
  99. dln = rng.uniform(-1, 1)
  100. # let x = (mu+1+q)/2, y = (mu+1-q)/2, M = {0, -1, -2, ...}
  101. # case 1: x in M, y in M => well-defined transform
  102. mu, bias = -4.0, 1.0
  103. with warnings.catch_warnings(record=True) as record:
  104. fht(a, dln, mu, bias=bias)
  105. assert not record, 'fht warned about a well-defined transform'
  106. # case 2: x not in M, y in M => well-defined transform
  107. mu, bias = -2.5, 0.5
  108. with warnings.catch_warnings(record=True) as record:
  109. fht(a, dln, mu, bias=bias)
  110. assert not record, 'fht warned about a well-defined transform'
  111. # with fht_lock:
  112. # case 3: x in M, y not in M => singular transform
  113. mu, bias = -3.5, 0.5
  114. with pytest.warns(Warning) as record:
  115. fht(a, dln, mu, bias=bias)
  116. assert record, 'fht did not warn about a singular transform'
  117. # with fht_lock:
  118. # case 4: x not in M, y in M => singular inverse transform
  119. mu, bias = -2.5, 0.5
  120. with pytest.warns(Warning) as record:
  121. ifht(a, dln, mu, bias=bias)
  122. assert record, 'ifht did not warn about a singular transform'
  123. @pytest.mark.parametrize('n', [64, 63])
  124. def test_fht_exact(n, xp):
  125. rng = np.random.RandomState(3491349965)
  126. # for a(r) a power law r^\gamma, the fast Hankel transform produces the
  127. # exact continuous Hankel transform if biased with q = \gamma
  128. mu = rng.uniform(0, 3)
  129. # convergence of HT: -1-mu < gamma < 1/2
  130. gamma = rng.uniform(-1-mu, 1/2)
  131. r = np.logspace(-2, 2, n)
  132. a = xp.asarray(r**gamma)
  133. dln = math.log(r[1]/r[0])
  134. offset = fhtoffset(dln, mu, initial=0.0, bias=gamma)
  135. # offset is a np.float64, which array-api-strict disallows
  136. # even if it's technically a subclass of float
  137. offset = float(offset)
  138. A = fht(a, dln, mu, offset=offset, bias=gamma)
  139. k = np.exp(offset)/r[::-1]
  140. # analytical result
  141. At = xp.asarray((2/k)**gamma * poch((mu+1-gamma)/2, gamma))
  142. xp_assert_close(A, At)
  143. @skip_xp_backends(np_only=True,
  144. reason='array-likes only supported for NumPy backend')
  145. @pytest.mark.parametrize("op", [fht, ifht])
  146. def test_array_like(xp, op):
  147. x = [[[1.0, 1.0], [1.0, 1.0]],
  148. [[1.0, 1.0], [1.0, 1.0]],
  149. [[1.0, 1.0], [1.0, 1.0]]]
  150. xp_assert_close(op(x, 1.0, 2.0), op(xp.asarray(x), 1.0, 2.0))
  151. @pytest.mark.parametrize('n', [128, 129])
  152. def test_gh_21661(xp, n):
  153. one = xp.asarray(1.0)
  154. mu = 0.0
  155. r = np.logspace(-7, 1, n)
  156. dln = math.log(r[1] / r[0])
  157. offset = fhtoffset(dln, initial=-6 * np.log(10), mu=mu)
  158. r = xp.asarray(r, dtype=one.dtype)
  159. k = math.exp(offset) / xp.flip(r, axis=-1)
  160. def f(x, mu):
  161. return x**(mu + 1)*xp.exp(-x**2/2)
  162. a_r = f(r, mu)
  163. fht_val = fht(a_r, dln, mu=mu, offset=offset)
  164. a_k = f(k, mu)
  165. rel_err = xp.max(xp.abs((fht_val - a_k) / a_k))
  166. xp_assert_less(rel_err, xp.asarray(7.28e+16)[()])