Coverage for PyFHD/plotting/calibration.py: 6%

95 statements  

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

1import numpy as np 

2import matplotlib.pyplot as plt 

3import matplotlib.ticker as ticker 

4import matplotlib 

5from pathlib import Path 

6 

7matplotlib.use("pdf") 

8 

9 

10def plot_cals(obs: dict, cal: dict, pyfhd_config: dict): 

11 """ 

12 Plot the calibration solutions, the calibration residuals, and the raw calibration solutions 

13 in a grid of 128 per page. 

14 

15 Parameters 

16 ---------- 

17 obs : dict 

18 Observation metadata dictionary 

19 cal : dict 

20 Calibration dictionary 

21 pyfhd_config : dict 

22 Run option dictionary 

23 """ 

24 

25 # Plotting only unflagged frequencies 

26 freq_i_use = np.where(obs["baseline_info"]["freq_use"])[0] 

27 freq_arr_use = obs["baseline_info"]["freq"][freq_i_use] / 1e6 

28 

29 # Plotting only unflagged tiles 

30 tile_i_use = np.where(obs["baseline_info"]["tile_use"])[0] 

31 

32 # Plotting amplitude and phase for the gain, gain_residual, and their sum (raw solutions) 

33 obs_id = pyfhd_config["obs_id"] 

34 cal_plot_dir = Path(pyfhd_config["output_dir"], "plots", "calibration") 

35 cal_plot_dir.mkdir(parents=True, exist_ok=True) 

36 n_types = 3 # plotting three types of amp and phase gains 

37 save_path_roots = [ 

38 Path(cal_plot_dir, f"{obs_id}_cal_amp"), 

39 Path(cal_plot_dir, f"{obs_id}_cal_phase"), 

40 Path(cal_plot_dir, f"{obs_id}_cal_residual_amp"), 

41 Path(cal_plot_dir, f"{obs_id}_cal_residual_phase"), 

42 Path(cal_plot_dir, f"{obs_id}_cal_raw_amp"), 

43 Path(cal_plot_dir, f"{obs_id}_cal_raw_phase"), 

44 ] 

45 

46 # Calibration solutions are referenced mutliple times, put them in variables for speed 

47 cal_sol_amp = np.abs(cal["gain"][:, freq_i_use, :]) 

48 cal_sol_phase = np.arctan2( 

49 (cal["gain"][:, freq_i_use, :]).imag, (cal["gain"][:, freq_i_use, :]).real 

50 ) 

51 cal_raw_amp = np.abs( 

52 cal["gain"][:, freq_i_use, :] + cal["gain_residual"][:, freq_i_use, :] 

53 ) 

54 cal_raw_phase = np.arctan2( 

55 (cal["gain"][:, freq_i_use, :] + cal["gain_residual"][:, freq_i_use, :]).imag, 

56 (cal["gain"][:, freq_i_use, :] + cal["gain_residual"][:, freq_i_use, :]).real, 

57 ) 

58 

59 # NOTE: The residuals that are plotted are the differences in raw amp/phase and solution amp/phase, 

60 # *not* the amp/phase of the raw and solution difference 

61 cal_res_amp = cal_raw_amp - cal_sol_amp 

62 cal_res_phase = np.unwrap(cal_raw_phase, axis=1) - np.unwrap(cal_sol_phase, axis=1) 

63 

64 # Find the min/max amplitude and phase for plotting, only using unflagged tiles 

65 amp_minmax = np.zeros((6)) 

66 phase_minmax = np.zeros((6)) 

67 amp_minmax[0:2] = [np.nanmin(cal_sol_amp), np.nanmax(cal_sol_amp)] 

68 amp_minmax[2:4] = [np.nanmin(cal_res_amp), np.nanmax(cal_res_amp)] 

69 amp_minmax[4:6] = [np.nanmin(cal_raw_amp), np.nanmax(cal_raw_amp)] 

70 phase_minmax[0:2] = [-np.pi - 0.1, np.pi + 0.1] 

71 phase_minmax[2:4] = [np.nanmin(cal_res_phase), np.nanmax(cal_res_phase)] 

72 phase_minmax[4:6] = [-np.pi - 0.1, np.pi + 0.1] 

73 

74 # Create patches for the legend 

75 patches = [ 

76 plt.plot( 

77 [], 

78 [], 

79 color=(["blue", "red"])[pol_i], 

80 linestyle="solid", 

81 label="{:s}".format(obs["pol_names"][pol_i]), 

82 )[0] 

83 for pol_i in range(cal["n_pol"]) 

84 ] 

85 

86 # Arrange calibration subplots in rows, cols with a maximum per page. 

87 # Multiple pages will be required beyond 128 stations 

88 n_rows = 8 

89 n_cols = 16 

90 n_pages = obs["n_tile"] // (n_cols * n_rows) 

91 

92 # Create dictionary for the subplot grid to create reliable, static plots 

93 gridspec_kw = { 

94 "left": 0.05, 

95 "right": 0.98, 

96 "top": 0.92, 

97 "bottom": 0.05, 

98 "hspace": 0.35, 

99 } 

100 

101 # Create calibration solution plots for each page of 128 maximum stations for the specified types 

102 for type_i in range(n_types): 

103 for page_i in range(n_pages): 

104 

105 # Calculate number of stations for this page 

106 if obs["n_tile"] // (n_cols * n_rows * (page_i + 1)) == 0: 

107 page_tiles = obs["n_tile"] - (n_cols * n_rows * (page_i + 1)) 

108 else: 

109 page_tiles = n_cols * n_rows 

110 

