Coverage for PyFHD/healpix/healpix_utils.py: 58%

247 statements  

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

1import sys 

2from logging import Logger 

3from pathlib import Path 

4import h5py 

5import importlib_resources 

6import numpy as np 

7from numpy.typing import NDArray 

8from healpy import query_disc 

9from healpy.pixelfunc import ang2vec, pix2vec, vec2ang 

10from PyFHD.beam_setup.beam_utils import beam_image 

11from PyFHD.gridding.visibility_grid import visibility_grid 

12from PyFHD.gridding.gridding_utils import dirty_image_generate 

13from PyFHD.io.pyfhd_io import load, save 

14from PyFHD.pyfhd_tools.pyfhd_utils import ( 

15 angle_difference, 

16 histogram, 

17 meshgrid, 

18 region_grow, 

19) 

20from PyFHD.pyfhd_tools.unit_conv import radec_to_altaz, radec_to_pixel 

21from astropy.coordinates import EarthLocation 

22 

23 

24def healpix_cnv_apply( 

25 image: NDArray[np.integer | np.floating | np.complexfloating], hpx_cnv: dict 

26) -> NDArray[np.float64]: 

27 """ 

28 healpix_cnv_apply creates a map based off the array/image and healpix convention dictionary given. 

29 In FHD the healpix_cnv_apply was mainly used as a wrapper for sprsax2, as such I will put the code 

30 for sprsax2 in here as PyFHD will only use sprsax2 here as we don't have the holographic mapping 

31 function at this time. 

32 

33 Vectors 'sa' (interpolation) and 'ija' (index) follow the row-index sparse storage mode as described 

34 in section 2.7 of Numerical Recipes in C, 2nd edition. 

35 

36 Parameters 

37 ---------- 

38 image : NDArray[np.integer | np.floating | np.complexfloating] 

39 An orthoslant image array that is to be converted to a HEALPix map. 

40 hpx_cnv : dict 

41 The HEALPix index/interpolation dictionary 

42 

43 Returns 

44 ------- 

45 hpx_map: NDArray[np.float64] 

46 A flattened 1D HEALPix array with the interpolated values from the image. 

47 """ 

48 # Is B in sprsax2 

49 hpx_map = np.zeros(np.size(hpx_cnv["inds"])) 

50 # Is X in sprsax2 or X_use 

51 image_vector = image.flatten() 

52 for i in range(0, np.size(hpx_cnv["i_use"])): 

53 i_use = hpx_cnv["i_use"][i] 

54 # Transpose is always True when used inside healpix_cnv_apply when using 

55 # sprsax2, so we can ignore the transpose keyword 

56 hpx_map[hpx_cnv["ija"][i]] += hpx_cnv["sa"][i] * image_vector[i_use] 

57 return hpx_map 

58 

59 

60def healpix_cnv_generate( 

61 obs: dict, 

62 mask: NDArray[np.int64] | None, 

63 hpx_radius: float, 

64 pyfhd_config: dict, 

65 logger: Logger, 

66 nside: float = None, 

67) -> dict: 

68 """ 

69 Generate the HEALPix index/interpolation dictionary that is used to convert 

70 between orthoslant image pixelization and the RING index HEALPix pixelization 

71 scheme. Binlinear interpolation is performed to HEALPix pixels centers. All 

72 angles are in degrees. 

73 

74 Parameters 

75 ---------- 

76 obs : dict 

77 Observation metadata dictionary 

78 mask : np.ndarray 

79 If HEALPix indices are not provided in the config file, this boolean 

80 mask map of the orthoslant image can be used to mask out areas from the 

81 HEALPix conversion. 

82 hpx_radius : float 

83 If HEALPix indices are not provided in the config file, this selects 

84 a radius in degrees, centred on the RA/Dec of the observation, to be 

85 converted to HEALPix pixels. 

86 pyfhd_config : dict 

87 PyFHD configuration settings 

88 logger : Logger 

89 PyFHD's Logger 

90 nside : float 

91 The HEALPix nside parameter, calculated based on the observation 

92 metadata if none provided. Defaults to None. 

93 

94 Returns 

95 ------- 

96 dict 

97 A dictionary containing the orthoslant to HEALPix pixel mapping and 

98 interpolation. Vectors 'sa' (bilinear interpolation calculated from 

99 fractional offsets) and 'ija' (flattened index) follow the row-index 

100 sparse storage mode as described in section 2.7 of Numerical Recipes 

101 in C, 2nd edition. 

102 """ 

