Coverage for PyFHD/data_setup/uvfits.py: 67%

303 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 

3from astropy.io import fits 

4from astropy.time import Time 

5from astropy.io.fits.fitsrec import FITS_rec 

6from astropy.io.fits.header import Header 

7from pathlib import Path 

8import logging 

9from astropy.coordinates import EarthLocation 

10import astropy 

11from PyFHD.io.pyfhd_io import save 

12 

13 

14def extract_header( 

15 pyfhd_config: dict, logger: logging.Logger, model_uvfits=False 

16) -> tuple[dict, np.recarray, FITS_rec, Header]: 

17 """ 

18 Extract data from the uvfits header, the data extracted will contain metadata about the observation, antennas, visibilities 

19 

20 Parameters 

21 ---------- 

22 uvfits_path : str 

23 Path to the uvfits to open (either the data or the model) 

24 pyfhd_config : dict 

25 This is the config created from the argparse 

26 logger : logging.Logger 

27 The PyFHD logger 

28 model_uvfits : bool 

29 If True, load in the model uvfits. If False, load in a data uvfits file, by default False 

30 

31 Returns 

32 ------- 

33 pyfhd_header : dict 

34 The result from the extraction of the header of the UVFITS file, containing observation metadata mostly 

35 params_data: np.recarray 

36 The data from the UVFITS file, containing the visibility metadata 

37 antenna_data : FITS_rec 

38 The layout data which will be used in the create_layout function, mostly antenna metadata 

39 antenna_header : Header 

40 The layout header which will be used in the create_layout function, mostly antenna metadatas 

41 

42 Raises 

43 ------ 

44 KeyError 

45 If the UVFITS file doesn't contain all the data then a KeyError will be raised 

46 """ 

47 

48 if model_uvfits: 

49 uvfits_path = Path(pyfhd_config["model_file_path"]) 

50 logger.info(f"Reading in model visibilities from: {uvfits_path}") 

51 else: 

52 uvfits_path = Path( 

53 pyfhd_config["input_path"], pyfhd_config["obs_id"] + ".uvfits" 

54 ) 

55 logger.info(f"Reading in visibilities from: {uvfits_path}") 

56 

57 # Retrieve all data from the observation 

58 with fits.open(uvfits_path) as observation: 

59 

60 params_header = observation[0].header 

61 params_data = observation[0].data 

62 

63 # Keep the layout header and data for the create_layout function 

64 antenna_data = observation[1].data 

65 antenna_header = observation[1].header 

66 

67 pyfhd_header = {} 

68 # Retrieve data from the params_header 

69 pyfhd_header["pol_dim"] = 2 

70 pyfhd_header["freq_dim"] = 4 

71 pyfhd_header["real_index"] = 0 

72 pyfhd_header["imaginary_index"] = 1 

73 pyfhd_header["weights_index"] = 2 

74 pyfhd_header["n_tile"] = 128 

75 pyfhd_header["naxis"] = params_header["naxis"] 

76 pyfhd_header["n_params"] = params_header["pcount"] 

77 pyfhd_header["n_baselines"] = params_header["gcount"] 

78 pyfhd_header["n_complex"] = params_header["naxis2"] 

79 pyfhd_header["n_pol"] = params_header["naxis3"] 

80 pyfhd_header["n_freq"] = params_header["naxis4"] 

81 pyfhd_header["freq_ref"] = params_header["crval4"] 

82 pyfhd_header["freq_res"] = params_header["cdelt4"] 

83 try: 

84 pyfhd_header["dateobs"] = params_header["date-obs"] 

85 except KeyError: 

86 pyfhd_header["dateobs"] = params_header["dateobs"] 

87 freq_ref_i = params_header["crpix4"] - 1 

88 pyfhd_header["frequency_array"] = ( 

89 np.arange(pyfhd_header["n_freq"]) - freq_ref_i 

90 ) * pyfhd_header["freq_res"] + pyfhd_header["freq_ref"] 

91 try: 

92 pyfhd_header["obsra"] = params_header["obsra"] 

93 except KeyError: 

94 logger.warning("OBSRA not found in UVFITS file") 

95 pyfhd_header["obsra"] = params_header["ra"] 

96 

97 try: 

98 pyfhd_header["obsdec"] = params_header["obsdec"] 

99 except KeyError: 

100 logger.warning("OBSDEC not found in UVFITS file") 

101 pyfhd_header["obsdec"] = params_header["dec"] 

102 # Put in locations of instrument from FITS file or from Astropy site data 