111 # Define the subplots, add extra rows on the figsize to account for labels 

112 fig_amp, ax_amp = plt.subplots( 

113 n_rows, n_cols, figsize=(n_cols, n_rows + 2), gridspec_kw=gridspec_kw 

114 ) 

115 fig_phase, ax_phase = plt.subplots( 

116 n_rows, n_cols, figsize=(n_cols, n_rows + 2), gridspec_kw=gridspec_kw 

117 ) 

118 

119 # Cycle through each row and col on the page and set tick and labels 

120 for row_i in range(n_rows): 

121 for col_i in range(n_cols): 

122 

123 # Set fontsize maximum number of ticks on xaxis to 2 

124 ax_amp[row_i, col_i].xaxis.set_major_locator(ticker.MaxNLocator(2)) 

125 ax_phase[row_i, col_i].xaxis.set_major_locator( 

126 ticker.MaxNLocator(2) 

127 ) 

128 ax_amp[row_i, col_i].tick_params(labelsize=8, direction="in") 

129 ax_phase[row_i, col_i].tick_params(labelsize=8, direction="in") 

130 

131 # Only set tick labels on the leftmost column and the bottommost row 

132 if col_i > 0: 

133 ax_amp[row_i, col_i].set_yticks([]) 

134 ax_phase[row_i, col_i].set_yticks([]) 

135 if row_i < 7: 

136 ax_amp[row_i, col_i].set_xticks([]) 

137 ax_phase[row_i, col_i].set_xticks([]) 

138 

139 # Set the y-axis min and max given the plot type 

140 ax_amp[row_i, col_i].set_ylim( 

141 amp_minmax[2 * type_i], amp_minmax[2 * type_i + 1] 

142 ) 

143 ax_phase[row_i, col_i].set_ylim( 

144 phase_minmax[2 * type_i], phase_minmax[2 * type_i + 1] 

145 ) 

146 

147 # Plot each tile on the specified subplot 

148 tile_start = page_i * (n_cols * n_rows) 

149 for tile_i in range(tile_start, page_tiles + tile_start): 

150 col = (tile_i - page_i * (n_cols * n_rows)) % n_cols 

151 row = (tile_i - page_i * (n_cols * n_rows)) // n_cols 

152 

153 ax_amp[row, col].set_title( 

154 str(obs["baseline_info"]["tile_names"][(page_i + 1) * tile_i]), 

155 fontsize=9, 

156 ) 

157 ax_phase[row, col].set_title( 

158 str(obs["baseline_info"]["tile_names"][(page_i + 1) * tile_i]), 

159 fontsize=9, 

160 ) 

161 

162 for pol_i in range(cal["n_pol"]): 

163 if pol_i == 0: 

164 colour = "b-" 

165 elif pol_i == 1: 

166 colour = "r-" 

167 

168 # Select current plotting type 

169 if type_i == 0: 

170 # Gain and phase fit solutions 

171 amp = cal_sol_amp[pol_i, :, tile_i].squeeze() 

172 phase = cal_sol_phase[pol_i, :, tile_i].squeeze() 

173 if type_i == 1: 

174 # Gain and phase residuals (per-frequency solutions minus fit solutions) 

175 amp = cal_res_amp[pol_i, :, tile_i].squeeze() 

176 phase = cal_res_phase[pol_i, :, tile_i].squeeze() 

177 if type_i == 2: 

178 # Gain and phase per-frequency solutions 

179 amp = cal_raw_amp[pol_i, :, tile_i].squeeze() 

180 phase = cal_raw_phase[pol_i, :, tile_i].squeeze() 

181 

182 # Leave blanks for flagged or undefined tiles 

183 if np.isnan([amp, phase]).all() or tile_i not in tile_i_use: 

184 ax_amp[row, col].set_axis_off() 

185 ax_phase[row, col].set_axis_off() 

186 else: 

187 ax_amp[row, col].plot(freq_arr_use, amp, colour, lw=1) 

188 ax_phase[row, col].plot(freq_arr_use, phase, colour, lw=1) 

189 

190 fig_amp.suptitle(pyfhd_config["obs_id"], fontsize=14) 

191 fig_phase.suptitle(pyfhd_config["obs_id"], fontsize=14) 

192 

193 fig_amp.supylabel("Amplitude") 

194 fig_amp.supxlabel("Frequency (MHz)") 

195 

196 fig_phase.supylabel("Phase (radians)") 

197 fig_phase.supxlabel("Frequency (MHz)") 

198 

199 fig_amp.legend( 

200 handles=patches, 

201 loc="outside upper right", 

202 ncol=2, 

203 frameon=False, 

204 fontsize=12, 

205 handlelength=1, 

206 ) 

207 fig_phase.legend( 

208 handles=patches, 

209 loc="outside upper right", 

210 ncol=2, 

211 frameon=False, 

212 fontsize=12, 

213 handlelength=1, 

214 ) 

215 

216 # Page naming conventions 

217 if n_pages > 1: 

218 page_name = "_page" + str(page_i) 

219 else: 

220 page_name = "" 

221 

222 # Save amplitude and phase plot given the type 

223 fig_amp.savefig( 

224 f"{save_path_roots[2 * type_i]}{page_name}.png", 

225 bbox_inches="tight", 

226 dpi=200, 

227 ) 

228 fig_phase.savefig( 

229 f"{save_path_roots[2 * type_i + 1]}{page_name}.png", 

230 bbox_inches="tight", 

231 dpi=200, 

232 ) 

233 

234 plt.close(fig_amp) 

235 plt.close(fig_phase)