Coverage for PyFHD/gridding/visibility_degrid.py: 3%

176 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-07-01 10:58 +0800

1import numpy as np 

2from numpy.typing import NDArray 

3import warnings 

4import h5py 

5from PyFHD.gridding.gridding_utils import ( 

6 baseline_grid_locations, 

7 interpolate_kernel, 

8 grid_beam_per_baseline, 

9) 

10from PyFHD.pyfhd_tools.pyfhd_utils import ( 

11 l_m_n, 

12 rebin, 

13 weight_invert, 

14 histogram, 

15 deriv_coefficients, 

16) 

17 

18 

19def visibility_degrid( 

20 image_uv: NDArray[np.complex128], 

21 vis_weights: NDArray[np.float64], 

22 obs: dict, 

23 psf: dict | h5py.File, 

24 params: dict, 

25 polarization: int = 0, 

26 fill_model_visibilities: bool = False, 

27 vis_input: NDArray[np.complex128] | None = None, 

28 spectral_model_uv_arr: NDArray[np.float64] | None = None, 

29 beam_per_baseline: bool = False, 

30 uv_grid_phase_only: bool = True, 

31 conserve_memory: bool = False, 

32 memory_threshold: float | int = 1e8, 

33): 

34 """ 

35 Generate visibilities from a 2D hyperresolved {u,v} plane using the Fourier transform of the beam sensitivity as the 

36 kernel (or integration function). The input {u,v} plane is the slant-orthographic projection of the sky when Fourier 

37 transformed with no instrumental effects. The integration kernel adds the instrumental effects to the visiblities. 

38 

39 Degridding is performed only on simulated model {u,v} planes. Simulated sources towards the edge of the sky image will be 

40 distorted (smeared) due to the projection of the 2D {u,v} plane. 

41 

42 The kernel is a extremely hyperresolved look-up table, which is (optionally) interpolated even further. 

43 Since the {u,v} pixels are discrete and the baseline locations are not, the kernel will integrate the pixels 

44 in a unique way for each individual baseline. This code is optimized to provide the best estimate for each 

45 baseline whilst maintaining speed. 

46 

47 Parameters 

48 ---------- 

49 image_uv : NDArray[np.complex128] 

50 A simulated {u,v} plane with no instrumental effects 

51 vis_weight : NDArray[np.float64] 

52 Weights (flags) of the visibilities 

53 obs : dict 

54 Observation metadata dictionary 

55 psf : dict | h5py.File 

56 Beam metadata dictionary 

57 params : dict 

58 Visibility metadata dictionary 

59 polarization : int 

60 Index of the current polarization, by default 0 

61 fill_model_visibilities : bool, optional 

62 Create all model visibilities disregarding flags, by default False 

63 vis_input : NDArray[np.complex128] | None, optional 

64 Extra model visibilities to add to the degridded products, by default None 

65 spectral_model_uv_arr : NDArray[np.float64] | None, optional 

66 Additional {u,v} planes to degrid for complicated source spectral dependencies, by default None 

67 beam_per_baseline : bool, optional 

68 Generate beams with corrective phases given the baseline location, by default False 

69 uv_grid_phase_only : bool, optional 

70 Generate beams with only {u,v} corrective phases, disregarding w phases, by default True 

71 conserve_memory : bool, optional 

72 Reduce memory load by running loops, by default False 

73 

74 Returns 

75 ------- 

76 visibility_array : NDArray[np.complex128] 

77 A simulated visibility array from degridding the input {u,v} plane with the instrumental kernel 

78 obs : dict 

79 Updated observation metadata dictionary 

80 """ 

81 

82 complex_flag = psf["complex_flag"][0] 

83 n_spectral = obs["degrid_spectral_terms"][0] 

84 interp_flag = psf["interpolate_kernel"][0] 

85 if conserve_memory: 

86 # memory threshold is in bytes 

87 if memory_threshold < 1e6: 

88 memory_threshold = 1e8 

89 

90 # If both beam and interp_flag leave a warning, prioritise beam_per_baseline 

91 if beam_per_baseline and interp_flag: 

92 warnings.warn( 

93 "Cannot have beam per baseline and interpolation at the same time, turning off interpolation" 

94 ) 