103 # If you want to see the list of current site names using EarthLocation.get_site_names() 

104 # If you want to use PyFHD with HERA in the future 

105 # and make it compatible you might have to put in the lat/lon/alt yourself 

106 try: 

107 location = EarthLocation.of_site(pyfhd_config["instrument"]) 

108 except astropy.coordinates.errors.UnknownSiteException: 

109 # If the site isn't known then select MWA, which no longer uses inbuilt corrdinates from the FHD repo. 

110 logger.info( 

111 f"Failed to load in the {pyfhd_config['instrument']} instrument location from astropy. If lon/lat/alt are not in the UVFITS things will fail." 

112 ) 

113 # Can also do MWA or Murchison Widefield Array 

114 location = EarthLocation("mwa") 

115 

116 try: 

117 pyfhd_header["lon"] = params_header["lon"] 

118 except KeyError: 

119 pyfhd_header["lon"] = location.lon.deg 

120 try: 

121 pyfhd_header["lat"] = params_header["lat"] 

122 except KeyError: 

123 pyfhd_header["lat"] = location.lat.deg 

124 try: 

125 pyfhd_header["alt"] = params_header["alt"] 

126 except KeyError: 

127 pyfhd_header["alt"] = location.height.value 

128 

129 logger.info( 

130 f"Setting {pyfhd_config['instrument']} instrument location to: lon {pyfhd_header['lon']:.2f}, lat {pyfhd_header['lat']:.2f}, alt {pyfhd_header['alt']:.2f}" 

131 ) 

132 

133 # Setup params list and names 

134 param_list = [] 

135 ptype_list = ["PTYPE{}".format(i) for i in range(1, pyfhd_header["n_params"] + 1)] 

136 for ptype in ptype_list: 

137 param_list.append(params_header[ptype].strip()) 

138 param_names = [] 

139 for key in list(params_header.keys()): 

140 if key.startswith("CTYPE"): 

141 param_names.append(params_header[key].strip().lower()) 

142 

143 # Validate params list 

144 params_valid = True 

145 pyfhd_header["uu_i"] = "UU" in param_list 

146 if not pyfhd_header["uu_i"]: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 logger.error( 

148 "Group parameter UU not found within uvfits params_header PTYPE keywords" 

149 ) 

150 params_valid = False 

151 

152 pyfhd_header["vv_i"] = "VV" in param_list 

153 if not pyfhd_header["vv_i"]: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 logger.error( 

155 "Group parameter VV not found within uvfits params_header PTYPE keywords" 

156 ) 

157 params_valid = False 

158 

159 pyfhd_header["ww_i"] = "WW" in param_list 

160 if not pyfhd_header["ww_i"]: 160 ↛ 161line 160 didn't jump to line 161 because the condition on line 160 was never true

161 logger.error( 

162 "Group parameter WW not found within uvfits params_header PTYPE keywords" 

163 ) 

164 params_valid = False 

165 

166 pyfhd_header["ant1_i"] = "ANTENNA1" in param_list 

167 pyfhd_header["ant2_i"] = "ANTENNA2" in param_list 

168 

169 if not pyfhd_header["ant1_i"] or not pyfhd_header["ant2_i"]: 

170 pyfhd_header["baseline_i"] = param_list.index("BASELINE") 

171 if not pyfhd_header["baseline_i"]: 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true

172 logger.error( 

173 "Group parameter BASELINE (or ANTENNA1 and ANTENNA2) not found within uvfits params_header PTYPE keywords" 

174 ) 

175 params_valid = False 

176 

177 pyfhd_header["date_i"] = param_list.index("DATE") 

178 if not pyfhd_header["date_i"]: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 logger.error( 

180 "Group parameter DATE not found within uvfits params_header PTYPE keywords" 

181 ) 

182 params_valid = False 

183 

184 # Stop PyFHD if its not valid 

185 if not params_valid: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true

186 raise KeyError( 

187 "One of these keys is missing from the UVFITS file: UU, VV, WW, BASELINE, DATE, check the log to see which one" 

188 ) 

189 

190 # Get the Julian Date 

191 if param_list.count("DATE") > 1: 

192 # This needs testing as Astropy scales automatically, which affects the DATE data read in, this should be the same though 

193 pyfhd_header["jd0"] = ( 

194 params_header["PZERO{}".format(pyfhd_header["date_i"] + 1)] 

195 + params_data["DATE"][0] 

196 - params_data.columns[pyfhd_header["date_i"]].bzero 

197 ) 

