Coverage for PyFHD/data_setup/obs.py: 72%

268 statements  

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

1import numpy as np 

2import logging 

3from math import pi, log10 

4from PyFHD.pyfhd_tools.pyfhd_utils import ( 

5 idl_argunique, 

6 histogram, 

7 angle_difference, 

8 parallactic_angle, 

9) 

10from PyFHD.pyfhd_tools.unit_conv import altaz_to_radec, radec_to_pixel, radec_to_altaz 

11from pathlib import Path 

12from astropy.io import fits 

13from astropy.table import Table 

14from astropy.time import Time 

15 

16 

17def create_obs( 

18 pyfhd_header: dict, 

19 params: dict, 

20 layout: dict, 

21 pyfhd_config: dict, 

22 logger: logging.Logger, 

23) -> dict: 

24 """ 

25 create_obs takes all the data that has been read in and creates the obs data structure which holds data 

26 and metadata of the observation we're doing a PyFHD run on. Inside this function the metafits file will 

27 be read as well. 

28 

29 Parameters 

30 ---------- 

31 pyfhd_header : dict 

32 The data from the UVFITS header 

33 params : dict 

34 The data from the UVFITS file 

35 layout : dict 

36 The data dictionary containing data and metadata about the antennas 

37 pyfhd_config : dict 

38 PyFHD's configuration dictionary 

39 logger : logging.Logger 

40 The PyFHD logger 

41 

42 Returns 

43 ------- 

44 obs : dict 

45 The observatiopn data structure for PyFHD containing data from the config and metadata from the observation UVFITS and METAFITS files. 

46 """ 

47 

48 obs = {} 

49 baseline_info = {} 

50 

51 # Save the data from the header 

52 obs["n_pol"] = ( 

53 pyfhd_config["n_pol"] if pyfhd_config["n_pol"] else pyfhd_header["n_pol"] 

54 ) 

55 obs["n_tile"] = pyfhd_header["n_tile"] 

56 obs["n_freq"] = pyfhd_header["n_freq"] 

57 obs["n_freq_flag"] = 0 

58 obs["instrument"] = pyfhd_config["instrument"] 

59 obs["obsname"] = pyfhd_config["obs_id"] 

60 # Deal with the times 

61 time = params["time"] 

62 b0i = idl_argunique(time) 

63 obs["n_time"] = b0i.size 

64 bin_width = np.empty(obs["n_time"]) 

65 if obs["n_time"] > 1: 65 ↛ 68line 65 didn't jump to line 68 because the condition on line 65 was always true

66 bin_width[0 : obs["n_time"]] = b0i + 1 

67 else: 

68 bin_width = time.size 

69 b0i_range = np.arange(1, obs["n_time"]) 

70 bin_width[b0i_range] = b0i[b0i_range] - b0i[b0i_range - 1] 

71 baseline_info["bin_offset"] = np.zeros(obs["n_time"], dtype=np.int64) 

72 if obs["n_time"] > 1: 72 ↛ 75line 72 didn't jump to line 75 because the condition on line 72 was always true

73 baseline_info["bin_offset"][1:] = np.cumsum(bin_width[: obs["n_time"] - 1]) 

74 # Deal with the number of visibilities 

75 obs["n_baselines"] = int(bin_width[0]) 

76 obs["n_vis"] = time.size * obs["n_freq"] 

77 obs["n_vis_raw"] = obs["n_vis_in"] = obs["n_vis"] 

78 obs["nf_vis"] = np.zeros(obs["n_freq"], dtype=np.int64) 

79 

80 obs["freq_res"] = pyfhd_header["freq_res"] 

81 baseline_info["freq"] = pyfhd_header["frequency_array"] 

82 if pyfhd_config["beam_nfreq_avg"] is not None: 82 ↛ 85line 82 didn't jump to line 85 because the condition on line 82 was always true

83 obs["beam_nfreq_avg"] = pyfhd_config["beam_nfreq_avg"] 

84 else: 

85 obs["beam_nfreq_avg"] = 1 

86 freq_bin = obs["beam_nfreq_avg"] * obs["freq_res"] 