103 

104 # Have ignored the code that relates to hpx_radius being empty, in PyFHD's case 

105 # we will assume that a value for hpx_radius is supplied, if you want to add it 

106 # yourself you can do that here 

107 hpx_inds = None 

108 # Fill hpx_inds and nside with values from a file if restrict_healpix_inds has been activated 

109 if pyfhd_config["restrict_healpix_inds"]: 109 ↛ 171line 109 didn't jump to line 171 because the condition on line 109 was always true

110 if pyfhd_config["healpix_inds"] is None: 110 ↛ 166line 110 didn't jump to line 166 because the condition on line 110 was always true

111 # Get the healpix indexes based off the observation, comes from observation_healpix_inds_select 

112 files = np.array( 

113 [ 

114 { 

115 "name": "EoR0_high_healpix_inds.h5", 

116 "ra": 0, 

117 "dec": -30, 

118 "freq": 182, 

119 }, 

120 { 

121 "name": "EoR0_low_healpix_inds.h5", 

122 "ra": 0, 

123 "dec": -30, 

124 "freq": 151, 

125 }, 

126 { 

127 "name": "EoR1_high_healpix_inds.h5", 

128 "ra": 60, 

129 "dec": -30, 

130 "freq": 182, 

131 }, 

132 { 

133 "name": "EoR1_low_healpix_inds.h5", 

134 "ra": 60, 

135 "dec": -30, 

136 "freq": 151, 

137 }, 

138 ] 

139 ) 

140 ang_dist = [] 

141 for file in files: 

142 ang_dist.append( 

143 np.abs( 

144 angle_difference( 

145 obs["obsra"], 

146 obs["obsdec"], 

147 file["ra"], 

148 file["dec"], 

149 degree=True, 

150 nearest=True, 

151 ) 

152 ) 

153 ) 

154 ang_dist = np.array(ang_dist) 

155 ang_use = np.min(ang_dist) 

156 i_use = np.where(np.abs(ang_dist - ang_use) <= 1) 

157 files = files[i_use] 

158 freq_dist = [] 

159 for file in files: 

160 freq_dist.append(file["freq"]) 

161 freq_dist = np.abs(np.array(freq_dist) - (obs["freq_center"] / 1e6)) 

162 min_i = np.argmin(freq_dist) 

163 pyfhd_config["healpix_inds"] = importlib_resources.files( 

164 "PyFHD.resources.healpix" 

165 ).joinpath(files[min_i]["name"]) 

166 hpx_inds = load(pyfhd_config["healpix_inds"], logger=logger) 

167 if type(hpx_inds) is dict: 167 ↛ 171line 167 didn't jump to line 171 because the condition on line 167 was always true

168 if "nside" in hpx_inds: 168 ↛ 170line 168 didn't jump to line 170 because the condition on line 168 was always true

169 nside = hpx_inds["nside"] 

170 hpx_inds = hpx_inds["hpx_inds"] 

171 if nside is None: 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true

172 pix_sky = ( 

173 4 * np.pi * ((180 / np.pi) ** 2) / np.prod(np.abs(obs["astr"]["cdelt"])) 

174 ) 

175 nside = 2 ** (np.ceil(np.log(np.sqrt(pix_sky / 12)) / np.log(2))) 

176 

177 # If you wish to implement the keyword divide_pixel_area implement it here and 

178 # add it as an option inside pyfhd_config 

179 

180 if hpx_inds is not None: 180 ↛ 186line 180 didn't jump to line 186 because the condition on line 180 was always true

181 pix_coords = np.vstack(pix2vec(int(nside), hpx_inds)).T 

182 pix_ra, pix_dec = vec2ang(pix_coords, lonlat=True) 