198 else: 

199 # This is the bzero value used to normalize the value in Astropy for date 

200 pyfhd_header["jd0"] = params_header[ 

201 "PZERO{}".format(pyfhd_header["date_i"] + 1) 

202 ] 

203 

204 # Take the julian date and use that in dateobs in the fits format 

205 if "jd0" in pyfhd_header.keys(): 205 ↛ 210line 205 didn't jump to line 210 because the condition on line 205 was always true

206 julian_time = Time(pyfhd_header["jd0"], format="jd") 

207 julian_time.format = "fits" 

208 pyfhd_header["dateobs"] = julian_time.value 

209 # Probably won't reach here, if it does fill in jd0 from dateobs (fits to julian) 

210 elif "dateobs" in pyfhd_header.keys(): 

211 fits_time = Time(pyfhd_header["dateobs"], format="fits") 

212 fits_time.format = "jd" 

213 pyfhd_header["jd0"] = fits_time.value 

214 

215 return pyfhd_header, params_data, antenna_header, antenna_data 

216 

217 

218def create_params( 

219 pyfhd_header: dict, params_data: np.recarray, logger: logging.Logger 

220) -> dict: 

221 """ 

222 Given the extracted header, params data from the uvfits file, create the params dictionary to store 

223 the relevant visibility metadata 

224 

225 Parameters 

226 ---------- 

227 pyfhd_header : dict 

228 The resulting header fom the fits file stored in a dictonary 

229 params_data : np.recarray 

230 The data from the fits file as taken from astropy.io.fits.getdata 

231 logger : logging.Logger 

232 The PyFHD logger 

233 

234 Returns 

235 ------- 

236 params : dict 

237 The visibility metadata stored as a dictionary (instead of recarray as a dict is faster) 

238 

239 Raises 

240 ====== 

241 KeyError 

242 If the UVFITS data returned doesn't contain the variables then a KeyError will get thrown. 

243 

244 See Also 

245 ======== 

246 astropy.io.fits.getdata : https://docs.astropy.org/en/stable/io/fits/api/files.html#getdata 

247 extract_header : Extracts the header from the UVFITS file and returns the header and data 

248 """ 

249 params = {} 

250 # Retrieve params values 

251 try: 

252 params["uu"] = params_data["UU"].astype(np.float64) 

253 params["vv"] = params_data["VV"].astype(np.float64) 

254 params["ww"] = params_data["WW"].astype(np.float64) 

255 # Astropy has already normalized the values by PZEROx, time in Julian 

256 params["time"] = params_data["DATE"] 

257 # Get baseline and antenna arrays 

258 # The antenna arrays already exist then take those 

259 if pyfhd_header["ant1_i"] and pyfhd_header["ant2_i"]: 259 ↛ 265line 259 didn't jump to line 265 because the condition on line 259 was always true

260 params["antenna1"] = params_data["ANTENNA1"] 

261 params["antenna2"] = params_data["ANTENNA2"] 

262 # Else calculate it from the baseline array 

263 else: 

264 # Calculate antenna_mod_index to check for bad fits 

265 params["baseline_arr"] = params_data["BASELINE"] 

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

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

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

269 tile_B_test = np.min(baseline_min) % antenna_mod_index 

270 if tile_B_test > 1: 

271 if baseline_min % 2 == 1: 

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

273 # Tile numbers start from 1 

274 params["antenna1"] = np.floor(params["baseline_arr"] / antenna_mod_index) 

275 params["antenna2"] = np.fix(params["baseline_arr"] % antenna_mod_index) 

276 

277 except KeyError as error: 

278 logger.error( 

279 f"Validation efforts failed, key not found in data, Traceback : {error}" 

280 ) 

281 exit() 

282 

283 return params 

284 

285 