87 # Let's get the freq_center and the bins we want to use 

88 freq_hist, _, freq_ri = histogram(baseline_info["freq"], bin_size=freq_bin) 

89 freq_bin_i = np.zeros(obs["n_freq"]) 

90 for bin in range(freq_hist.size): 

91 if freq_ri[bin] < freq_ri[bin + 1]: 91 ↛ 90line 91 didn't jump to line 90 because the condition on line 91 was always true

92 freq_bin_i[freq_ri[freq_ri[bin] : freq_ri[bin + 1]]] = bin 

93 baseline_info["fbin_i"] = freq_bin_i.astype(np.int64) 

94 obs["freq_center"] = np.median(baseline_info["freq"]) 

95 

96 antenna_flag = True 

97 if np.max(params["antenna1"]) > 0 and (np.max(params["antenna2"]) > 0): 97 ↛ 101line 97 didn't jump to line 101 because the condition on line 97 was always true

98 baseline_info["tile_a"] = params["antenna1"] 

99 baseline_info["tile_b"] = params["antenna2"] 

100 antenna_flag = False 

101 if antenna_flag: 101 ↛ 104line 101 didn't jump to line 104 because the condition on line 101 was never true

102 # 256 tile upper limit is hard-coded in CASA format 

103 # these tile numbers have been verified to be correct 

104 baseline_min = np.min(params["baseline_arr"]) 

105 exponent = np.log(np.min(baseline_min)) / np.log(2) 

106 antenna_mod_index = 2 ** np.floor(exponent) 

107 tile_B_test = baseline_min % antenna_mod_index 

108 # Check if a bad fit and if autocorrelations or the first tile are missing 

109 if (tile_B_test > 1) and (baseline_min % 2 == 1): 

110 antenna_mod_index /= 2 ** np.floor(np.log(np.min(tile_B_test)) / np.log(2)) 

111 baseline_info["tile_a"] = np.floor(params["baseline_arr"] / antenna_mod_index) 

112 baseline_info["tile_b"] = np.fix(params["baseline_arr"] / antenna_mod_index) 

113 if ( 

114 max(np.max(baseline_info["tile_a"]), np.max(baseline_info["tile_b"])) 

115 != obs["n_tile"] 

116 ): 

117 logger.warning( 

118 f"Mis-matched n_tiles Header: {obs['n_tile']}, Data: {max(np.max(baseline_info['tile_a']), np.max(baseline_info['tile_b']))}, adjusting n_tiles to be same as data" 

119 ) 

120 obs["n_tile"] = max( 

121 np.max(baseline_info["tile_a"]), np.max(baseline_info["tile_b"]) 

122 ) 

123 params["antenna1"] = baseline_info["tile_a"] 

124 params["antenna2"] = baseline_info["tile_b"] 

125 

126 # check that all elements in the antenna1 and antenna2 array exist in the antenna numbers 

127 # from the uvfits antenna table 

128 all_ants = np.hstack([params["antenna1"], params["antenna2"]]) 

129 all_ants = np.unique(all_ants) 

130 if not (np.all(np.isin(all_ants, layout["antenna_numbers"]))): 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 logger.warning("Antenna arrays contain number(s) not found in antenna table") 

132 

133 # fhd expects antenna1 and antenna2 arrays containing indices that are one-indexed. 

134 # Some uvfits files contain actual antenna numbers in these fields, while others 

135 # (particularly, those written by cotter or birli) contain indices. 

136 # To account for this, all antenna numbers from the uvfits header are mapped to indices 

137 # using the antenna numbers from the uvfits antenna table. 

138 # If the antenna numbers were written into the file as indices, they will be mapped to themselves. 

139 for tile_i in range(obs["n_tile"]): 

140 tile_a_antennas = np.where( 

141 layout["antenna_numbers"][tile_i] == params["antenna1"] 

142 ) 

143 if np.size(tile_a_antennas) > 0: 

144 baseline_info["tile_a"][tile_a_antennas] = tile_i + 1 

145 tile_b_antennas = np.where( 

146 layout["antenna_numbers"][tile_i] == params["antenna2"] 

147 ) 