183 # This assume the refraction fix on FHD has been implemented 

184 xv_hpx, yv_hpx = radec_to_pixel(pix_ra, pix_dec, obs["astr"]) 

185 else: 

186 cen_coords = ang2vec(obs["obsra"], obs["obsdec"], lonlat=True) 

187 hpx_inds = query_disc(nside, cen_coords, hpx_radius) 

188 pix_coords = np.vstack(pix2vec(nside, hpx_inds)).T 

189 pix_dec, pix_ra = vec2ang(pix_coords, lonlat=True) 

190 xv_hpx, yv_hpx = radec_to_pixel(pix_ra, pix_dec, obs["astr"]) 

191 # slightly more restrictive boundary here ('LT' and 'GT' instead of 'LE' and 'GE') 

192 pix_i_use = np.where( 

193 (xv_hpx > 0) 

194 & (xv_hpx < obs["dimension"] - 1) 

195 & (yv_hpx > 0) 

196 & (yv_hpx < obs["elements"] - 1) 

197 ) 

198 xv_hpx = xv_hpx[pix_i_use] 

199 yv_hpx = yv_hpx[pix_i_use] 

200 if mask is not None: 

201 hpx_mask00 = mask[np.floor(xv_hpx), np.floor(yv_hpx)] 

202 hpx_mask01 = mask[np.floor(xv_hpx), np.ceil(yv_hpx)] 

203 hpx_mask10 = mask[np.ceil(xv_hpx), np.floor(yv_hpx)] 

204 hpx_mask11 = mask[np.ceil(xv_hpx), np.ceil(yv_hpx)] 

205 hpx_mask = hpx_mask00 * hpx_mask01 * hpx_mask10 * hpx_mask11 

206 pix_i_use2 = np.nonzero(hpx_mask) 

207 xv_hpx = xv_hpx[pix_i_use2] 

208 yv_hpx = yv_hpx[pix_i_use2] 

209 pix_i_use = pix_i_use[pix_i_use2] 

210 hpx_inds = hpx_inds[pix_i_use] 

211 

212 # Test for pixels past the horizon. We don't need to be precise with this, so turn off precession, etc.. 

213 # Get the location from instrument name 

214 location = EarthLocation.of_site(obs["instrument"]) 

215 alt, _ = radec_to_altaz( 

216 pix_ra, 

217 pix_dec, 

218 location.lat.value, 

219 location.lon.value, 

220 location.height.value, 

221 obs["jd0"], 

222 ) 

223 horizon_i = np.where(alt <= 0) 

224 if np.size(horizon_i) > 0: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 logger.info("Cutting the HEALPix pixels that were below the horizon") 

226 h_use = np.where(alt > 0) 

227 xv_hpx = xv_hpx[h_use] 

228 yv_hpx = yv_hpx[h_use] 

229 hpx_inds = hpx_inds[h_use] 

230 # The differences in precision through the use of vec2ang, radec_to_pixel are exposed 

231 # directly causing differences in the results. We can probably assume these numbers are the results. We can probably assume these numbers are 

232 # "better" compared to IDL 

233 x_frac = 1 - (xv_hpx - np.floor(xv_hpx)) 

234 y_frac = 1 - (yv_hpx - np.floor(yv_hpx)) 

235 

236 v_floor = np.floor(xv_hpx) + obs["dimension"] * np.floor(yv_hpx) 

237 v_ceil = np.ceil(xv_hpx) + obs["dimension"] * np.ceil(yv_hpx) 

238 # Differences in precision occur here compared to IDL, unsolvable ones as we're using 

239 # built in HEALPIX and astropy functions to get these arrays. We can probably assume these 

240 # numbers are "better" compared to IDL, as a result the v_floor and v_ceil arrays are one off 

241 # compared to IDL, making min_bin off by one 

242 min_bin = max(np.nanmin(v_floor), 0) 

243 max_bin = min(np.nanmax(v_ceil), obs["dimension"] * obs["elements"] - 1) 

244 h00, _, ri00 = histogram(v_floor, min=min_bin, max=max_bin) 

