_fftlog.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. '''Fast Hankel transforms using the FFTLog algorithm.
  2. The implementation closely follows the Fortran code of Hamilton (2000).
  3. added: 14/11/2020 Nicolas Tessore <n.tessore@ucl.ac.uk>
  4. '''
  5. import numpy as np
  6. from warnings import warn
  7. from ._basic import rfft, irfft
  8. from ..special import loggamma, poch
  9. __all__ = [
  10. 'fht', 'ifht',
  11. 'fhtoffset',
  12. ]
  13. # constants
  14. LN_2 = np.log(2)
  15. def fht(a, dln, mu, offset=0.0, bias=0.0):
  16. r'''Compute the fast Hankel transform.
  17. Computes the discrete Hankel transform of a logarithmically spaced periodic
  18. sequence using the FFTLog algorithm [1]_, [2]_.
  19. Parameters
  20. ----------
  21. a : array_like (..., n)
  22. Real periodic input array, uniformly logarithmically spaced. For
  23. multidimensional input, the transform is performed over the last axis.
  24. dln : float
  25. Uniform logarithmic spacing of the input array.
  26. mu : float
  27. Order of the Hankel transform, any positive or negative real number.
  28. offset : float, optional
  29. Offset of the uniform logarithmic spacing of the output array.
  30. bias : float, optional
  31. Exponent of power law bias, any positive or negative real number.
  32. Returns
  33. -------
  34. A : array_like (..., n)
  35. The transformed output array, which is real, periodic, uniformly
  36. logarithmically spaced, and of the same shape as the input array.
  37. See Also
  38. --------
  39. ifht : The inverse of `fht`.
  40. fhtoffset : Return an optimal offset for `fht`.
  41. Notes
  42. -----
  43. This function computes a discrete version of the Hankel transform
  44. .. math::
  45. A(k) = \int_{0}^{\infty} \! a(r) \, J_\mu(kr) \, k \, dr \;,
  46. where :math:`J_\mu` is the Bessel function of order :math:`\mu`. The index
  47. :math:`\mu` may be any real number, positive or negative.
  48. The input array `a` is a periodic sequence of length :math:`n`, uniformly
  49. logarithmically spaced with spacing `dln`,
  50. .. math::
  51. a_j = a(r_j) \;, \quad
  52. r_j = r_c \exp[(j-j_c) \, \mathtt{dln}]
  53. centred about the point :math:`r_c`. Note that the central index
  54. :math:`j_c = (n-1)/2` is half-integral if :math:`n` is even, so that
  55. :math:`r_c` falls between two input elements. Similarly, the output
  56. array `A` is a periodic sequence of length :math:`n`, also uniformly
  57. logarithmically spaced with spacing `dln`
  58. .. math::
  59. A_j = A(k_j) \;, \quad
  60. k_j = k_c \exp[(j-j_c) \, \mathtt{dln}]
  61. centred about the point :math:`k_c`.
  62. The centre points :math:`r_c` and :math:`k_c` of the periodic intervals may
  63. be chosen arbitrarily, but it would be usual to choose the product
  64. :math:`k_c r_c = k_j r_{n-1-j} = k_{n-1-j} r_j` to be unity. This can be
  65. changed using the `offset` parameter, which controls the logarithmic offset
  66. :math:`\log(k_c) = \mathtt{offset} - \log(r_c)` of the output array.
  67. Choosing an optimal value for `offset` may reduce ringing of the discrete
  68. Hankel transform.
  69. If the `bias` parameter is nonzero, this function computes a discrete
  70. version of the biased Hankel transform
  71. .. math::
  72. A(k) = \int_{0}^{\infty} \! a_q(r) \, (kr)^q \, J_\mu(kr) \, k \, dr
  73. where :math:`q` is the value of `bias`, and a power law bias
  74. :math:`a_q(r) = a(r) \, (kr)^{-q}` is applied to the input sequence.
  75. Biasing the transform can help approximate the continuous transform of
  76. :math:`a(r)` if there is a value :math:`q` such that :math:`a_q(r)` is
  77. close to a periodic sequence, in which case the resulting :math:`A(k)` will
  78. be close to the continuous transform.
  79. References
  80. ----------
  81. .. [1] Talman J. D., 1978, J. Comp. Phys., 29, 35
  82. .. [2] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
  83. Examples
  84. --------
  85. This example is the adapted version of ``fftlogtest.f`` which is provided
  86. in [2]_. It evaluates the integral
  87. .. math::
  88. \int^\infty_0 r^{\mu+1} \exp(-r^2/2) J_\mu(k, r) k dr
  89. = k^{\mu+1} \exp(-k^2/2) .
  90. >>> import numpy as np
  91. >>> from scipy import fft
  92. >>> import matplotlib.pyplot as plt
  93. Parameters for the transform.
  94. >>> mu = 0.0 # Order mu of Bessel function
  95. >>> r = np.logspace(-7, 1, 128) # Input evaluation points
  96. >>> dln = np.log(r[1]/r[0]) # Step size
  97. >>> offset = fft.fhtoffset(dln, initial=-6*np.log(10), mu=mu)
  98. >>> k = np.exp(offset)/r[::-1] # Output evaluation points
  99. Define the analytical function.
  100. >>> def f(x, mu):
  101. ... """Analytical function: x^(mu+1) exp(-x^2/2)."""
  102. ... return x**(mu + 1)*np.exp(-x**2/2)
  103. Evaluate the function at ``r`` and compute the corresponding values at
  104. ``k`` using FFTLog.
  105. >>> a_r = f(r, mu)
  106. >>> fht = fft.fht(a_r, dln, mu=mu, offset=offset)
  107. For this example we can actually compute the analytical response (which in
  108. this case is the same as the input function) for comparison and compute the
  109. relative error.
  110. >>> a_k = f(k, mu)
  111. >>> rel_err = abs((fht-a_k)/a_k)
  112. Plot the result.
  113. >>> figargs = {'sharex': True, 'sharey': True, 'constrained_layout': True}
  114. >>> fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), **figargs)
  115. >>> ax1.set_title(r'$r^{\mu+1}\ \exp(-r^2/2)$')
  116. >>> ax1.loglog(r, a_r, 'k', lw=2)
  117. >>> ax1.set_xlabel('r')
  118. >>> ax2.set_title(r'$k^{\mu+1} \exp(-k^2/2)$')
  119. >>> ax2.loglog(k, a_k, 'k', lw=2, label='Analytical')
  120. >>> ax2.loglog(k, fht, 'C3--', lw=2, label='FFTLog')
  121. >>> ax2.set_xlabel('k')
  122. >>> ax2.legend(loc=3, framealpha=1)
  123. >>> ax2.set_ylim([1e-10, 1e1])
  124. >>> ax2b = ax2.twinx()
  125. >>> ax2b.loglog(k, rel_err, 'C0', label='Rel. Error (-)')
  126. >>> ax2b.set_ylabel('Rel. Error (-)', color='C0')
  127. >>> ax2b.tick_params(axis='y', labelcolor='C0')
  128. >>> ax2b.legend(loc=4, framealpha=1)
  129. >>> ax2b.set_ylim([1e-9, 1e-3])
  130. >>> plt.show()
  131. '''
  132. # size of transform
  133. n = np.shape(a)[-1]
  134. # bias input array
  135. if bias != 0:
  136. # a_q(r) = a(r) (r/r_c)^{-q}
  137. j_c = (n-1)/2
  138. j = np.arange(n)
  139. a = a * np.exp(-bias*(j - j_c)*dln)
  140. # compute FHT coefficients
  141. u = fhtcoeff(n, dln, mu, offset=offset, bias=bias)
  142. # transform
  143. A = _fhtq(a, u)
  144. # bias output array
  145. if bias != 0:
  146. # A(k) = A_q(k) (k/k_c)^{-q} (k_c r_c)^{-q}
  147. A *= np.exp(-bias*((j - j_c)*dln + offset))
  148. return A
  149. def ifht(A, dln, mu, offset=0.0, bias=0.0):
  150. r'''Compute the inverse fast Hankel transform.
  151. Computes the discrete inverse Hankel transform of a logarithmically spaced
  152. periodic sequence. This is the inverse operation to `fht`.
  153. Parameters
  154. ----------
  155. A : array_like (..., n)
  156. Real periodic input array, uniformly logarithmically spaced. For
  157. multidimensional input, the transform is performed over the last axis.
  158. dln : float
  159. Uniform logarithmic spacing of the input array.
  160. mu : float
  161. Order of the Hankel transform, any positive or negative real number.
  162. offset : float, optional
  163. Offset of the uniform logarithmic spacing of the output array.
  164. bias : float, optional
  165. Exponent of power law bias, any positive or negative real number.
  166. Returns
  167. -------
  168. a : array_like (..., n)
  169. The transformed output array, which is real, periodic, uniformly
  170. logarithmically spaced, and of the same shape as the input array.
  171. See Also
  172. --------
  173. fht : Definition of the fast Hankel transform.
  174. fhtoffset : Return an optimal offset for `ifht`.
  175. Notes
  176. -----
  177. This function computes a discrete version of the Hankel transform
  178. .. math::
  179. a(r) = \int_{0}^{\infty} \! A(k) \, J_\mu(kr) \, r \, dk \;,
  180. where :math:`J_\mu` is the Bessel function of order :math:`\mu`. The index
  181. :math:`\mu` may be any real number, positive or negative.
  182. See `fht` for further details.
  183. '''
  184. # size of transform
  185. n = np.shape(A)[-1]
  186. # bias input array
  187. if bias != 0:
  188. # A_q(k) = A(k) (k/k_c)^{q} (k_c r_c)^{q}
  189. j_c = (n-1)/2
  190. j = np.arange(n)
  191. A = A * np.exp(bias*((j - j_c)*dln + offset))
  192. # compute FHT coefficients
  193. u = fhtcoeff(n, dln, mu, offset=offset, bias=bias)
  194. # transform
  195. a = _fhtq(A, u, inverse=True)
  196. # bias output array
  197. if bias != 0:
  198. # a(r) = a_q(r) (r/r_c)^{q}
  199. a /= np.exp(-bias*(j - j_c)*dln)
  200. return a
  201. def fhtcoeff(n, dln, mu, offset=0.0, bias=0.0):
  202. '''Compute the coefficient array for a fast Hankel transform.
  203. '''
  204. lnkr, q = offset, bias
  205. # Hankel transform coefficients
  206. # u_m = (kr)^{-i 2m pi/(n dlnr)} U_mu(q + i 2m pi/(n dlnr))
  207. # with U_mu(x) = 2^x Gamma((mu+1+x)/2)/Gamma((mu+1-x)/2)
  208. xp = (mu+1+q)/2
  209. xm = (mu+1-q)/2
  210. y = np.linspace(0, np.pi*(n//2)/(n*dln), n//2+1)
  211. u = np.empty(n//2+1, dtype=complex)
  212. v = np.empty(n//2+1, dtype=complex)
  213. u.imag[:] = y
  214. u.real[:] = xm
  215. loggamma(u, out=v)
  216. u.real[:] = xp
  217. loggamma(u, out=u)
  218. y *= 2*(LN_2 - lnkr)
  219. u.real -= v.real
  220. u.real += LN_2*q
  221. u.imag += v.imag
  222. u.imag += y
  223. np.exp(u, out=u)
  224. # fix last coefficient to be real
  225. u.imag[-1] = 0
  226. # deal with special cases
  227. if not np.isfinite(u[0]):
  228. # write u_0 = 2^q Gamma(xp)/Gamma(xm) = 2^q poch(xm, xp-xm)
  229. # poch() handles special cases for negative integers correctly
  230. u[0] = 2**q * poch(xm, xp-xm)
  231. # the coefficient may be inf or 0, meaning the transform or the
  232. # inverse transform, respectively, is singular
  233. return u
  234. def fhtoffset(dln, mu, initial=0.0, bias=0.0):
  235. '''Return optimal offset for a fast Hankel transform.
  236. Returns an offset close to `initial` that fulfils the low-ringing
  237. condition of [1]_ for the fast Hankel transform `fht` with logarithmic
  238. spacing `dln`, order `mu` and bias `bias`.
  239. Parameters
  240. ----------
  241. dln : float
  242. Uniform logarithmic spacing of the transform.
  243. mu : float
  244. Order of the Hankel transform, any positive or negative real number.
  245. initial : float, optional
  246. Initial value for the offset. Returns the closest value that fulfils
  247. the low-ringing condition.
  248. bias : float, optional
  249. Exponent of power law bias, any positive or negative real number.
  250. Returns
  251. -------
  252. offset : float
  253. Optimal offset of the uniform logarithmic spacing of the transform that
  254. fulfils a low-ringing condition.
  255. See Also
  256. --------
  257. fht : Definition of the fast Hankel transform.
  258. References
  259. ----------
  260. .. [1] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
  261. '''
  262. lnkr, q = initial, bias
  263. xp = (mu+1+q)/2
  264. xm = (mu+1-q)/2
  265. y = np.pi/(2*dln)
  266. zp = loggamma(xp + 1j*y)
  267. zm = loggamma(xm + 1j*y)
  268. arg = (LN_2 - lnkr)/dln + (zp.imag + zm.imag)/np.pi
  269. return lnkr + (arg - np.round(arg))*dln
  270. def _fhtq(a, u, inverse=False):
  271. '''Compute the biased fast Hankel transform.
  272. This is the basic FFTLog routine.
  273. '''
  274. # size of transform
  275. n = np.shape(a)[-1]
  276. # check for singular transform or singular inverse transform
  277. if np.isinf(u[0]) and not inverse:
  278. warn('singular transform; consider changing the bias')
  279. # fix coefficient to obtain (potentially correct) transform anyway
  280. u = u.copy()
  281. u[0] = 0
  282. elif u[0] == 0 and inverse:
  283. warn('singular inverse transform; consider changing the bias')
  284. # fix coefficient to obtain (potentially correct) inverse anyway
  285. u = u.copy()
  286. u[0] = np.inf
  287. # biased fast Hankel transform via real FFT
  288. A = rfft(a, axis=-1)
  289. if not inverse:
  290. # forward transform
  291. A *= u
  292. else:
  293. # backward transform
  294. A /= u.conj()
  295. A = irfft(A, n, axis=-1)
  296. A = A[..., ::-1]
  297. return A