148 if np.size(tile_b_antennas) > 0: 

149 baseline_info["tile_b"][tile_b_antennas] = tile_i + 1 

150 # Change the type to int to avoid issues with numba 

151 baseline_info["tile_a"] = baseline_info["tile_a"].astype(np.int64) 

152 baseline_info["tile_b"] = baseline_info["tile_b"].astype(np.int64) 

153 params["antenna1"] = baseline_info["tile_a"] 

154 params["antenna2"] = baseline_info["tile_b"] 

155 

156 baseline_info["freq_use"] = np.ones(obs["n_freq"], dtype=np.int64) 

157 

158 # Calculate kx and ky for each baseline at high precision to get most accurate observation information 

159 kx_arr = np.outer(baseline_info["freq"], params["uu"]) 

160 ky_arr = np.outer(baseline_info["freq"], params["vv"]) 

161 kr_arr = np.sqrt(kx_arr**2 + ky_arr**2) 

162 max_baseline = max(np.max(np.abs(kx_arr)), np.max(np.abs(ky_arr))) 

163 

164 # Determine the imaging parameters to use 

165 if pyfhd_config["FoV"] is not None: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true

166 obs["kpix"] = (180 / pi) / pyfhd_config["FoV"] 

167 if pyfhd_config["kbinsize"] is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 obs["kpix"] = 0.5 

169 else: 

170 obs["kpix"] = pyfhd_config["kbinsize"] 

171 

172 # Determine observation resolution/extent parameters given number of pixels in x direction (dimension) 

173 if pyfhd_config["dimension"] is None and pyfhd_config["elements"] is None: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 obs["dimension"] = 2 ** int( 

175 (log10((2 * max_baseline) / pyfhd_config["kpix"]) / log10(2)) 

176 ) 

177 obs["elements"] = obs["dimension"] 

178 elif pyfhd_config["dimension"] is not None and pyfhd_config["elements"] is None: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 obs["dimension"] = pyfhd_config["dimension"] 

180 obs["elements"] = pyfhd_config["dimension"] 

181 elif pyfhd_config["dimension"] is None and pyfhd_config["elements"] is not None: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true

182 obs["dimension"] = pyfhd_config["elements"] 

183 obs["elements"] = pyfhd_config["elements"] 

184 else: 

185 obs["dimension"] = pyfhd_config["dimension"] 

186 obs["elements"] = pyfhd_config["elements"] 

187 # Ensure both dimension and elements are ints to prevent issues down the pipeline 

188 obs["dimension"] = int(obs["dimension"]) 

189 obs["elements"] = int(obs["elements"]) 

190 obs["degpix"] = (180 / pi) / (obs["kpix"] * pyfhd_config["dimension"]) 

191 

192 # Set the max and min baseline 

193 max_baseline_inds = np.where( 

194 (np.abs(kx_arr) / obs["kpix"] < obs["dimension"] / 2) 

195 & (np.abs(ky_arr) / obs["kpix"] < obs["elements"] / 2) 

196 ) 

197 obs["max_baseline"] = np.max(np.abs(kr_arr[max_baseline_inds])) 

198 if pyfhd_config["min_baseline"] is None: 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true

199 obs["min_baseline"] = np.min(kr_arr[np.nonzero(kr_arr)]) 

200 else: 

201 obs["min_baseline"] = max( 

202 pyfhd_config["min_baseline"], np.min(kr_arr[np.nonzero(kr_arr)]) 

203 ) 

204 

205 meta = read_metafits(obs, pyfhd_header, params, pyfhd_config, logger) 

206 

207 baseline_info["time_use"] = np.ones(obs["n_time"], dtype=np.int8) 

208 # time cut is specified in seconds to cut (rounded up to next time integration point). 

209 # Specify negative time_cut to cut time off the end. Specify a vector to cut at both the start and end 

210 if pyfhd_config["time_cut"] is not None: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true

211 time_cut = pyfhd_config["time_cut"] 

212 for ti in time_cut: 

213 if time_cut[ti] < 0: 