245 h01, _, ri01 = histogram( 

246 np.floor(xv_hpx) + obs["dimension"] * np.ceil(yv_hpx), min=min_bin, max=max_bin 

247 ) 

248 h10, _, ri10 = histogram( 

249 np.ceil(xv_hpx) + obs["dimension"] * np.floor(yv_hpx), min=min_bin, max=max_bin 

250 ) 

251 h11, _, ri11 = histogram(v_ceil, min=min_bin, max=max_bin) 

252 htot = h00 + h01 + h10 + h11 

253 inds = np.nonzero(htot)[0] 

254 

255 n_arr = htot[inds] 

256 i_use = (inds + min_bin).astype(np.int64) 

257 # To make sure it's an array we can store into a HDF5 file we're 

258 # creating object arrays full of Nones which will be populated by 

259 # NumPy arrays, creating a "variable length" array of arrays, to which 

260 # the save function in pyfhd_io can now handle through the variable_length 

261 # parameter 

262 sa = np.full(np.size(n_arr), None, dtype=object) 

263 ija = np.full(np.size(n_arr), None, dtype=object) 

264 

265 for i in range(np.size(n_arr)): 

266 ind0 = inds[i] 

267 sa0 = np.zeros(n_arr[i]) 

268 ija0 = np.zeros(n_arr[i], dtype=np.int64) 

269 hist_arr = np.array( 

270 [0, h00[ind0], h01[ind0], h10[ind0], h11[ind0]], dtype=np.int64 

271 ) 

272 bin_i = np.cumsum(hist_arr) 

273 if h00[ind0] > 0: 

274 bi = 0 

275 inds1 = ri00[ri00[ind0] : ri00[ind0 + 1]] 

276 sa0[bin_i[bi] : bin_i[bi + 1]] = x_frac[inds1] * y_frac[inds1] 

277 ija0[bin_i[bi] : bin_i[bi + 1]] = inds1 

278 if h01[ind0] > 0: 

279 bi = 1 

280 inds1 = ri01[ri01[ind0] : ri01[ind0 + 1]] 

281 sa0[bin_i[bi] : bin_i[bi + 1]] = x_frac[inds1] * (1 - y_frac[inds1]) 

282 ija0[bin_i[bi] : bin_i[bi + 1]] = inds1 

283 if h10[ind0] > 0: 

284 bi = 2 

285 inds1 = ri10[ri10[ind0] : ri10[ind0 + 1]] 

286 sa0[bin_i[bi] : bin_i[bi + 1]] = (1 - x_frac[inds1]) * y_frac[inds1] 

287 ija0[bin_i[bi] : bin_i[bi + 1]] = inds1 

288 if h11[ind0] > 0: 

289 bi = 3 

290 inds1 = ri11[ri11[ind0] : ri11[ind0 + 1]] 

291 sa0[bin_i[bi] : bin_i[bi + 1]] = (1 - x_frac[inds1]) * (1 - y_frac[inds1]) 

292 ija0[bin_i[bi] : bin_i[bi + 1]] = inds1 

293 # Since we didn't translate pixel_area we can ignore the if statement coverring 

294 # the case where pixel_area is greater than 1 

295 sa[i] = sa0 

296 ija[i] = ija0 

297 

298 hpx_cnv = {"nside": nside, "ija": ija, "sa": sa, "i_use": i_use, "inds": hpx_inds} 

299 obs["healpix"]["nside"] = nside 

300 if pyfhd_config["restrict_healpix_inds"]: 300 ↛ 303line 300 didn't jump to line 303 because the condition on line 300 was always true

301 obs["healpix"]["ind_list"] = None 

302 else: 

303 obs["healpix"]["ind_list"] = hpx_inds 

304 obs["healpix"]["n_pix"] = np.size(hpx_inds) 

305 mask_test = healpix_cnv_apply(mask, hpx_cnv) 

306 mask_test_i0 = np.where(mask_test == 0) 

307 obs["healpix"]["n_zero"] = np.size(mask_test_i0[0]) 

308 

309 return hpx_cnv, obs 

