_fftlog_backend.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import numpy as np
  2. from warnings import warn
  3. from ._basic import rfft, irfft
  4. from ..special import loggamma, poch
  5. from scipy._lib._array_api import array_namespace, xp_capabilities
  6. __all__ = ['fht', 'ifht', 'fhtoffset']
  7. # constants
  8. LN_2 = np.log(2)
  9. def fht(a, dln, mu, offset=0.0, bias=0.0):
  10. xp = array_namespace(a)
  11. a = xp.asarray(a)
  12. # size of transform
  13. n = a.shape[-1]
  14. # bias input array
  15. if bias != 0:
  16. # a_q(r) = a(r) (r/r_c)^{-q}
  17. j_c = (n-1)/2
  18. j = xp.arange(n, dtype=xp.float64)
  19. a = a * xp.exp(-bias*(j - j_c)*dln)
  20. # compute FHT coefficients
  21. u = xp.asarray(fhtcoeff(n, dln, mu, offset=offset, bias=bias))
  22. # transform
  23. A = _fhtq(a, u, xp=xp)
  24. # bias output array
  25. if bias != 0:
  26. # A(k) = A_q(k) (k/k_c)^{-q} (k_c r_c)^{-q}
  27. A *= xp.exp(-bias*((j - j_c)*dln + offset))
  28. return A
  29. def ifht(A, dln, mu, offset=0.0, bias=0.0):
  30. xp = array_namespace(A)
  31. A = xp.asarray(A)
  32. # size of transform
  33. n = A.shape[-1]
  34. # bias input array
  35. if bias != 0:
  36. # A_q(k) = A(k) (k/k_c)^{q} (k_c r_c)^{q}
  37. j_c = (n-1)/2
  38. j = xp.arange(n, dtype=xp.float64)
  39. A = A * xp.exp(bias*((j - j_c)*dln + offset))
  40. # compute FHT coefficients
  41. u = xp.asarray(fhtcoeff(n, dln, mu, offset=offset, bias=bias, inverse=True))
  42. # transform
  43. a = _fhtq(A, u, inverse=True, xp=xp)
  44. # bias output array
  45. if bias != 0:
  46. # a(r) = a_q(r) (r/r_c)^{q}
  47. a /= xp.exp(-bias*(j - j_c)*dln)
  48. return a
  49. def fhtcoeff(n, dln, mu, offset=0.0, bias=0.0, inverse=False):
  50. """Compute the coefficient array for a fast Hankel transform."""
  51. lnkr, q = offset, bias
  52. # Hankel transform coefficients
  53. # u_m = (kr)^{-i 2m pi/(n dlnr)} U_mu(q + i 2m pi/(n dlnr))
  54. # with U_mu(x) = 2^x Gamma((mu+1+x)/2)/Gamma((mu+1-x)/2)
  55. xp = (mu+1+q)/2
  56. xm = (mu+1-q)/2
  57. y = np.linspace(0, np.pi*(n//2)/(n*dln), n//2+1)
  58. u = np.empty(n//2+1, dtype=complex)
  59. v = np.empty(n//2+1, dtype=complex)
  60. u.imag[:] = y
  61. u.real[:] = xm
  62. loggamma(u, out=v)
  63. u.real[:] = xp
  64. loggamma(u, out=u)
  65. y *= 2*(LN_2 - lnkr)
  66. u.real -= v.real
  67. u.real += LN_2*q
  68. u.imag += v.imag
  69. u.imag += y
  70. np.exp(u, out=u)
  71. # fix last coefficient to be real
  72. if n % 2 == 0:
  73. u.imag[-1] = 0
  74. # deal with special cases
  75. if not np.isfinite(u[0]):
  76. # write u_0 = 2^q Gamma(xp)/Gamma(xm) = 2^q poch(xm, xp-xm)
  77. # poch() handles special cases for negative integers correctly
  78. u[0] = 2**q * poch(xm, xp-xm)
  79. # the coefficient may be inf or 0, meaning the transform or the
  80. # inverse transform, respectively, is singular
  81. # check for singular transform or singular inverse transform
  82. if np.isinf(u[0]) and not inverse:
  83. warn('singular transform; consider changing the bias', stacklevel=3)
  84. # fix coefficient to obtain (potentially correct) transform anyway
  85. u = np.copy(u)
  86. u[0] = 0
  87. elif u[0] == 0 and inverse:
  88. warn('singular inverse transform; consider changing the bias', stacklevel=3)
  89. # fix coefficient to obtain (potentially correct) inverse anyway
  90. u = np.copy(u)
  91. u[0] = np.inf
  92. return u
  93. @xp_capabilities(out_of_scope=True)
  94. def fhtoffset(dln, mu, initial=0.0, bias=0.0):
  95. """Return optimal offset for a fast Hankel transform.
  96. Returns an offset close to `initial` that fulfils the low-ringing
  97. condition of [1]_ for the fast Hankel transform `fht` with logarithmic
  98. spacing `dln`, order `mu` and bias `bias`.
  99. Parameters
  100. ----------
  101. dln : float
  102. Uniform logarithmic spacing of the transform.
  103. mu : float
  104. Order of the Hankel transform, any positive or negative real number.
  105. initial : float, optional
  106. Initial value for the offset. Returns the closest value that fulfils
  107. the low-ringing condition.
  108. bias : float, optional
  109. Exponent of power law bias, any positive or negative real number.
  110. Returns
  111. -------
  112. offset : float
  113. Optimal offset of the uniform logarithmic spacing of the transform that
  114. fulfils a low-ringing condition.
  115. Examples
  116. --------
  117. >>> from scipy.fft import fhtoffset
  118. >>> dln = 0.1
  119. >>> mu = 2.0
  120. >>> initial = 0.5
  121. >>> bias = 0.0
  122. >>> offset = fhtoffset(dln, mu, initial, bias)
  123. >>> offset
  124. 0.5454581477676637
  125. See Also
  126. --------
  127. fht : Definition of the fast Hankel transform.
  128. References
  129. ----------
  130. .. [1] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
  131. """
  132. lnkr, q = initial, bias
  133. xp = (mu+1+q)/2
  134. xm = (mu+1-q)/2
  135. y = np.pi/(2*dln)
  136. zp = loggamma(xp + 1j*y)
  137. zm = loggamma(xm + 1j*y)
  138. arg = (LN_2 - lnkr)/dln + (zp.imag + zm.imag)/np.pi
  139. return lnkr + (arg - np.round(arg))*dln
  140. def _fhtq(a, u, inverse=False, *, xp=None):
  141. """Compute the biased fast Hankel transform.
  142. This is the basic FFTLog routine.
  143. """
  144. if xp is None:
  145. xp = np
  146. # size of transform
  147. n = a.shape[-1]
  148. # biased fast Hankel transform via real FFT
  149. A = rfft(a, axis=-1)
  150. if not inverse:
  151. # forward transform
  152. A *= u
  153. else:
  154. # backward transform
  155. A /= xp.conj(u)
  156. A = irfft(A, n, axis=-1)
  157. A = xp.flip(A, axis=-1)
  158. return A