214 ti_start = min( 

215 obs["n_time"] 

216 - max(np.ceil(np.abs(time_cut[ti]) / meta["time_res"]), 0), 

217 obs["n_time"] - 1, 

218 ) 

219 ti_end = obs["n_time"] - 1 

220 else: 

221 ti_start = 0 

222 ti_end = min( 

223 np.ceil(np.abs(time_cut[ti])) / meta["time_res"] - 1, 

224 obs["n_time"] - 1, 

225 ) 

226 if ti_end >= ti_start: 

227 baseline_info["time_use"][ti_start : ti_end + 1] = 0 

228 obs["n_time_flag"] = obs["n_time"] - np.sum(baseline_info["time_use"]) 

229 

230 # Flag tiles based on meta data 

231 baseline_info["tile_use"] = 1 - meta["tile_flag"] 

232 obs["n_tile_flag"] = np.count_nonzero(baseline_info["tile_use"] == 0) 

233 

234 # Set the last of obs values 

235 if pyfhd_config["dft_threshold"]: 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true

236 obs["dft_threshold"] = 1 / (2 * pi) ** 2 * obs["dimension"] 

237 else: 

238 obs["dft_threshold"] = 0 

239 obs["degrid_spectral_terms"] = 0 

240 obs["grid_spectral_terms"] = 0 

241 obs["alpha"] = -0.8 

242 obs["pol_names"] = ["XX", "YY", "XY", "YX", "I", "Q", "U", "V"] 

243 obs["residual"] = False 

244 

245 # Setup healpix structure for obs 

246 healpix = {} 

247 healpix["nside"] = 0 

248 healpix["n_pix"] = 0 

249 # May be none! 

250 healpix["ind_list"] = pyfhd_config["healpix_inds"] 

251 healpix["n_zero"] = -1 

252 obs["healpix"] = healpix 

253 

254 # Save the baseline_info into obs 

255 baseline_info["jdate"] = meta["jdate"] 

256 baseline_info["tile_names"] = meta["tile_names"] 

257 baseline_info["tile_height"] = meta["tile_height"] 

258 baseline_info["tile_flag"] = meta["tile_flag"] 

259 obs["baseline_info"] = baseline_info 

260 

261 # Save the last of the metadata into obs 

262 for key in meta.keys(): 

263 if key not in baseline_info.keys(): 

264 obs[key] = meta[key] 

265 

266 return obs 

267 

268 

269def read_metafits( 

270 obs: dict, 

271 pyfhd_header: dict, 

272 params: dict, 

273 pyfhd_config: dict, 

274 logger: logging.Logger, 

275) -> dict: 

276 """ 

277 Reads the metafits file provided inside the same input directory as the UVFITS file. 

278 It will process the data found in the METAFITS file and then returns a meta dictionary. 

279 Which will eventually end up inside the obs dictionary. 

280 

281 Parameters 

282 ---------- 

283 obs : dict 

284 The current obs structure without the metadata 

285 pyfhd_header : dict 

286 The data from the UVFITS header 

287 params : dict 

288 The data from the UVFITS file 

289 pyfhd_config : dict 

290 PyFHD's configuration dictionary 

291 logger : logging.Logger 

292 PyFHD's logger 

293 

294 Returns 

295 ------- 

296 meta : dict 

297 The dictionary holding the metadata from the UVFITS and METAFITS files 

298 """ 

299 

300 meta = {} 

301 time = params["time"] 

302 b0i = idl_argunique(time) 

303 meta["jdate"] = time[ 

304 b0i 

305 ] # Time is already in julian. No need to add the bzero (or pzero) value 

306 meta["obsx"] = obs["dimension"] // 2 

307 meta["obsy"] = obs["elements"] // 2 

308 meta["jd0"] = np.min(meta["jdate"]) 

309 meta["epoch"] = Time(meta["jd0"], format="jd").to_value("decimalyear") 

310 meta_path = Path(pyfhd_config["input_path"], pyfhd_config["obs_id"] + ".metafits") 

311 if meta_path.is_file(): 311 ↛ 340line 311 didn't jump to line 340 because the condition on line 311 was always true