310 

311 

312def beam_image_cube( 

313 obs: dict, 

314 psf: dict | h5py.File, 

315 logger: Logger, 

316 freq_i_arr: NDArray[np.integer] | None = None, 

317 pol_i_arr: NDArray[np.integer] | None = None, 

318 n_freq_bin: int | None = None, 

319 square: bool = True, 

320 beam_threshold: float | None = None, 

321) -> tuple[NDArray[np.complex128], NDArray[np.float64]]: 

322 """ 

323 Calculate the beam image per unflagged frequency channel to build a cube. 

324 Build a contiguous mask in x and y based off of a threshold value. 

325 

326 Parameters 

327 ---------- 

328 obs : dict 

329 Observation metadata dictionary 

330 psf : dict | h5py.File 

331 Beam dictionary 

332 logger : Logger 

333 PyFHD's Logger 

334 freq_i_arr : np.ndarray | None, optional 

335 Index array of the beam frequency binning in the observation 

336 metadata dictionary, by default None 

337 pol_i_arr : np.ndarray | None, optional 

338 Index array of the polarizations in the observation metadata 

339 dictionary, by default None 

340 n_freq_bin : int | None, optional 

341 Number of frequency channels to divide the full observation bandwidth 

342 into for the beam image cube, by default None 

343 square : bool, optional 

344 Square the beam image, by default True 

345 beam_threshold : float | None, optional 

346 Fractional threshold in which to build a mask for all smaller 

347 beam values, by default None 

348 

349 Returns 

350 ------- 

351 tuple[NDArray[np.complex128], NDArray[np.float64]] 

352 Beam image frequency cube and mask threshold array. 

353 """ 

354 

355 if beam_threshold is None: 355 ↛ 356line 355 didn't jump to line 356 because the condition on line 355 was never true

356 beam_threshold = 0.05 

357 

358 if pol_i_arr is None: 358 ↛ 361line 358 didn't jump to line 361 because the condition on line 358 was always true

359 pol_i_arr = np.arange(obs["n_pol"]) 

360 

361 if n_freq_bin is not None: 361 ↛ 365line 361 didn't jump to line 365 because the condition on line 361 was always true

362 freq_i_arr = np.floor( 

363 np.arange(n_freq_bin) * (obs["n_freq"] / n_freq_bin) 

364 ).astype(np.int64) 

365 if freq_i_arr is None: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true

366 logger.error("beam_image_cube requires n_freq_bin or freq_i_arr to be set") 

367 sys.exit(1) 

368 if n_freq_bin is None and freq_i_arr is not None: 368 ↛ 369line 368 didn't jump to line 369 because the condition on line 368 was never true

369 n_freq_bin = freq_i_arr.size 

370 

371 if np.median(freq_i_arr) > obs["n_freq"]: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true

372 freq_i_use = np.interp( 

373 np.arange(obs["n_freq"]), obs["baseline_info"]["freq"], freq_i_arr 

374 ) 

375 else: 

376 freq_i_use = freq_i_arr 

377 

378 beam_arr = np.zeros([obs["n_pol"], n_freq_bin, obs["dimension"], obs["elements"]]) 

379 

380 bin_arr = obs["baseline_info"]["fbin_i"][freq_i_use] 

381 bin_hist, _, bri = histogram(bin_arr, min=0) 

382 bin_use = np.nonzero(bin_hist)[0] 

383 if np.size(bin_use) == 0: 383 ↛ 384line 383 didn't jump to line 384 because the condition on line 383 was never true

384 return beam_arr 

385 bin_n = bin_hist[bin_use] 

386 beam_mask = np.ones([obs["dimension"], obs["elements"]]) 

387 for pol_i in range(obs["n_pol"]): 

388 for fb_i in range(np.size(bin_use)): 

389 f_i_i = bri[bri[bin_use[fb_i]] : bri[bin_use[fb_i] + 1]] 

390 f_i = freq_i_use[f_i_i[0]] 

391 beam_single = beam_image(psf, obs, pol_i, freq_i=f_i, square=square) 