95 interp_flag = False 

96 

97 # For each unflagged baseline, get the minimum contributing pixel number for gridding 

98 # and the 2D derivatives for bilinear interpolation 

99 baselines_dict = baseline_grid_locations( 

100 obs, 

101 psf, 

102 params, 

103 vis_weights, 

104 fill_model_visibilities=fill_model_visibilities, 

105 interp_flag=interp_flag, 

106 ) 

107 

108 # Extract data from the data structures 

109 bin_n = baselines_dict["bin_n"] 

110 bin_i = baselines_dict["bin_i"] 

111 n_bin_use = baselines_dict["n_bin_use"] 

112 ri = baselines_dict["ri"] 

113 xmin = baselines_dict["xmin"] 

114 ymin = baselines_dict["ymin"] 

115 x_offset = baselines_dict["x_offset"] 

116 y_offset = baselines_dict["y_offset"] 

117 if interp_flag: 

118 dx0dy0_arr = baselines_dict["dx0dy0_arr"] 

119 dx0dy1_arr = baselines_dict["dx0dy1_arr"] 

120 dx1dy0_arr = baselines_dict["dx1dy0_arr"] 

121 dx1dy1_arr = baselines_dict["dx1dy1_arr"] 

122 dimension = obs["dimension"][0] 

123 kbinsize = obs["kpix"][0] 

124 freq_bin_i = obs["baseline_info"][0]["fbin_i"][0] 

125 frequency_array = obs["baseline_info"][0]["freq"][0] 

126 freq_delta = (frequency_array - obs["freq_center"][0]) / obs["freq_center"][0] 

127 psf_dim = psf["dim"][0] 

128 psf_resolution = psf["resolution"][0] 

129 psf_dim3 = int(psf_dim**2) 

130 n_baselines = obs["n_baselines"][0] 

131 n_samples = obs["n_time"][0] 

132 n_freq_use = frequency_array.size 

133 n_freq = obs["n_freq"][0] 

134 group_arr = np.squeeze(psf["id"][0][:, freq_bin_i, polarization]) 

135 beam_arr = psf["beam_ptr"][0] 

136 

137 if beam_per_baseline: 

138 uu = params["uu"][0] 

139 vv = params["vv"][0] 

140 ww = params["ww"][0] 

141 psf_image_dim = psf["image_info"][0]["psf_image_dim"][0] 

142 psf_intermediate_res = np.min( 

143 np.ceil(np.sqrt(psf_resolution) / 2) * 2, psf_resolution 

144 ) 

145 image_bot = -(psf_dim / 2) * psf_intermediate_res + psf_image_dim / 2 

146 image_top = ( 

147 (psf_dim * psf_resolution - 1) 

148 - (psf_dim / 2) * psf_intermediate_res 

149 + psf_image_dim / 2 

150 ) 

151 

152 l_mode, m_mode, n_tracked = l_m_n(obs, psf) 

153 

154 # w-terms have not been tested, thus they've been turned off for now 

155 if uv_grid_phase_only: 

156 n_tracked = np.zeros_like(n_tracked) 

157 

158 beam_int = np.zeros(n_freq) 

159 beam2_int = np.zeros(n_freq) 

160 n_grp_use = np.zeros(n_freq) 

161 primary_beam_area = np.zeros(n_freq) 

162 primary_beam_sq_area = np.zeros(n_freq) 

163 

164 x = (np.arange(dimension) - dimension / 2) * kbinsize 

165 y = x.copy() 

166 

167 conj_i = np.where(params["vv"][0] > 0) 

168 if conj_i[0].size > 0: 

169 if beam_per_baseline: 

170 uu[conj_i] = -uu[conj_i] 

171 vv[conj_i] = -vv[conj_i] 

172 ww[conj_i] = -ww[conj_i] 

173 

174 # Create the correct size visibility array 

175 vis_dimension = n_baselines * n_samples 

176 visibility_array = np.zeros((vis_dimension, n_freq), dtype=np.cdouble) 

177 

178 ind_ref = np.arange(max(bin_n)) 

179 

180 if complex_flag: 

181 arr_type = np.cdouble 

182 else: 

183 arr_type = np.double 

184 

185 if n_spectral: 