312 metadata = fits.open(meta_path) 

313 hdr = metadata[0].header 

314 data = metadata[1].data 

315 # Sort the data by antenna using a stable sort, astropy Table is required to access Antenna column for sorting 

316 # Standard Astropy does not do stable sorting, hence use of argsort to do stable sorting 

317 data = data[np.array(Table(data).argsort("Antenna", kind="stable"))] 

318 single_i = np.where(data["pol"] == data["pol"][0]) 

319 meta["tile_names"] = data["tile"][single_i] 

320 meta["tile_height"] = data["height"][single_i] - pyfhd_header["alt"] 

321 meta["tile_flag"] = data["flag"][single_i] 

322 if np.sum(meta["tile_flag"]) == meta["tile_flag"].size - 1: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true

323 if pyfhd_config["run_simulation"]: 

324 logger.warning("All tiles flagged in metadata") 

325 else: 

326 logger.error("All tiles flagged in metadata") 

327 exit() 

328 meta["obsra"] = hdr["RA"] 

329 meta["obsdec"] = hdr["DEC"] 

330 meta["phasera"] = hdr["RAPHASE"] 

331 meta["phasedec"] = hdr["DECPHASE"] 

332 meta["time_res"] = hdr["INTTIME"] 

333 delays = hdr["DELAYS"].split(",") 

334 meta["delays"] = ( 

335 np.asarray(delays, np.int64) 

336 .repeat(obs["n_pol"]) 

337 .reshape((obs["n_pol"], len(delays))) 

338 ) 

339 else: 

340 logger.warning( 

341 "METAFITS file has not been found, Calculating obs meta settings from the uvfits header instead" 

342 ) 

343 hdr = None 

344 # Simulate the flagging of tiles by taking where tiles don't exist 

345 tile_A1 = params["antenna1"] 

346 tile_B1 = params["antenna2"] 

347 hist_A1, _, _ = histogram(tile_A1, min=1, max=obs["n_tile"]) 

348 hist_B1, _, _ = histogram(tile_B1, min=1, max=obs["n_tile"]) 

349 hist_AB = hist_A1 + hist_B1 

350 meta["tile_names"] = np.arange(1, obs["n_tile"] + 1) 

351 meta["tile_height"] = np.zeros(obs["n_tile"]) 

352 tile_use = np.where(hist_AB == 0)[0] 

353 meta["tile_flag"] = np.zeros(obs["n_tile"], dtype=np.int8) 

354 if tile_use.size > 0: 

355 meta["tile_flag"][tile_use] = 1 

356 if b0i.size > 1: 

357 meta["time_res"] = (time[b0i[1]] - time[b0i[0]]) * 24.0 * 3600.0 

358 else: 

359 meta["time_res"] = 1 

360 meta["obsra"] = pyfhd_header["obsra"] 

361 meta["obsdec"] = pyfhd_header["obsdec"] 

362 meta["phasera"] = pyfhd_header["obsra"] 

363 meta["phasedec"] = pyfhd_header["obsdec"] 

364 meta["delays"] = None 

365 

366 # Store an origin/target phase ra/dec 

367 meta["orig_phasera"] = ( 

368 pyfhd_config["override_target_phasera"] 

369 if pyfhd_config["override_target_phasera"] is not None 

370 else meta["phasera"] 

371 ) 

372 meta["orig_phasedec"] = ( 

373 pyfhd_config["override_target_phasedec"] 

374 if pyfhd_config["override_target_phasedec"] is not None 

375 else meta["phasedec"] 

376 ) 

377 

378 # Get the Zenith RA and DEC from the location and time 

379 zenra, zendec = altaz_to_radec( 

380 90, 

381 0, 

382 pyfhd_header["lat"], 

383 pyfhd_header["lon"], 

384 pyfhd_header["alt"], 

385 meta["jd0"], 

386 ) 

387 meta["zenra"] = zenra 

388 meta["zendec"] = zendec 

389 

390 # Project Slant Orthographic 

391 # astr is basically a WCS data structure. 