392 beam_arr[pol_i, f_i_i[0] : f_i_i[bin_n[fb_i] - 1] + 1] = beam_single 

393 b_i = int(obs["obsx"] + obs["obsy"] * obs["dimension"]) 

394 beam_i = region_grow( 

395 beam_single, 

396 b_i, 

397 low=beam_threshold ** (square + 1), 

398 high=np.max(beam_single), 

399 ) 

400 beam_mask1 = np.zeros([obs["dimension"], obs["elements"]]) 

401 beam_mask1.flat[beam_i] = 1 

402 beam_mask *= beam_mask1 

403 return beam_arr, beam_mask 

404 

405 

406def phase_shift_uv_image(obs: dict) -> NDArray[np.complex128]: 

407 """ 

408 Phase shift the entire u-v plane to the original phase center. 

409 

410 Parameters 

411 ---------- 

412 obs : dict 

413 Observation metadata dictionary 

414 

415 Returns 

416 ------- 

417 NDArray[np.float64] 

418 A 2D u-v phase shift array that will shift the current phase centre to 

419 the original phase centre. 

420 """ 

421 # Since we only use it once in PyFHD, assume we always want to do /to_orig_phase 

422 # Implement the other options if you decide to use this function elsewhere 

423 ra_use = obs["orig_phasera"] 

424 dec_use = obs["orig_phasedec"] 

425 

426 # Skip calculations if phased correctly 

427 if ( 427 ↛ 431line 427 didn't jump to line 431 because the condition on line 427 was never true

428 obs["phasera"] == obs["orig_phasera"] 

429 and obs["phasedec"] == obs["orig_phasedec"] 

430 ): 

431 return np.ones([obs["dimension"], obs["elements"]], dtype=np.complex128) 

432 

433 x, y = radec_to_pixel(ra_use, dec_use, obs["astr"]) 

434 

435 # uv_mask is not applied in FHD examples or docs decided not to translate it, if you want it put it here 

436 

437 dx = (x - (obs["dimension"] / 2)) * (2 * np.pi / obs["dimension"]) 

438 dy = (y - (obs["elements"] / 2)) * (2 * np.pi / obs["dimension"]) 

439 

440 xvals = meshgrid(obs["dimension"], obs["elements"], 1) - (obs["dimension"] / 2) 

441 yvals = meshgrid(obs["dimension"], obs["elements"], 2) - (obs["elements"] / 2) 

442 

443 phase = xvals * dx + yvals * dy 

444 rephase_vals = np.cos(phase) + np.sin(phase) * 1j 

445 

446 # Again no uv_mask therefore just return the rephase as is 

447 

448 return rephase_vals 

449 

450 

451def vis_model_freq_split( 

452 obs: dict, 

453 psf: dict | h5py.File, 

454 params: dict, 

455 vis_weights: NDArray[np.float64], 

456 vis_model_arr: NDArray[np.complex128], 

457 vis_arr: NDArray[np.complex128], 

458 polarization: int, 

459 pyfhd_config: dict, 

460 logger: Logger, 

461 fft: bool = True, 

462 save_uvf: bool = True, 

463 uvf_name: str = "", 

464 bi_use: NDArray[np.integer] = None, 

465) -> dict: 

466 """ 

467 Grid as a function of frequency, which can be split into larger channels. This 

468 uvf cube can be saved as is or used to create an orthoslant image as a function 

469 of frequency. Optional bi_use argument allows for the separate gridding of even/odd 

470 interleaved time samples for error propagation in the power spectrum. 

471 

472 Parameters 

473 ---------- 

474 obs : dict 

475 Observation metadata dictionary 

476 psf : dict | h5py.File 

477 Beam dictionary 

478 params : dict 

479 Visibility metadata dictionary 

480 vis_weights : NDArray[np.float64] 

481 Visibility weights array 

482 vis_model_arr : NDArray[np.complex128] 

483 Model visibility array 

484 vis_arr : NDArray[np.complex128] 

485 Calibrated visibility array 

486 polarization : int 

487 The polarization index 

488 pyfhd_config : dict 

489 PyFHD configuration settings 

490 logger : Logger 

491 PyFHD's Logger 

492 fft : bool, optional 

493 Calculate the orthoslant image instead of the u-v plane, 

494 by default True 

495 save_uvf : bool, optional 

496 Save the uvf cube, by default True 

497 uvf_name : str, optional 

498 The name after the obsid in the uvf cube filename, by default '' 

499 bi_use : NDArray[np.integer], optional 

500 The visibility indices to use in gridding, i.e. even/odd 

501 interleaved time samples, by default None 

502 

503 Returns 

504 ------- 

505 cube_split: dict 

506 Dictionary of the gridded u-v planes or orthoslant images as a function 

507 of frequency for the calibrated data, model (if present), residual data 

508 (if present), sampling map, and variance map, along with the observation 

509 metadata dictionary. 

510 """ 

