Coverage for frank/hankel.py: 78%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

82 statements  

1# Frankenstein: 1D disc brightness profile reconstruction from Fourier data 

2# using non-parametric Gaussian Processes 

3# 

4# Copyright (C) 2019-2020 R. Booth, J. Jennings, M. Tazzari 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU General Public License as published by 

8# the Free Software Foundation, either version 3 of the License, or 

9# (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, 

12# but WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

14# GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License 

17# along with this program. If not, see <https://www.gnu.org/licenses/> 

18# 

19"""This module contains functions for computing the 

20discrete Hankel transform (DHT). 

21""" 

22import numpy as np 

23from scipy.special import j0, j1, jn_zeros, jv 

24 

25class DiscreteHankelTransform(object): 

26 r""" 

27 Utilities for computing the discrete Hankel transform. 

28 

29 This class provides the necessary interface to compute 

30 a discrete version of the Hankel transform (DHT): 

31 

32 H[f](q) = \int_0^R_{max} f(r) J_nu(2*pi*q*r) * 2*pi*r dr. 

33 

34 The DHT is based on [1]. 

35 

36 Additionally this class provides coefficients of the DHT [1] transform 

37 matrix 

38 

39 Parameters 

40 ---------- 

41 Rmax : float 

42 Maximum radius beyond which f(r) is zero 

43 N : integer 

44 Number of terms to use in the series 

45 nu : integer, default = 0 

46 Order of the Bessel function, J_nu(r) 

47 

48 References 

49 ---------- 

50 [1] Baddour & Chouinard (2015) 

51 DOI: https://doi.org/10.1364/JOSAA.32.000611 

52 Note: the definition of the DHT used here differs by factors 

53 of 2*pi. 

54 """ 

55 def __init__(self, Rmax, N, nu=0): 

56 

57 # Select the fast Bessel functions, if available 

58 if nu == 0: 

59 self._jnu0 = j0 

60 self._jnup = j1 

61 elif nu == 1: 

62 self._jnu0 = j1 

63 self._jnup = lambda x: jv(2, x) 

64 else: 

65 self._jnu0 = lambda x: jv(nu, x) 

66 self._jnup = lambda x: jv(nu + 1, x) 

67 

68 self._N = N 

69 self._nu = nu 

70 

71 # Compute the collocation points 

72 j_nk = jn_zeros(nu, N + 1) 

73 j_nk, j_nN = j_nk[:-1], j_nk[-1] 

74 

75 Qmax = j_nN / (2 * np.pi * Rmax) 

76 

77 self._Rnk = Rmax * (j_nk / j_nN) 

78 self._Qnk = Qmax * (j_nk / j_nN) 

79 

80 self._Rmax = Rmax 

81 self._Qmax = Qmax 

82 

83 # Compute the weights matrix 

84 Jnk = np.outer(np.ones_like(j_nk), self._jnup(j_nk)) 

85 

86 self._Ykm = (2 / (j_nN * Jnk * Jnk)) * \ 

87 self._jnu0(np.prod(np.meshgrid(j_nk, j_nk/j_nN), axis=0)) 

88 

89 self._scale_factor = 1 / self._jnup(j_nk) ** 2 

90 

91 # Store the extra data needed 

92 self._j_nk = j_nk 

93 self._j_nN = j_nN 

94 

95 @classmethod 

96 def get_collocation_points(cls, Rmax, N, nu=0): 

97 """ 

98 Compute the collocation points for a Hankel Transform. 

99 

100 Parameters 

101 ---------- 

102 Rmax : float 

103 Maximum radius beyond which f(r) is zero 

104 N : integer 

105 Number of terms to use in the series 

106 nu : integer, default = 0 

107 Order of the Bessel function, J_nu(r) 

108 

109 Returns 

110 ------- 

111 Rnk : array, shape=(N,) unit=radians 

112 Radial collocation points in 

113 Qnk : array, shape=(N,) unit=1/radians 

114 Frequency collocation points 

115 """ 

116 

117 j_nk = jn_zeros(nu, N + 1) 

118 j_nk, j_nN = j_nk[:-1], j_nk[-1] 

119 

120 Qmax = j_nN / (2 * np.pi * Rmax) 

121 

122 Rnk = Rmax * (j_nk / j_nN) 

123 Qnk = Qmax * (j_nk / j_nN) 

124 

125 return Rnk, Qnk 

126 

127 def transform(self, f, q=None, direction='forward'): 

128 """ 

129 Compute the Hankel transform of an array 

130 

131 Parameters 

132 ---------- 

133 f : array, size = N 

134 Function to Hankel transform, evaluated at the collocation points: 

135 f[k] = f(r_k) or f[k] = f(q_k) 

136 q : array or None 

137 The frequency points at which to evaluate the Hankel 

138 transform. If not specified, the conjugate points of the 

139 DHT will be used. For the backwards transform, q should be 

140 the radius points 

141 direction : { 'forward', 'backward' }, optional 

142 Direction of the transform. If not supplied, the forward 

143 transform is used 

144 

145 Returns 

146 ------- 

147 H[f] : array, size = N or len(q) if supplied 

148 The Hankel transform of the array f 

149 

150 """ 