392 # zenx and zeny are the Zenith pixel coordinates 

393 meta["astr"], meta["zenx"], meta["zeny"] = project_slant_orthographic(meta, obs) 

394 

395 # Get the alt and azimuth of the observation 

396 meta["obsalt"], meta["obsaz"] = radec_to_altaz( 

397 meta["obsra"], 

398 meta["obsdec"], 

399 pyfhd_header["lat"], 

400 pyfhd_header["lon"], 

401 pyfhd_header["alt"], 

402 meta["jd0"], 

403 ) 

404 

405 if hdr is not None: 405 ↛ 419line 405 didn't jump to line 419 because the condition on line 405 was always true

406 # Save the raw header and data into the meta dictionary 

407 # Save the header as a Python dictionary 

408 meta["meta_hdr"] = {} 

409 for key in hdr.keys(): 

410 # Check if they is HISTORY or COMMENT which will be changed to a list for ease of use with hdf5 files 

411 if key in ["HISTORY", "COMMENT"]: 

412 meta["meta_hdr"][key] = list(hdr[key]) 

413 else: 

414 meta["meta_hdr"][key] = hdr[key] 

415 # The astropy FITS_rec class is based off a numpy record array so saving as is should be fine 

416 # If so desired a tolist() function to turn the data into list of lists, but you lose the column names 

417 meta["meta_data"] = data 

418 

419 return meta 

420 

421 

422def project_slant_orthographic(meta: dict, obs: dict, epoch=2000) -> dict: 

423 """ 

424 Create an astrometry data structure holding key astrometry information. 

425 It's essentially a WCS data structure, done as a Python dictionary allowing greater compatibility 

426 with other packages. 

427 

428 Parameters 

429 ---------- 

430 meta : dict 

431 The current metadata dictionary 

432 obs : dict 

433 The current obs dictionary 

434 epoch : float 

435 The equinox used for the dictionary structure 

436 

437 Returns 

438 ------- 

439 astr : dict 

440 An astrometry structure built from meta and obs 

441 """ 

442 

443 if abs(meta["phasera"] - meta["zenra"]) > 90: 443 ↛ 450line 443 didn't jump to line 450 because the condition on line 443 was always true

444 lon_offset = ( 

445 meta["phasera"] 

446 - (360 if meta["phasera"] > meta["zenra"] else -360) 

447 - meta["zenra"] 

448 ) 

449 else: 

450 lon_offset = meta["phasera"] - meta["zenra"] 

451 zenith_ang = angle_difference( 

452 meta["phasera"], meta["phasedec"], meta["zenra"], meta["zendec"], degree=True 

453 ) 

454 parallactic_ang = parallactic_angle(meta["zendec"], lon_offset, meta["phasedec"]) 

455 

456 xi = -1 * np.tan(np.radians(zenith_ang)) * np.sin(np.radians(parallactic_ang)) 

457 eta = np.tan(np.radians(zenith_ang)) * np.cos(np.radians(parallactic_ang)) 

458 

459 # Replicate MAKE_ASTR return dictionary structure from astrolib 

460 # We don't have to do it perfectly as it's only used for this function with the above as inputs 

461 # This is essentially a WCS in a dictionary for use with other libraries other than Astropy 

462 astr = {} 

463 astr["naxis"] = np.array([obs["dimension"], obs["elements"]]) 

464 astr["cd"] = np.identity(2) 

465 astr["cdelt"] = np.full(2, obs["degpix"]) 

466 astr["crpix"] = np.array([meta["obsx"], meta["obsy"]]) + 1 

467 astr["crval"] = np.array([meta["phasera"], meta["phasedec"]]) 

468 projection_name = "SIN" 

469 astr["ctype"] = ["RA---" + projection_name, "DEC--" + projection_name] 

470 astr["longpole"] = 180 

471 astr["latpole"] = 0 

472 astr["pv2"] = np.array([xi, eta]) 

473 # The PV1 array in Astrolib ASTR contains 5 projection parameters associated with longitude axis 

474 # [xyoff, phi0, theta0, longpole, latpole] 

475 # xyoff and phi0 are 0 as default 