186 prefactor = np.empty(n_spectral, dtype=object) 

187 for s_i in range(n_spectral): 

188 prefactor[s_i] = deriv_coefficients(s_i + 1, divide_factorial=True) 

189 box_arr_ptr = np.empty(n_spectral, dtype=object) 

190 

191 for bi in range(n_bin_use): 

192 vis_n = bin_n[bin_i[bi]] 

193 """ 

194 Python is not inclusive of end of loop  

195 while IDL is, as such n_bin_use - 1 does not do 

196 the last index properly on Python, as such when we 

197 have reached the last index we need to change the  

198 indexation of ri to include from that point to the end. 

199 """ 

200 if bi == n_bin_use - 1: 

201 inds = ri[ri[bin_i[bi]] :] 

202 else: 

203 inds = ri[ri[bin_i[bi]] : ri[bin_i[bi + 1]]] 

204 

205 # if constraining memory usage, then est number of loops needed 

206 if conserve_memory: 

207 required_bytes = 8 * vis_n * psf_dim3 

208 mem_iter = int(np.ceil(required_bytes / memory_threshold)) 

209 if mem_iter > 1: 

210 vis_n_full = vis_n 

211 inds_full = inds 

212 vis_n_per_iter = int(np.ceil(vis_n_full / mem_iter)) 

213 else: 

214 mem_iter = 1 

215 # loop over chunks of visibilities to grid to conserve memory 

216 for mem_i in range(mem_iter): 

217 if mem_iter > 1: 

218 # calculate the indices of this visibility chunk if split into multiple chunks 

219 if vis_n_per_iter * (mem_i + 1) > vis_n_full: 

220 max_ind = vis_n_full 

221 else: 

222 max_ind = vis_n_per_iter * (mem_i + 1) 

223 inds = inds_full[vis_n_per_iter * mem_i : max_ind] 

224 vis_n = max_ind - vis_n_per_iter * mem_i 

225 

226 x_off = x_offset.flat[inds].astype(np.int64) 

227 y_off = y_offset.flat[inds].astype(np.int64) 

228 # xmin and ymin should be all the same 

229 xmin_use = xmin.flat[inds[0]] 

230 ymin_use = ymin.flat[inds[0]] 

231 freq_i = inds % n_freq_use 

232 fbin = freq_bin_i[freq_i] 

233 baseline_inds = (inds / n_freq_use).astype(int) % n_baselines 

234 

235 box_matrix = np.zeros((vis_n, psf_dim3), dtype=arr_type) 

236 box_arr = image_uv[ 

237 ymin_use : ymin_use + psf_dim, xmin_use : xmin_use + psf_dim 

238 ].flatten() 

239 

240 if interp_flag: 

241 dx0dy0 = dx0dy0_arr.flat[inds] 

242 dx0dy1 = dx0dy1_arr.flat[inds] 

243 dx1dy0 = dx1dy0_arr.flat[inds] 

244 dx1dy1 = dx1dy1_arr.flat[inds] 

245 ind_remap_flag = False 

246 bt_index = inds / n_freq_use 

247 for ii in range(vis_n): 

248 kernel = interpolate_kernel( 

249 beam_arr[baseline_inds[ii], fbin[ii], polarization], 

250 x_off[ii], 

251 y_off[ii], 

252 dx0dy0[ii], 

253 dx1dy0[ii], 

254 dx0dy1[ii], 

255 dx1dy1[ii], 

256 ) 

257 box_matrix.flat[psf_dim3 * ii : psf_dim3 * ii + kernel.size] = ( 

258 kernel 

259 ) 

260 else: 

261 group_id = group_arr[inds] 

262 group_max = np.max(group_id) + 1 

263 xyf_i = ( 

264 x_off + y_off * psf_resolution + fbin * psf_resolution**2 

265 ) * group_max + group_id 

266 xyf_si = np.sort(xyf_i) 

267 xyf_i = xyf_i[xyf_si] 

268 xyf_ui = np.unique(xyf_i) 

269 n_xyf_bin = xyf_ui.size 

270 

271 # There might be a better selection criteria to determine which is more efficient 

272 if vis_n > np.ceil(1.1 * n_xyf_bin) and not beam_per_baseline: 