286def extract_visibilities( 

287 pyfhd_header: dict, 

288 params_data: np.recarray, 

289 pyfhd_config: dict, 

290 logger: logging.Logger, 

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

292 """ 

293 Extract the visibilities and their weights from the UVFITS data. 

294 

295 Parameters 

296 ---------- 

297 pyfhd_header : dict 

298 The resulting header fom the fits file stored in a dictonary 

299 params_data : np.recarray 

300 The data from the fits file as taken from astropy.io.fits.getdata 

301 pyfhd_config : dict 

302 This is the config created from the argprase 

303 logger : logging.Logger 

304 The PyFHD Logger 

305 

306 Returns 

307 ------- 

308 vis_arr : NDArray[np.complex128] 

309 The visibility array 

310 vis_weights : NDArray[np.float64] 

311 The visibility weights array 

312 

313 See Also 

314 ======== 

315 astropy.io.fits.getdata : https://docs.astropy.org/en/stable/io/fits/api/files.html#getdata 

316 extract_header : Extracts the header from the UVFITS file and returns the header and data 

317 """ 

318 

319 data_array = np.squeeze(params_data["DATA"]) 

320 # Set the number of polarizations 

321 if pyfhd_config["n_pol"] == 0: 321 ↛ 322line 321 didn't jump to line 322 because the condition on line 321 was never true

322 n_pol = pyfhd_header["n_pol"] 

323 else: 

324 n_pol = pyfhd_config["n_pol"] 

325 

326 if data_array.ndim > 4: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true

327 logger.error("No current support for PyFHD to support spectral dimensions yet") 

328 exit() 

329 else: 

330 polarizations = np.arange(n_pol) 

331 vis_arr = data_array[ 

332 :, :, polarizations, pyfhd_header["real_index"] 

333 ] + data_array[:, :, polarizations, pyfhd_header["imaginary_index"]] * (1j) 

334 vis_weights = data_array[:, :, polarizations, pyfhd_header["weights_index"]] 

335 # Redo the shape so its the format per polarization, per frequency per baseline. 

336 # Also ensure the types are double precision to ensure calculations from them result 

337 # in double precision 

338 return vis_arr.transpose().astype(np.complex128), vis_weights.transpose().astype( 

339 np.float64 

340 ) 

341 

342 

343def _check_layout_valid( 

344 layout: dict, key: str, logger: logging.Logger, check_min_max=False 

345): 

346 """ 

347 Check if the key given is a valid part of the layout, if not give an error in the log. 

348 The errors do not stop the run as it might only affect compatibility with other packages and 

349 could be solved by editing or fixing the UVFITS file. 

350 

351 Parameters 

352 ---------- 

353 layout : dict 

354 The current layout 

355 key : str 

356 The key we're interested in validating 

357 logger : logging.Logger 

358 The logger 

359 check_min_max : bool, optional 

360 When True check if the min is the same as max, if so changes the value so its only one number, by default False 

361 """ 

362 

363 if check_min_max: 

364 if type(layout[key]) == np.ndarray and np.min(layout[key]) == np.max( 364 ↛ 369line 364 didn't jump to line 369 because the condition on line 364 was always true

365 layout[key] 

366 ): 

367 layout[key] = layout[key][0] 

368 

369 if type(layout[key]) == np.ndarray and (layout[key].size != layout["n_antenna"]): 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true

370 logger.error( 

371 f"The layout[{key}] array set is not the same size of the number of antennas. Check the UVFITS file for errors." 

372 ) 

373 

374 

375def create_layout( 

376 antenna_header: Header, 

377 antenna_data: FITS_rec, 

378 pyfhd_config: dict, 

379 logger: logging.Logger, 

380) -> dict: 

381 """ 

382 Create a very explicit antenna and telescope position dictionary, incorperating 

383 timing (e.g. time system, reference, leap seconds), location (e.g. array center, 

384 coordinate frame, Earth's rotation), and antenna information (e.g. names, numbers, 

385 coordinates, mount type, feed polarization). This is used to create the layout 

386 dictionary which is compatible with pyuvdata. 

387 

388 Parameters 

389 ---------- 

390 antenna_header : Header 

391 The header from the second table of the observation 

392 antenna_data : FITS_rec 

393 The data from the second table of the observation 

394 pyfhd_config : dict 

395 PyFHD's configuration dictionary containing all the options for a run 

396 logger : logging.Logger 

397 PyFHD's logger 

398 

399 Returns 

400 ------- 

401 layout: dict 

402 The antenna layout dictionary compatible with pyuvdata 

403 

404 See Also 

405 --------- 

406 extract_header : Opens the UVFITS file and extracts the header and data, including the antenna_header and antenna_data. 

407 """ 

408 

409 layout = {} 

410 

411 # Extract data from the header 

412 # array_center 

413 try: 

414 layout["array_center"] = [ 

415 antenna_header["arrayx"], 

416 antenna_header["arrayy"], 

417 antenna_header["arrayz"], 

418 ] 

419 except KeyError: 

420 # if no center given, assume MWA center (Tingay et al. 2013, converted from lat/lon using pyuvdata) 

421 logger.info( 

422 "No center was given in the UVFITS file, assuming MWA is the array and using a default center for MWA" 

423 ) 

424 layout["array_center"] = [ 

425 -2559454.07880307, 

426 5095372.14368305, 

427 -2849057.18534633, 

428 ] 

429 

430 # Coordinate_frame 

431 try: 

432 layout["coordinate_frame"] = antenna_header["frame"] 

433 except KeyError: 

434 logger.info("Coordinate Frame is missing from the UVFITS file, using IRTF") 

435 layout["coordinate_frame"] = "IRTF" 

436 

437 # Greenwich Sidereal Time 

438 try: 

439 layout["gst0"] = antenna_header["gstia0"] 

440 except KeyError: 

441 logger.warning( 

442 "Greenwich sidereal time missing from UVFITS file gst0 will be -1" 

443 ) 

444 layout["gst0"] = -1 

445 

446 # Earth's Rotation 

447 try: 

448 layout["earth_degpd"] = antenna_header["degpdy"] 

449 except KeyError: 

450 logger.info( 

451 "degpdy is missing from the UVFITS file, using 360.985 for Earth's rotation in degrees" 

452 ) 

453 layout["earth_degpd"] = 360.985 

454 

455 # Reference Date 

456 try: 

457 layout["refdate"] = antenna_header["rdate"] 

458 except KeyError: 

459 logger.warning("No refdate supplied in UVFITS file, set ref_date as -1") 

460 layout["refdate"] = -1 

461 

462 # Time System 

463 try: 

464 layout["time_system"] = antenna_header["timesys"].strip() 

465 except KeyError: 

466 try: 

467 layout["time_system"] = antenna_header["timsys"].strip() 

468 except KeyError: 

469 logger.warning( 

470 "No Time System supplied in UVFITS file setting time system as UTC" 

471 ) 

472 layout["time_system"] = "UTC" 

473 

474 # UT1UTC 

475 try: 

476 layout["dut1"] = antenna_header["ut1utc"] 

477 except KeyError: 

478 logger.info("UT1UTC is mising from UVFITS, using 0") 

479 layout["dut1"] = 0 

480 

481 # DATUTC 

482 try: 

483 layout["diff_utc"] = antenna_header["datutc"] 

484 except: 

485 logger.info("No difference set between time_system and UTC, set to 0") 

486 layout["diff_utc"] = 0 

487 

488 # Number of leap seconds 

489 try: 

490 layout["nleap_sec"] = antenna_header["iautc"] 

491 except KeyError: 

492 if layout["time_system"] == "IAT": 492 ↛ 493line 492 didn't jump to line 493 because the condition on line 492 was never true

493 logger.info( 

494 "Time System is IAT and leap seconds is missing, using value from diff_utc(layout)/datutc(uvfits)" 

495 ) 

496 layout["nleap_sec"] = layout["diff_utc"] 

497 else: 

498 logger.warning( 

499 "Number of Leap Seconds is missing and the time system isn't IAT so we can't know the leap seconds, setting as -1" 

500 ) 

501 

502 # Polarization Type 

503 try: 

504 layout["pol_type"] = antenna_header["poltype"] 

505 except KeyError: 

506 logger.info( 

507 "Polarization Type not in UVFITS file, Linear approximation for linear feeds is being used" 

508 ) 

509 layout["pol_type"] = "X-Y LIN" 

510 

511 # Polarization Characteristics 

512 try: 

513 layout["n_pol_cal_params"] = antenna_header["nopcal"] 

514 except KeyError: 

515 logger.info( 

516 "Polarization Characteristics of the feed not given in UVFITS file, Set n_pol_cal_params to 0" 

517 ) 

518 layout["n_pol_cal_params"] = 0 

519 

520 # Number of antennas 

521 try: 

522 layout["n_antenna"] = antenna_header["naxis2"] 

523 except KeyError: 

524 logger.info("Number of antennas missing from header, set 128 as per MWA") 

525 layout["n_antenna"] = 128 

526 

527 # Extract data from the data table 

528 # Antenna Names 

529 try: 

530 layout["antenna_names"] = antenna_data["anname"] 

531 except KeyError: 

532 logger.warning("Antenna Names missing, replacing with a string of numbers") 

533 layout["antenna_names"] = np.arange(layout["n_antenna"]).astype(str) 

534 

535 # Antenna Numbers 

536 try: 

537 layout["antenna_numbers"] = antenna_data["nosta"] 

538 except KeyError: 

539 layout.warning( 

540 "Antenna Numbers missing replacing with an array of numbers of range 1: n_antenna" 

541 ) 

542 layout["antenna_numbers"] = np.arange(1, layout["n_antenna"]) 

543 

544 # Antenna Coordinates 

545 try: 

546 layout["antenna_coords"] = antenna_data["stabxyz"] 

547 except KeyError: 

548 logger.warning( 

549 "Antenna Coordinates missing, replacing with a zero array of shape n_antenna, 3" 

550 ) 

551 layout["antenna_coords"] = np.zeros((layout["n_antenna"], 3)) 

552 

553 # Mount Type 

554 try: 

555 layout["mount_type"] = antenna_data["mntsta"] 

556 except KeyError: 

557 logger.warning("No Mount Type set, mount_type has been set to 0") 

558 layout["mount_type"] = 0 

559 

560 # Axis Offset 

561 try: 

562 layout["axis_offset"] = antenna_data["staxof"] 

563 except KeyError: 

564 logger.warning("Axis Offset is not given, setting to 0") 

565 layout["axis_offset"] = 0 

566 

567 # Feed Polarization of feed A (Pol A) 

568 try: 

569 layout["pola"] = antenna_data["poltya"] 

570 except: 

571 logger.warning("Pol A polarization not given setting to X") 

572 layout["pola"] = "X" 

573 

574 # PolA Orientation 

575 try: 

576 layout["pola_orientation"] = antenna_data["polaa"] 

577 except KeyError: 

578 logging.warning("PolA orientation not given setting to 0") 

579 layout["pola_orientation"] = 0 

580 

581 # PolA params 

582 try: 

583 layout["pola_cal_params"] = antenna_data["polcala"] 

584 except KeyError: 

585 logger.warning( 

586 "PolA params is missing from the UVFITS, set to array of zeros of length n_pol_cal_params or 0" 

587 ) 

588 if layout["n_pol_cal_params"] > 1: 588 ↛ 589line 588 didn't jump to line 589 because the condition on line 588 was never true

589 layout["pola_cal_params"] = np.zeros(layout["n_pol_cal_params"]) 

590 else: 

591 layout["pola_cal_params"] = 0 

592 

593 # Feed Polarization of feed B (Pol B) 

594 try: 

595 layout["polb"] = antenna_data["poltyb"] 

596 except: 

597 logger.warning("Pol B polarization not given setting to Y") 

598 layout["polb"] = "Y" 

599 

600 # PolB Orientation 

601 try: 

602 layout["polb_orientation"] = antenna_data["polab"] 

603 except KeyError: 

604 logging.warning("PolB orientation not given setting to 0") 

605 layout["polb_orientation"] = 90 

606 

607 # PolB params 

608 try: 

609 layout["polb_cal_params"] = antenna_data["polcalb"] 

610 except KeyError: 

611 logger.warning( 

612 "PolB params is missing from the UVFITS, set to array of zeros of length n_pol_cal_params or 0" 

613 ) 

614 if layout["n_pol_cal_params"] > 1: 614 ↛ 615line 614 didn't jump to line 615 because the condition on line 614 was never true

615 layout["polb_cal_params"] = np.zeros(layout["n_pol_cal_params"]) 

616 else: 

617 layout["polb_cal_params"] = 0 

618 

619 # Diameters 

620 try: 

621 layout["diameters"] = antenna_data["diameter"] 

622 except KeyError: 

623 logger.info("Diameters not in UVFITS file continuing.") 

624 

625 # Beam Full Width Half Maximum 

626 try: 

627 layout["beam_fwhm"] = antenna_data["beamfwhm"] 

628 except KeyError: 

629 logger.info("Beam Full Width Half maximum not present in UVFITS continuing.") 

630 

631 # Layout Validation 

632 _check_layout_valid(layout, "antenna_names", logger) 

633 _check_layout_valid(layout, "antenna_numbers", logger) 

634 _check_layout_valid(layout, "mount_type", logger, check_min_max=True) 

635 _check_layout_valid(layout, "axis_offset", logger, check_min_max=True) 

636 _check_layout_valid(layout, "pola", logger) 

637 _check_layout_valid(layout, "pola_orientation", logger, check_min_max=True) 

638 _check_layout_valid(layout, "polb", logger) 

639 _check_layout_valid(layout, "polb_orientation", logger, check_min_max=True) 

640 

641 save(Path(pyfhd_config["output_dir"], "layout.h5"), layout, "layout", logger) 

642 

643 return layout