511 

512 freq_bin_i2 = np.arange(obs["n_freq"]) // pyfhd_config["n_avg"] 

513 nf = int(np.max(freq_bin_i2) + 1) 

514 if save_uvf: 

515 dirty_uv_arr = np.zeros( 

516 [nf, obs["dimension"], obs["dimension"]], dtype=np.complex128 

517 ) 

518 weights_uv_arr = np.zeros( 

519 [nf, obs["dimension"], obs["dimension"]], dtype=np.complex128 

520 ) 

521 variance_uv_arr = np.zeros( 

522 [nf, obs["dimension"], obs["dimension"]], dtype=np.complex128 

523 ) 

524 model_uv_arr = np.zeros( 

525 [nf, obs["dimension"], obs["dimension"]], dtype=np.complex128 

526 ) 

527 dirty_arr = np.zeros([nf, obs["dimension"], obs["dimension"]]) 

528 weights_arr = np.zeros([nf, obs["dimension"], obs["dimension"]]) 

529 variance_arr = np.zeros([nf, obs["dimension"], obs["dimension"]]) 

530 model_arr = np.zeros([nf, obs["dimension"], obs["dimension"]]) 

531 vis_n_arr = np.zeros([nf]) 

532 if pyfhd_config["rephase_weights"]: 

533 rephase_use = phase_shift_uv_image(obs) 

534 else: 

535 rephase_use = 1 

536 # No x_range and y_range is used in PyFHD, if you wish to do that add that here 

537 

538 if bi_use is None: 

539 if obs["n_pol"] > 1: 

540 flag_test = np.maximum(np.maximum(vis_weights[0], vis_weights[1]), 0) 

541 # Double check the axis used 

542 flag_test = np.sum(flag_test, axis=1) 

543 bi_use = np.where(flag_test > 0)[0] 

544 else: 

545 flag_test = np.maximum(vis_weights[0], 0) 

546 flag_test = np.sum(flag_test, axis=1) 

547 bi_use = np.where(flag_test > 0)[0] 

548 

549 n_vis_use = 0 

550 for fi in range(nf): 

551 fi_use = np.where((freq_bin_i2 == fi) & (obs["baseline_info"]["freq_use"] > 0))[ 

552 0 

553 ] 

554 if np.size(fi_use) == 0: 

555 continue 

556 gridding_dict = visibility_grid( 

557 vis_arr[polarization], 

558 vis_weights[polarization], 

559 obs, 

560 psf, 

561 params, 

562 polarization, 

563 pyfhd_config, 

564 logger, 

565 model=vis_model_arr[polarization], 

566 fi_use=fi_use, 

567 bi_use=bi_use, 

568 verbose_logging=False, 

569 ) 

570 if not gridding_dict: 

571 logger.warning( 

572 f"No visibilities gridded for frequency channel {fi_use} and polarization {obs['pol_names'][polarization]} ({polarization})" 

573 ) 

574 continue 

575 n_vis_use += gridding_dict["n_vis"] 

576 vis_n_arr[fi] = gridding_dict["n_vis"] 

577 

578 if save_uvf: 

579 dirty_uv_arr[fi] = gridding_dict["image_uv"] * gridding_dict["n_vis"] 