476 # The third number [i = 2] is determined by the fact we are using SIN zenithal projections 

477 # The last are the longpole and latpole we set earlier 

478 astr["pv1"] = np.array([0, 0, 90, 180, 0], dtype=np.float64) 

479 astr["axes"] = np.array([1, 2]) 

480 astr["reverse"] = ( 

481 0 # Since Axes are always valid Celestial, we don't need to reverse them 

482 ) 

483 astr["coord_sys"] = "C" # Celestial Coordinate System in MAKE_ASTR 

484 astr["projection"] = projection_name 

485 astr["known"] = np.array([1]) # The projection name is guaranteed to be known 

486 astr["radecsys"] = "ICRS" # Using ICRS instead of FK5 

487 astr["equinox"] = epoch 

488 astr["dateobs"] = Time(meta["jd0"], format="jd").to_value("fits") 

489 astr["mjdobs"] = meta["jd0"] - 2400000.5 

490 astr["x0y0"] = np.zeros(2, dtype=np.float64) 

491 # Get the pixel coordinates of zenra and zendec 

492 zenx, zeny = radec_to_pixel(meta["zenra"], meta["zendec"], astr) 

493 

494 return astr, zenx, zeny 

495 

496 

497def update_obs( 

498 obs: dict, 

499 dimension: int, 

500 kbinsize: float | int, 

501 beam_nfreq_avg: float | None = None, 

502 fov: float | None = None, 

503) -> dict: 

504 """ 

505 Inside the quickview function for exporting files we need to update the obs 

506 dictionary based on the new dimension and kbinsize given. This differs slightly from 

507 FHD as we only adjust the exact things required for this as we only use this function 

508 once in quickview. 

509 

510 Parameters 

511 ---------- 

512 obs : dict 

513 The original observation dictionary 

514 dimension : int 

515 The new dimension for the size of each axes 

516 kbinsize : float | int 

517 The new kbin 

518 beam_nfreq_avg: float | None 

519 Set the new factor to average up the frequency resolution,by default None 

520 fov: float | None 

521 Set a new field of view, by default None 

522 

523 Returns 

524 ------- 

525 obs: dict 

526 The new updated obs dictionary 

527 """ 

528 if beam_nfreq_avg is None: 528 ↛ 532line 528 didn't jump to line 532 because the condition on line 528 was always true

529 beam_nfreq_avg = np.round( 

530 obs["n_freq"] / (np.max(obs["baseline_info"]["fbin_i"]) + 1) 

531 ) 

532 freq_bin = beam_nfreq_avg * obs["freq_res"] 

533 freq_hist, _, freq_ri = histogram(obs["baseline_info"]["freq"], bin_size=freq_bin) 

534 freq_bin_i = np.zeros(obs["n_freq"], dtype=np.int64) 

535 for bin in range(freq_hist.size - 1): 

536 if freq_ri[bin] < freq_ri[bin + 1]: 536 ↛ 535line 536 didn't jump to line 535 because the condition on line 536 was always true

537 freq_bin_i[freq_ri[freq_ri[bin] : freq_ri[bin + 1]]] = bin 

538 # Adjust the obs dictionary based on the new dimension and kbinsize 

539 obs["dimension"] = dimension 

540 obs["elements"] = dimension 

541 obs["obsx"] = dimension / 2 

542 obs["obsy"] = dimension / 2 

543 obs["kpix"] = kbinsize if fov is None else (180 / np.pi) / fov 

544 obs["degpix"] = (180 / np.pi) / (obs["kpix"] * dimension) 

545 obs["max_baseline"] = min( 

546 obs["max_baseline"], (dimension * obs["kpix"]) / np.sqrt(2) 

547 ) 

548 obs["astr"]["naxis"] = np.array([dimension, dimension]) 

549 obs["astr"]["cdelt"] = np.full(2, obs["degpix"]) 

550 obs["astr"]["crpix"] = np.array([obs["obsx"] + 1, obs["obsy"] + 1]) 

551 obs["baseline_info"]["fbin_i"] = freq_bin_i 

552 

553 return obs