151 if q is None: 

152 Y = self._Ykm 

153 

154 if direction == 'forward': 

155 norm = (2 * np.pi * self._Rmax ** 2) / self._j_nN 

156 elif direction == 'backward': 

157 norm = (2 * np.pi * self._Qmax ** 2) / self._j_nN 

158 else: 

159 raise AttributeError("direction must be one of {}" 

160 "".format(['forward', 'backward'])) 

161 else: 

162 Y = self.coefficients(q, direction=direction) 

163 norm = 1.0 

164 

165 return norm * np.dot(Y, f) 

166 

167 def coefficients(self, q=None, direction='forward'): 

168 """ 

169 Coefficients of the transform matrix, defined by 

170 H[f](q) = np.dot(Y, f) 

171 

172 Parameters 

173 ---------- 

174 q : array or None 

175 Frequency points at which to evaluate the transform. If q = None, 

176 the points of the DHT are used. If direction='backward', these 

177 points should instead be the radius points 

178 direction : { 'forward', 'backward' }, optional 

179 Direction of the transform. If not supplied, the forward transform 

180 is used 

181 

182 Returns 

183 ------- 

184 Y : array, size = (len(q), N) 

185 The transformation matrix 

186 """ 

187 if direction == 'forward': 

188 norm = 1 / (np.pi * self._Qmax ** 2) 

189 k = 1. / self._Qmax 

190 elif direction == 'backward': 

191 norm = 1 / (np.pi * self._Rmax ** 2) 

192 k = 1. / self._Rmax 

193 else: 

194 raise AttributeError("direction must be one of {}" 

195 "".format(['forward', 'backward'])) 

196 

197 # For the DHT points we can use the cached Ykm points 

198 if q is None: 

199 return 0.5 * self._j_nN * norm * self._Ykm 

200 

201 H = (norm * self._scale_factor) * \ 

202 self._jnu0(np.outer(k * q, self._j_nk)) 

203 

204 return H 

205 

206 def interpolation_coefficients(self, q, space='Real'): 

207 """ 

208 Coefficients of the interpolation matrix, defined by 

209 f(q) = np.dot(Y, f) 

210 

211 Parameters 

212 ---------- 

213 q : array or None 

214 The points at which to evaluate the interpolation. 

215 space : { 'Real', 'Fourier' }, optional 

216 Space in which the interpolation is done. If not supplied,  

217 'Real' is assumed. 

218 

219 Returns 

220 ------- 

221 Y : array, size = (len(q), N) 

222 The interpolation matrix 

223 """ 

224 if space == 'Real': 

225 x = np.atleast_1d(2*np.pi * q * self._Qmax) 

226 elif space == 'Fourier': 

227 x = np.atleast_1d(2*np.pi * q * self._Rmax) 

228 else: 

229 raise ValueError("Space must be one of 'Real' or 'Fourier', not " 

230 f"{space}.") 

231 

232 coeff = np.outer(np.where(x < self._j_nN, self._jnu0(x), 0), 

233 2*self._j_nk / self._jnup(self._j_nk)) 

234 Y = coeff / (self._j_nk.reshape(1,-1)**2 - x.reshape(-1, 1)**2) 

235 

236 return Y 

237 

238 def interpolate(self, f, q, space='Real'): 

239 """ 

240 Interpolate f (evaluated at the collocation points) to the new points, 

241 pts, using interpolation that is consistent with the Fourier-Bessel 

242 Series / Discrete Hankel Transform. 

243 

244 Parameters 

245 ---------- 

246 f : array, size = N 

247 Function to interpolate, evaluated at the collocation points: 

248 f[k] = f(r_k) or f[k] = f(q_k) 

249 q : array or None 

250 The points at which to evaluate the interpolation. 

251 space : { 'Real', 'Fourier' }, optional 

252 Space in which the interpolation is done. If not supplied,  

253 'Real' is assumed. 

254 

255 

256 Returns 

257 ------- 

258 f_interp : array, size = len(q) 

259 The interpolated results 

260 """ 

261 Y = self.interpolation_coefficients(q, space) 

262 

263 return np.dot(Y, f) 

264 

265 

266 @property 

267 def r(self): 

268 """Radius points""" 

269 return self._Rnk 

270 

271 @property 

272 def Rmax(self): 

273 """Maximum radius""" 

274 return self._Rmax 

275 

276 @property 

277 def q(self): 

278 """Frequency points""" 

279 return self._Qnk 

280 

281 @property 

282 def Qmax(self): 

283 """Maximum frequency""" 

284 return self._Qmax 

285 

286 @property 

287 def size(self): 

288 """Number of points used in the DHT""" 

289 return self._N 

290 

291 @property 

292 def order(self): 

293 """Order of the Bessel function""" 

294 return self._nu