273 ind_remap_flag = True 

274 inds = inds[xyf_si] 

275 inds_use = xyf_si[xyf_ui] 

276 freq_i = freq_i[inds_use] 

277 x_off = x_off[inds_use] 

278 y_off = y_off[inds_use] 

279 fbin = fbin[inds_use] 

280 baseline_inds = baseline_inds[inds] 

281 

282 if n_xyf_bin == 1: 

283 ind_remap = np.arange(vis_n, dtype=int) 

284 else: 

285 hist_inds_u, _, ri_xyf = histogram(xyf_ui, bin_size=1, min=0) 

286 ind_remap = ind_ref[ri_xyf[0 : hist_inds_u.size] - ri_xyf[0]] 

287 

288 vis_n = n_xyf_bin 

289 else: 

290 ind_remap_flag = False 

291 bt_index = inds / n_freq_use 

292 

293 if beam_per_baseline: 

294 box_matrix = grid_beam_per_baseline( 

295 psf, 

296 uu, 

297 vv, 

298 ww, 

299 l_mode, 

300 m_mode, 

301 n_tracked, 

302 frequency_array, 

303 x, 

304 y, 

305 xmin_use, 

306 ymin_use, 

307 freq_i, 

308 bt_index, 

309 polarization, 

310 fbin, 

311 image_bot, 

312 image_top, 

313 psf_dim3, 

314 box_matrix, 

315 vis_n, 

316 beam_int=beam_int, 

317 beam2_int=beam2_int, 

318 n_grp_use=n_grp_use, 

319 degrid_flag=True, 

320 obs=obs, 

321 params=params, 

322 weights=vis_weights, 

323 ) 

324 else: 

325 for ii in range(vis_n): 

326 # more efficient array subscript notation 

327 box_matrix[psf_dim3 * ii] = beam_arr[ 

328 baseline_inds[ii], fbin[ii], polarization 

329 ][y_off[ii], x_off[ii]] 

330 

331 if n_spectral: 

332 vis_box = np.dot(box_arr, np.transpose(box_matrix)) 

333 freq_term_arr = rebin( 

334 np.transpose(freq_delta[freq_i]), (vis_n, psf_dim3), sample=True 

335 ) 

336 for s_i in range(n_spectral): 

337 # s_i loop is over terms of the Taylor expansion, starting from the lowest-order term 

338 prefactor_use = prefactor[s_i] 

339 box_matrix *= freq_term_arr 

340 box_arr_ptr[s_i] = spectral_model_uv_arr[s_i][ 

341 ymin_use : ymin_use + psf_dim - 1, 

342 xmin_use : xmin_use + psf_dim - 1, 

343 ].flatten() 

344 

345 for s_i_i in range(s_i): 

346 # s_i_i loop is over powers of the model x alpha^n, n=s_i_i+1 

347 box_arr = prefactor_use[s_i_i] * box_arr_ptr[s_i_i] 

348 vis_box += np.dot(box_arr, np.transpose(box_matrix)) 

349 del box_arr_ptr 

350 else: 

351 vis_box = np.dot(box_arr, np.transpose(box_matrix)) 

352 if ind_remap_flag: 

353 vis_box = vis_box[ind_remap] 

354 visibility_array.flat[inds] = vis_box 

355 

356 if beam_per_baseline: 

357 # factor of kbinsize^2 is FFT units normalization 

358 beam2_int *= weight_invert(n_grp_use) / kbinsize**2 

359 beam_int *= weight_invert(n_grp_use) / kbinsize**2 

360 primary_beam_area = beam2_int 

361 primary_beam_area = beam_int 

362 # Returning obs in this case? 

363 obs["primary_beam_area"][polarization] = primary_beam_area 

364 obs["primary_beam_sq_area"][polarization] = primary_beam_sq_area 

365 

366 del (x_offset, y_offset, xmin, ymin, bin_n) 

367 if conj_i[0].size > 0: 

368 visibility_array[conj_i, :] = np.conj(visibility_array[conj_i, :]) 

369 

370 if vis_input is not None: 

371 visibility_array += vis_input 

372 

373 if beam_per_baseline: 

374 return visibility_array, obs 

375 else: 

376 return visibility_array