580 weights_uv_arr[fi] = ( 

581 gridding_dict["weights"] * rephase_use * gridding_dict["n_vis"] 

582 ) 

583 variance_uv_arr[fi] = ( 

584 gridding_dict["variance"] * rephase_use * gridding_dict["n_vis"] 

585 ) 

586 model_uv_arr[fi] = gridding_dict["model_return"] * gridding_dict["n_vis"] 

587 

588 if fft: 

589 # No x_range and y_range hence no check for it here 

590 dirty_arr[fi], _, _ = dirty_image_generate( 

591 gridding_dict["image_uv"], 

592 pyfhd_config, 

593 logger, 

594 degpix=obs["degpix"], 

595 ) 

596 dirty_arr[fi] *= gridding_dict["n_vis"] 

597 weights_arr[fi], _, _ = dirty_image_generate( 

598 gridding_dict["weights"] * rephase_use, 

599 pyfhd_config, 

600 logger, 

601 degpix=obs["degpix"], 

602 ) 

603 weights_arr[fi] *= gridding_dict["n_vis"] 

604 variance_arr[fi], _, _ = dirty_image_generate( 

605 gridding_dict["variance"] * rephase_use, 

606 pyfhd_config, 

607 logger, 

608 degpix=obs["degpix"], 

609 ) 

610 variance_arr[fi] *= gridding_dict["n_vis"] 

611 model_arr[fi], _, _ = dirty_image_generate( 

612 gridding_dict["model_return"], 

613 pyfhd_config, 

614 logger, 

615 degpix=obs["degpix"], 

616 ) 

617 model_arr[fi] *= gridding_dict["n_vis"] 

618 else: 

619 dirty_arr[fi] = gridding_dict["image_uv"] * gridding_dict["n_vis"] 

620 weights_arr[fi] = ( 

621 gridding_dict["weights"] * rephase_use * gridding_dict["n_vis"] 

622 ) 

623 variance_arr[fi] = ( 

624 gridding_dict["variance"] * rephase_use * gridding_dict["n_vis"] 

625 ) 

626 model_arr[fi] = gridding_dict["model_return"] * gridding_dict["n_vis"] 

627 obs["n_vis"] = n_vis_use 

628 

629 if save_uvf: 

630 h5_save_dict = { 

631 "dirty_uv": dirty_uv_arr, 

632 "weights_uv": weights_uv_arr, 

633 "variance_uv": variance_uv_arr, 

634 "model_uv": model_uv_arr, 

635 } 

636 uvf_dir = Path(Path(pyfhd_config["output_dir"], "healpix", "uvf_grid")) 

637 uvf_dir.mkdir(exist_ok=True, parents=True) 

638 save( 

639 Path( 

640 uvf_dir, 

641 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_dirty_uv_arr_gridded_uvf.h5', 

642 ), 

643 h5_save_dict, 

644 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_dirty_uv_arr_gridded_uvf.h5', 

645 logger=logger, 

646 ) 

647 save( 

648 Path( 

649 uvf_dir, 

650 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_weights_uv_gridded_uvf.h5', 

651 ), 

652 h5_save_dict, 

653 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_weights_uv_gridded_uvf.h5', 

654 logger=logger, 

655 ) 

656 save( 

657 Path( 

658 uvf_dir, 

659 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_variance_uv_arr_gridded_uvf.h5', 

660 ), 

661 h5_save_dict, 

662 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_variance_uv_arr_gridded_uvf.h5', 

663 logger=logger, 

664 ) 

665 save( 

666 Path( 

667 uvf_dir, 

668 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_model_uv_arr_gridded_uvf.h5', 

669 ), 

670 h5_save_dict, 

671 f'{pyfhd_config["obs_id"]}_{uvf_name}_{obs["pol_names"][polarization]}_model_uv_arr_gridded_uvf.h5', 

672 logger=logger, 

673 ) 

674 

675 cube_split = { 

676 "obs": obs, 

677 "residual_arr": dirty_arr, 

678 "weights_arr": weights_arr, 

679 "variance_arr": variance_arr, 

680 "model_arr": model_arr, 

681 } 

682 

683 return cube_split