Coverage for frank/make_figs.py: 87%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

555 statements  

1# Frankenstein: 1D disc brightness profile reconstruction from Fourier data 

2# using non-parametric Gaussian Processes 

3# 

4# Copyright (C) 2019-2020 R. Booth, J. Jennings, M. Tazzari 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU General Public License as published by 

8# the Free Software Foundation, either version 3 of the License, or 

9# (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, 

12# but WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

14# GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License 

17# along with this program. If not, see <https://www.gnu.org/licenses/> 

18# 

19"""This module generates figures for a Frankenstein fit and its diagnostics. 

20""" 

21import os 

22import numpy as np 

23import matplotlib.pyplot as plt 

24from matplotlib.gridspec import GridSpec 

25from matplotlib.colors import PowerNorm 

26import logging 

27 

28from frank.utilities import UVDataBinner 

29 

30from frank.plot import ( 

31 plot_deprojection_effect, 

32 plot_brightness_profile, 

33 plot_vis_quantity, 

34 plot_vis_hist, 

35 plot_iterations, 

36 plot_2dsweep, 

37 plot_convergence_criterion 

38) 

39 

40# Suppress some benign warnings 

41import warnings 

42warnings.filterwarnings('ignore', '.*handles with labels found.*') 

43 

44 

45# Global settings for plots 

46cs = ['#a4a4a4', 'k', '#f781bf', '#dede00'] 

47cs2 = ['#3498DB', '#984ea3', '#4daf4a', '#ff7f00'] 

48hist_cs = ['#377eb8', '#ff7f00', '#e41a1c', '#999999', '#4daf4a', '#f781bf', 

49 '#984ea3', '#dede00'] 

50multifit_cs = ['#e41a1c', '#999999', '#377eb8', '#ff7f00', '#4daf4a', '#f781bf', 

51 '#984ea3', '#dede00'] 

52ms = ['x', '+', '.', '1'] 

53 

54 

55class _DoNothingContextManager(object): 

56 def __init__(self, dummy_resource=None): 

57 self.dummy_resource = dummy_resource 

58 def __enter__(self): 

59 return self.dummy_resource 

60 def __exit__(self, *args): 

61 pass 

62 

63def frank_plotting_style_context_manager(use_frank_style=True): 

64 """Get a context manager for temporary use of frank's own plotting style""" 

65 if use_frank_style: 

66 frank_path = os.path.dirname(__file__) 

67 style_path = os.path.join(frank_path, 'frank.mplstyle') 

68 return plt.style.context(style_path) 

69 else: 

70 return _DoNothingContextManager() 

71 

72def use_frank_plotting_style(): 

73 """Set matplotlib to use frank's own plotting style""" 

74 frank_path = os.path.dirname(__file__) 

75 style_path = os.path.join(frank_path, 'frank.mplstyle') 

76 plt.style.use(style_path) 

77 

78 

79def make_deprojection_fig(u, v, vis, weights, geom, bin_widths, logx=False, 

80 logy=False, force_style=True, save_prefix=None, 

81 figsize=(8, 6)): 

82 r""" 

83 Produce a simple figure showing the effect of deprojection on the (u, v) 

84 coordinates and visibilities 

85 

86 Parameters 

87 ---------- 

88 u, v : array, unit = :math:`\lambda` 

89 Projected (u, v) coordinates 

90 vis : array, unit = Jy 

91 Projected visibilities (complex: real + imag * 1j) 

92 weights : array, unit = Jy^-2 

93 Weights assigned to observed visibilities, of the form 

94 :math:`1 / \sigma^2` 

95 geom : SourceGeometry object 

96 Fitted geometry (see frank.geometry.SourceGeometry) 

97 bin_widths : list, unit = \lambda 

98 Bin widths in which to bin the observed visibilities 

99 logy : bool, default = False 

100 Whether to plot the visibility distributions in log(flux) 

101 force_style: bool, default = True 

102 Whether to use preconfigured matplotlib rcParams in generated figure 

103 save_prefix : string, default = None 

104 Prefix for saved figure name. If None, the figure won't be saved 

105 figsize : tuple = (width, height) of figure, unit = inch 

106 

107 Returns 

108 ------- 

109 fig : Matplotlib `.Figure` instance 

110 The produced figure, including the GridSpec 

111 axes : Matplotlib `~.axes.Axes` class 

112 The axes of the produced figure 

113 """ 

114 

115 logging.info(' Making deprojection figure') 

116 

117 # Apply the deprojection to the provided (u, v) coordinates 

118 # and visibility amplitudes 

119 up, vp, visp = geom.apply_correction(u, v, vis) 

120 

121 re_vis = np.real(vis) 

122 re_visp = np.real(visp) 

123 

124 bsp = np.hypot(up, vp) 

125 

126 with frank_plotting_style_context_manager(force_style): 

127 gs = GridSpec(2, 1, hspace=0.2, top=0.97, left=0.07, right=0.97) 

128 fig = plt.figure(figsize=figsize) 

129 

130 ax0 = fig.add_subplot(gs[0]) 

131 ax1 = fig.add_subplot(gs[1]) 

132 axes = [ax0, ax1] 

133 

134 plot_deprojection_effect(u / 1e6, v / 1e6, up / 1e6, vp / 1e6, 

135 re_vis * 1e3, re_visp * 1e3, axes) 

136 

137 for i in range(len(bin_widths)): 

138 binned_vis = UVDataBinner(bsp, visp, weights, bin_widths[i]) 

139 vis_re = binned_vis.V.real 

140 vis_err_re = binned_vis.error.real 

141 

142 if logy: 

143 lab=r'Deprojected, >0, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3) 

144 else: 

145 lab=r'Deprojected, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3) 

146 plot_vis_quantity(binned_vis.uv / 1e6, vis_re * 1e3, ax1, c=cs[i], 

147 marker=ms[i], ls='None', 

148 label=lab) 

149 

150 if logy: 

151 plot_vis_quantity(binned_vis.uv / 1e6, -vis_re * 1e3, ax1, c=cs2[i], 

152 marker=ms[i], ls='None', 

153 label=r'Deprojected, <0, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

154 

155 ax0.set_xlabel(r'u [M$\lambda$]') 

156 ax0.set_ylabel(r'v [M$\lambda$]') 

157 

158 ax1.set_xlabel(r'Baseline [M$\lambda$]') 

159 ax1.set_ylabel('Re(V) [mJy]') 

160 if logx: 

161 ax1.set_xscale('log') 

162 if logy: 

163 ax1.set_yscale('log') 

164 ax1.set_ylim(bottom=1e-3) 

165 

166 ax0.legend(loc=0) 

167 ax1.legend(loc=0) 

168 

169 if save_prefix: 

170 plt.savefig(save_prefix + '_frank_deprojection.png', dpi=300, 

171 bbox_inches='tight') 

172 plt.close() 

173 

174 return fig, axes 

175 

176 

177def make_quick_fig(u, v, vis, weights, sol, bin_widths, dist=None, 

178 logx=False, force_style=True, save_prefix=None, 

179 stretch='power', gamma=1.0, asinh_a=0.02, figsize=(8,6)): 

180 r""" 

181 Produce a simple figure showing just a Frankenstein fit, not any diagnostics 

182 

183 Parameters 

184 ---------- 

185 u, v : array, unit = :math:`\lambda` 

186 u and v coordinates of observations 

187 vis : array, unit = Jy 

188 Observed visibilities (complex: real + imag * 1j) 

189 weights : array, unit = Jy^-2 

190 Weights assigned to observed visibilities, of the form 

191 :math:`1 / \sigma^2` 

192 sol : _HankelRegressor object 

193 Reconstructed profile using Maximum a posteriori power spectrum 

194 (see frank.radial_fitters.FrankFitter) 

195 bin_widths : list, unit = \lambda 

196 Bin widths in which to bin the observed visibilities 

197 dist : float, optional, unit = AU, default = None 

198 Distance to source, used to show second x-axis in [AU] 

199 logx : bool, default = False 

200 Whether to plot the visibility distributions in log(baseline) 

201 gamma : float, default = 1.0 

202 Index of power law normalization to apply to swept profile image's 

203 colormap (see matplotlib.colors.PowerNorm). 

204 gamma=1.0 yields a linear colormap 

205 force_style: bool, default = True 

206 Whether to use preconfigured matplotlib rcParams in generated figure 

207 save_prefix : string, default = None 

208 Prefix for saved figure name. If None, the figure won't be saved 

209 stretch : string, default = 'power' 

210 Transformation to apply to the colorscale. The default 'power' is a 

211 power law stretch. The other option is 'asinh', an arcsinh stretch, 

212 which requires astropy.visualization.mpl_normalize.simple_norm 

213 asinh_a : float, default = 0.02 

214 Scale parameter for an asinh stretch 

215 figsize : tuple = (width, height) of figure, unit = inch 

216 

217 Returns 

218 ------- 

219 fig : Matplotlib `.Figure` instance 

220 The produced figure, including the GridSpec 

221 axes : Matplotlib `~.axes.Axes` class 

222 The axes of the produced figure 

223 """ 

224 

225 logging.info(' Making quick figure') 

226 

227 Rmax, N, alpha, wsmooth, p0 = sol._info["Rmax"], sol._info["N"], \ 

228 sol._info["alpha"], sol._info["wsmooth"], sol._info["p0"] 

229 

230 with frank_plotting_style_context_manager(force_style): 

231 gs = GridSpec(3, 2, hspace=0, bottom=.12) 

232 gs2 = GridSpec(3, 2, hspace=.2) 

233 fig = plt.figure(figsize=figsize) 

234 fig.suptitle(r'Fit hyperparameters: $\alpha$={:.2f}, $w_{{smooth}}$={:.1e}, R$_{{max}}$={:.2f}, ' 

235 'N={:0d}, p$_0$={:.0e}'.format(alpha, wsmooth, Rmax, N, p0)) 

236 

237 ax0 = fig.add_subplot(gs[0]) 

238 ax1 = fig.add_subplot(gs[2]) 

239 

240 ax2 = fig.add_subplot(gs[1]) 

241 ax3 = fig.add_subplot(gs[3]) 

242 

243 ax4 = fig.add_subplot(gs2[4]) 

244 ax5 = fig.add_subplot(gs2[5]) 

245 

246 ax0.text(.5, .9, 'a)', transform=ax0.transAxes) 

247 ax1.text(.5, .9, 'b)', transform=ax1.transAxes) 

248 

249 ax2.text(.5, .9, 'c)', transform=ax2.transAxes) 

250 ax3.text(.5, .9, 'd)', transform=ax3.transAxes) 

251 

252 ax4.text(.5, .9, 'e)', c='w', transform=ax4.transAxes) 

253 ax5.text(.5, .9, 'f)', c='w', transform=ax5.transAxes) 

254 

255 axes = [ax0, ax1, ax2, ax3, ax4, ax5] 

256 

257 plot_brightness_profile(sol.r, sol.I / 1e10, ax0, c='r', label='frank') 

258 

259 if dist: 

260 ax0_5 = plot_brightness_profile(sol.r, sol.I / 1e10, ax0, dist=dist, c='r') 

261 xlims = ax0.get_xlim() 

262 ax0_5.set_xlim(np.multiply(xlims, dist)) 

263 

264 plot_brightness_profile(sol.r, sol.I / 1e10, ax1, c='r', label='frank') 

265 

266 if hasattr(sol, 'nonneg'): 

267 plot_brightness_profile(sol.r, sol.nonneg / 1e10, ax0, ls='--', c='#009933', label='non-neg.') 

268 plot_brightness_profile(sol.r, sol.nonneg / 1e10, ax1, ls='--', c='#009933', label='non-neg.') 

269 

270 

271 u_deproj, v_deproj, vis_deproj = sol.geometry.apply_correction(u, v, vis) 

272 baselines = (u_deproj**2 + v_deproj**2)**.5 

273 grid = np.logspace(np.log10(min(baselines.min(), sol.q[0])), 

274 np.log10(max(baselines.max(), sol.q[-1])), 

275 10**4) 

276 

277 for i in range(len(bin_widths)): 

278 binned_vis = UVDataBinner( 

279 baselines, vis_deproj, weights, bin_widths[i]) 

280 vis_re = binned_vis.V.real * 1e3 

281 vis_err_re = binned_vis.error.real * 1e3 

282 vis_fit = sol.predict_deprojected(binned_vis.uv).real * 1e3 

283 

284 for ax in [ax2, ax3]: 

285 plot_vis_quantity(binned_vis.uv / 1e6, vis_re, ax, 

286 vis_err_re, c=cs[i], 

287 marker=ms[i], ls='None', 

288 label=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

289 

290 vis_fit = sol.predict_deprojected(grid).real * 1e3 

291 plot_vis_quantity(grid / 1e6, vis_fit, ax2, c='r', label='frank', zorder=10) 

292 

293 plot_vis_quantity(grid / 1e6, vis_fit, ax3, c='r', label='frank', zorder=10) 

294 

295 if hasattr(sol, 'nonneg'): 

296 vis_fit_nonneg = sol.predict_deprojected(grid, I=sol.nonneg).real * 1e3 

297 plot_vis_quantity(grid / 1e6, vis_fit_nonneg, ax2, ls='--', c='#009933', label='non-neg.', zorder=10) 

298 plot_vis_quantity(grid / 1e6, vis_fit_nonneg, ax3, ls='--', c='#009933', label='non-neg.', zorder=10) 

299 

300 # Make a guess of good y-bounds for zooming in on the visibility fit 

301 zoom_ylim_guess = vis_fit.mean() 

302 zoom_bounds = [-1.5 * zoom_ylim_guess, 1.5 * zoom_ylim_guess] 

303 ax3.set_ylim(zoom_bounds) 

304 

305 vmax = sol.I.max() 

306 if stretch == 'asinh': 

307 vmin = max(0, min(sol.I)) 

308 from astropy.visualization.mpl_normalize import simple_norm 

309 norm = simple_norm(sol.I, stretch='asinh', asinh_a=asinh_a, 

310 min_cut=vmin, max_cut=vmax) 

311 elif stretch == 'power': 

312 vmin = 0 

313 norm = PowerNorm(gamma, vmin, vmax) 

314 else: 

315 err = ValueError("Unknown 'stretch'. Should be one of 'power' or 'asinh'") 

316 raise err 

317 

318 plot_2dsweep(sol.r, sol.I, ax=ax4, cmap='inferno', norm=norm, 

319 project=False) 

320 plot_2dsweep(sol.r, sol.I, ax=ax5, cmap='inferno', norm=norm, 

321 project=True, geom=sol.geometry) 

322 

323 ax1.set_xlabel('r ["]') 

324 ax0.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

325 ax1.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

326 ax1.set_yscale('log') 

327 ax1.set_ylim(sol.I[sol.I > 0].min() * 0.9 / 1e10, sol.I.max() * 1.1 / 1e10) 

328 

329 ax3.set_xlabel(r'Baseline [M$\lambda$]') 

330 ax2.set_ylabel('Re(V) [mJy]') 

331 ax3.set_ylabel('Re(V) [mJy]') 

332 

333 if logx: 

334 ax2.set_xscale('log') 

335 ax3.set_xscale('log') 

336 

337 ax2.set_xlim(right=max(baselines) / 1e6 * 1.2) 

338 else: 

339 ax2.set_xlim(0, max(baselines) / 1e6 * 1.2) 

340 xlims = ax2.get_xlim() 

341 ax3.set_xlim(xlims) 

342 

343 ax4.set_xlabel('RA offset ["]') 

344 ax4.set_ylabel('Dec offset ["]') 

345 ax5.set_xlabel('RA offset ["]') 

346 ax5.set_ylabel('Dec offset ["]') 

347 

348 plt.setp(ax0.get_xticklabels(), visible=False) 

349 plt.setp(ax2.get_xticklabels(), visible=False) 

350 

351 if save_prefix: 

352 plt.savefig(save_prefix + '_frank_fit_quick.png', dpi=300, 

353 bbox_inches='tight') 

354 plt.close() 

355 

356 return fig, axes 

357 

358 

359def make_full_fig(u, v, vis, weights, sol, bin_widths, 

360 dist=None, logx=False, force_style=True, 

361 save_prefix=None, norm_residuals=False, stretch='power', 

362 gamma=1.0, asinh_a=0.02, figsize=(8, 6)): 

363 r""" 

364 Produce a figure showing a Frankenstein fit and some useful diagnostics 

365 

366 Parameters 

367 ---------- 

368 u, v : array, unit = :math:`\lambda` 

369 u and v coordinates of observations 

370 vis : array, unit = Jy 

371 Observed visibilities (complex: real + imag * 1j) 

372 weights : array, unit = Jy^-2 

373 Weights assigned to observed visibilities, of the form 

374 :math:`1 / \sigma^2` 

375 sol : _HankelRegressor object 

376 Reconstructed profile using maximum a posteriori power spectrum 

377 (see frank.radial_fitters.FrankFitter) 

378 bin_widths : list, unit = \lambda 

379 Bin widths in which to bin the observed visibilities 

380 dist : float, optional, unit = AU, default = None 

381 Distance to source, used to show second x-axis in [AU] 

382 logx : bool, default = False 

383 Whether to plot the visibility distributions in log(baseline) 

384 force_style: bool, default = True 

385 Whether to use preconfigured matplotlib rcParams in generated figure 

386 save_prefix : string, default = None 

387 Prefix for saved figure name. If None, the figure won't be saved 

388 norm_residuals : bool, default = False 

389 Whether to normalize the residual visibilities by the data's 

390 visibility amplitudes 

391 stretch : string, default = 'power' 

392 Transformation to apply to the colorscale. The default 'power' is a 

393 power law stretch. The other option is 'asinh', an arcsinh stretch, 

394 which requires astropy.visualization.mpl_normalize.simple_norm 

395 gamma : float, default = 1.0 

396 Index of power law normalization to apply to swept profile image's 

397 colormap (see matplotlib.colors.PowerNorm). 

398 gamma=1.0 yields a linear colormap 

399 asinh_a : float, default = 0.02 

400 Scale parameter for an asinh stretch 

401 figsize : tuple = (width, height) of figure, unit = inch 

402 

403 Returns 

404 ------- 

405 fig : Matplotlib `.Figure` instance 

406 The produced figure, including the GridSpec 

407 axes : Matplotlib `~.axes.Axes` class 

408 The axes of the produced figure 

409 """ 

410 

411 logging.info(' Making full figure') 

412 

413 Rmax, N, alpha, wsmooth, p0 = sol._info["Rmax"], sol._info["N"], \ 

414 sol._info["alpha"], sol._info["wsmooth"], sol._info["p0"] 

415 

416 with frank_plotting_style_context_manager(force_style): 

417 gs = GridSpec(3, 3, hspace=0, wspace=0.25, left=0.06, right=0.98) 

418 gs1 = GridSpec(4, 3, hspace=0, wspace=0.25, left=0.06, right=0.98) 

419 gs2 = GridSpec(3, 3, hspace=.35, wspace=0.25, left=0.06, right=0.98) 

420 fig = plt.figure(figsize=figsize) 

421 fig.suptitle(r'Fit hyperparameters: $\alpha$={:.2f}, $w_{{smooth}}$={:.1e}, R$_{{max}}$={:.2f}, ' 

422 'N={:0d}, p$_0$={:.0e}'.format(alpha, wsmooth, Rmax, N, p0)) 

423 

424 ax0 = fig.add_subplot(gs[0]) 

425 ax1 = fig.add_subplot(gs[3]) 

426 ax2 = fig.add_subplot(gs2[6]) 

427 

428 ax3 = fig.add_subplot(gs[1]) 

429 ax4 = fig.add_subplot(gs[4]) 

430 ax5 = fig.add_subplot(gs[7]) 

431 

432 ax6 = fig.add_subplot(gs1[2]) 

433 ax7 = fig.add_subplot(gs1[5]) 

434 ax8 = fig.add_subplot(gs1[8]) 

435 ax9 = fig.add_subplot(gs1[11]) 

436 

437 ax0.text(.9, .6, 'a)', transform=ax0.transAxes) 

438 ax1.text(.9, .6, 'b)', transform=ax1.transAxes) 

439 ax2.text(.1, .9, 'c)', c='w', transform=ax2.transAxes) 

440 

441 ax3.text(.9, .5, 'd)', transform=ax3.transAxes) 

442 ax4.text(.9, .1, 'e)', transform=ax4.transAxes) 

443 ax5.text(.9, .1, 'f)', transform=ax5.transAxes) 

444 ax6.text(.9, .1, 'g)', transform=ax6.transAxes) 

445 ax7.text(.9, .9, 'h)', transform=ax7.transAxes) 

446 ax8.text(.9, .1, 'i)', transform=ax8.transAxes) 

447 ax9.text(.9, .1, 'j)', transform=ax9.transAxes) 

448 

449 axes = [ax0, ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9] 

450 

451 # Plot the fitted brightness profile in linear- and log-y 

452 plot_brightness_profile(sol.r, sol.I / 1e10, ax0, c='r', label='frank') 

453 

454 if dist: 

455 ax0_5 = plot_brightness_profile(sol.r, sol.I / 1e10, ax0, dist=dist, c='r') 

456 xlims = ax0.get_xlim() 

457 ax0_5.set_xlim(np.multiply(xlims, dist)) 

458 

459 plot_brightness_profile(sol.r, sol.I / 1e10, ax1, c='r', label='frank') 

460 

461 if hasattr(sol, 'nonneg'): 

462 plot_brightness_profile(sol.r, sol.nonneg / 1e10, ax0, ls='--', c='#009933', label='non-neg.') 

463 plot_brightness_profile(sol.r, sol.nonneg / 1e10, ax1, ls='--', c='#009933', label='non-neg.') 

464 

465 # Apply deprojection to the provided (u, v) coordinates 

466 # and visibility amplitudes 

467 u_deproj, v_deproj, vis_deproj = sol.geometry.apply_correction(u, v, vis) 

468 baselines = (u_deproj**2 + v_deproj**2)**.5 

469 # Set a grid of baselines on which to plot the visibility domain frank fit 

470 grid = np.logspace(np.log10(min(baselines.min(), sol.q[0])), 

471 np.log10(max(baselines.max(), sol.q[-1])), 10**4 

472 ) 

473 

474 # Bin the observed (real and imaginary components of the) visibilities 

475 # for plotting 

476 for i in range(len(bin_widths)): 

477 binned_vis = UVDataBinner(baselines, vis_deproj, weights, bin_widths[i]) 

478 vis_re = binned_vis.V.real * 1e3 

479 vis_im = binned_vis.V.imag * 1e3 

480 vis_err_re = binned_vis.error.real * 1e3 

481 vis_err_im = binned_vis.error.imag * 1e3 

482 vis_fit = sol.predict_deprojected(binned_vis.uv).real * 1e3 

483 if hasattr(sol, 'nonneg'): 

484 vis_fit_nn = sol.predict_deprojected(binned_vis.uv, I=sol.nonneg).real * 1e3 

485 

486 # Determine the visiblity domain frank fit residuals (and RMS error) 

487 # for Real(V) 

488 resid = vis_re - vis_fit 

489 if norm_residuals: 

490 resid /= vis_re 

491 rmse = (np.mean(resid**2))**.5 

492 if hasattr(sol, 'nonneg'): 

493 resid_nn = vis_re - vis_fit_nn 

494 if norm_residuals: 

495 resid_nn /= vis_re 

496 rmse_nn = (np.mean(resid_nn**2))**.5 

497 

498 # Plot the observed, binned visibilities (with errorbars) and the residuals 

499 plot_vis_quantity(binned_vis.uv / 1e6, vis_re, ax3, c=cs[i], 

500 marker=ms[i], ls='None', 

501 label=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

502 

503 plot_vis_quantity(binned_vis.uv / 1e6, vis_re, ax4, c=cs[i], 

504 marker=ms[i], ls='None', 

505 label=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

506 

507 plot_vis_quantity(binned_vis.uv / 1e6, vis_im, ax6, c=cs[i], 

508 marker=ms[i], ls='None', 

509 label=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

510 

511 plot_vis_quantity(binned_vis.uv / 1e6, resid, ax5, c='r', marker=ms[i], ls='None', 

512 label=r'{:.0f} k$\lambda$ bins, RMSE {:.3f} mJy'.format(bin_widths[i]/1e3, rmse)) 

513 

514 if hasattr(sol, 'nonneg'): 

515 plot_vis_quantity(binned_vis.uv / 1e6, resid_nn, ax5, c='#009933', marker=ms[i], ls='None', 

516 label=r'{:.0f} k$\lambda$ bins, RMSE {:.3f} mJy'.format(bin_widths[i]/1e3, rmse_nn)) 

517 

518 # Plot a histogram of the observed visibilties to examine how the 

519 # visibility count varies with baseline 

520 plot_vis_hist(binned_vis, ax8, rescale=1e6, color=hist_cs[i], 

521 label=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

522 # Plot the binned data signal-to-noise as a function of baseline 

523 plot_vis_quantity(binned_vis.uv / 1e6, 

524 binned_vis.V.real**2 / binned_vis.error.real**2, 

525 ax9, color=hist_cs[i], marker=ms[i], ls='None', 

526 label=r'{:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

527 

528 # Map the frank visibility fit to `grid`, considering only the real component 

529 # that frank fits 

530 vis_fit = sol.predict_deprojected(grid).real * 1e3 

531 

532 # Plot the visibility domain frank fit 

533 plot_vis_quantity(grid / 1e6, vis_fit, ax3, c='r', label='frank') 

534 plot_vis_quantity(grid / 1e6, vis_fit, ax4, c='r', label='frank') 

535 

536 if hasattr(sol, 'nonneg'): 

537 vis_fit_nonneg = sol.predict_deprojected(grid, I=sol.nonneg).real * 1e3 

538 plot_vis_quantity(grid / 1e6, vis_fit_nonneg, ax3, ls='--', c='#009933', label='non-neg.') 

539 plot_vis_quantity(grid / 1e6, vis_fit_nonneg, ax4, ls='--', c='#009933', label='non-neg.') 

540 

541 # Make a guess of good y-bounds for zooming in on the visibility fit 

542 zoom_ylim_guess = vis_fit.mean() 

543 zoom_bounds = [-1.5 * zoom_ylim_guess, 1.5 * zoom_ylim_guess] 

544 ax4.set_ylim(zoom_bounds) 

545 

546 # Plot the frank inferred power spectrum 

547 plot_vis_quantity(sol.q / 1e6, sol.power_spectrum, ax7, c='k') 

548 

549 # Plot a sweep over 2\pi of the frank 1D fit 

550 # (analogous to a model image of the source) 

551 vmax = sol.I.max() 

552 if stretch == 'asinh': 

553 vmin = max(0, min(sol.I)) 

554 from astropy.visualization.mpl_normalize import simple_norm 

555 norm = simple_norm(sol.I, stretch='asinh', asinh_a=asinh_a, 

556 min_cut=vmin, max_cut=vmax) 

557 elif stretch == 'power': 

558 vmin = 0 

559 norm = PowerNorm(gamma, vmin, vmax) 

560 else: 

561 err = ValueError("Unknown 'stretch'. Should be one of 'power' or 'asinh'") 

562 raise err 

563 

564 plot_2dsweep(sol.r, sol.I, ax=ax2, cmap='inferno', norm=norm, 

565 project=True, geom=sol.geometry) 

566 

567 ax1.set_xlabel('r ["]') 

568 ax0.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

569 ax1.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

570 ax1.set_yscale('log') 

571 ax1.set_ylim(sol.I[sol.I > 0].min() * 0.9 / 1e10, sol.I.max() * 1.1 / 1e10) 

572 ax2.set_xlabel('RA offset ["]') 

573 ax2.set_ylabel('Dec offset ["]') 

574 

575 axs = [ax3, ax4, ax5, ax6, ax7, ax8, ax9] 

576 if logx: 

577 for aa in axs: 

578 aa.set_xscale('log') 

579 ax3.set_xlim(right=max(baselines) / 1e6 * 1.2) 

580 else: 

581 ax3.set_xlim(0, max(baselines) / 1e6 * 1.2) 

582 xlims = ax3.get_xlim() 

583 for aa in axs: 

584 aa.set_xlim(xlims) 

585 

586 ax7.set_yscale('log') 

587 ax8.set_yscale('log') 

588 

589 ax8.set_yticks([1, 10, 1e2, 1e3]) 

590 

591 ax9.set_yscale('log') 

592 ax9.set_ylim(1e-2,200) 

593 ax9.set_yticks([1e-2, 1e-1, 1, 10, 100]) 

594 ax9.axhline(y=1, ls='--', c='c') 

595 

596 ax3.set_ylabel('Re(V) [mJy]') 

597 ax4.set_ylabel('Re(V) [mJy]') 

598 if norm_residuals: 

599 ax5.set_ylabel('Norm. residual') 

600 else: 

601 ax5.set_ylabel('Residual [mJy]') 

602 ax5.set_xlabel(r'Baseline [M$\lambda$]') 

603 

604 ax6.set_ylabel('Im(V) [mJy]') 

605 ax7.set_ylabel(r'Power [Jy$^2$]') 

606 ax8.set_ylabel('Count') 

607 ax9.set_ylabel(r'SNR=$\mu_{\rm bin}^2 / \sigma_{\rm bin}^2$') 

608 ax9.set_xlabel(r'Baseline [M$\lambda$]') 

609 

610 axs = [ax0, ax3, ax4, ax6, ax7, ax8] 

611 for aa in axs: 

612 plt.setp(aa.get_xticklabels(), visible=False) 

613 

614 if save_prefix: 

615 plt.savefig(save_prefix + '_frank_fit_full.png', dpi=300) 

616 plt.close() 

617 

618 return fig, axes 

619 

620 

621def make_diag_fig(r, q, iteration_diagnostics, iter_plot_range=None, logx=False, 

622 force_style=True, save_prefix=None, figsize=(8, 6)): 

623 r""" 

624 Produce a diagnostic figure showing fit convergence metrics 

625 

626 Parameters 

627 ---------- 

628 r : array 

629 Radial data coordinates at which the brightness profile is defined. 

630 The assumed unit (for the x-label) is arcsec 

631 q : array 

632 Baselines at which the power spectrum is defined. 

633 The assumed unit (for the x-label) is :math:`\lambda` 

634 iteration_diagnostics : dict 

635 The iteration_diagnostics from FrankFitter. 

636 iter_plot_range : list 

637 Range of iterations in the fit over which to 

638 plot brightness profile and power spectrum reconstructions 

639 logx : bool, default = False 

640 Whether to plot the visibility distributions in log(baseline) 

641 force_style: bool, default = True 

642 Whether to use preconfigured matplotlib rcParams in generated figure 

643 save_prefix : string, default = None 

644 Prefix for saved figure name. If None, the figure won't be saved 

645 figsize : tuple = (width, height) of figure, unit = inch 

646 

647 Returns 

648 ------- 

649 fig : Matplotlib `.Figure` instance 

650 The produced figure, including the GridSpec 

651 axes : Matplotlib `~.axes.Axes` class 

652 The axes of the produced figure 

653 """ 

654 

655 logging.info(' Making diagnostic figure') 

656 

657 if iter_plot_range is None: 

658 logging.info(" diag_plot is 'true' in your parameter file but" 

659 " iter_plot_range is 'null' --> Defaulting to" 

660 " plotting all iterations") 

661 

662 iter_plot_range = [0, iteration_diagnostics['num_iterations']] 

663 

664 else: 

665 if iter_plot_range[0] > iteration_diagnostics['num_iterations']: 

666 logging.info(' iter_plot_range[0] in your parameter file' 

667 ' exceeds the number of fit iterations -->' 

668 ' Defaulting to plotting all iterations') 

669 

670 iter_plot_range = [0, iteration_diagnostics['num_iterations']] 

671 

672 with frank_plotting_style_context_manager(force_style): 

673 gs = GridSpec(2, 2, hspace=0, bottom=.35) 

674 gs2 = GridSpec(3, 2, hspace=0, top=.7) 

675 fig = plt.figure(figsize=figsize) 

676 

677 ax0 = fig.add_subplot(gs[0]) 

678 ax1 = fig.add_subplot(gs[2]) 

679 

680 ax2 = fig.add_subplot(gs[1]) 

681 ax3 = fig.add_subplot(gs[3]) 

682 

683 ax4 = fig.add_subplot(gs2[4]) 

684 

685 ax0.text(.92, .5, 'a)', transform=ax0.transAxes) 

686 ax1.text(.92, .1, 'b)', transform=ax1.transAxes) 

687 

688 ax2.text(.05, .5, 'c)', transform=ax2.transAxes) 

689 ax3.text(.92, .5, 'd)', transform=ax3.transAxes) 

690 

691 ax4.text(.92, .9, 'e)', transform=ax4.transAxes) 

692 

693 axes = [ax0, ax1, ax2, ax3, ax4] 

694 

695 profile_iter = iteration_diagnostics['MAP'] 

696 profile_iter_toplot = [x / 1e10 for x in profile_iter] 

697 pwr_spec_iter = iteration_diagnostics['power_spectrum'] 

698 num_iter = iteration_diagnostics['num_iterations'] 

699 

700 plot_iterations(r, profile_iter_toplot, iter_plot_range, ax0) 

701 

702 # Plot the difference in the profile between the last 100 iterations 

703 iter_plot_range_end = [max(iter_plot_range[1] - 100, 0), 

704 iter_plot_range[1] - 1] 

705 

706 plot_iterations(r, np.diff(profile_iter_toplot, axis=0), 

707 iter_plot_range_end, ax1, 

708 cmap=plt.cm.cividis) # pylint: disable=no-member 

709 

710 plot_iterations(q, pwr_spec_iter, iter_plot_range, ax2) 

711 

712 # Plot the difference in the power spectrum between the last 100 iterations 

713 plot_iterations(q, abs(np.diff(pwr_spec_iter, axis=0)), 

714 iter_plot_range_end, ax3, 

715 cmap=plt.cm.cividis) # pylint: disable=no-member 

716 

717 plot_convergence_criterion(profile_iter_toplot, num_iter, ax4, c='k') 

718 

719 ax0.set_ylabel(r'I [$10^{10}$ Jy sr$^{-1}$]') 

720 ax1.set_ylabel(r'$I_i - I_{i-1}$ [$10^{10}$ Jy sr$^{-1}$]') 

721 ax1.set_xlabel('r ["]') 

722 

723 ax2.set_ylabel(r'Power [Jy$^2$]') 

724 ax3.set_ylabel(r'|PS$_i$ - PS$_{i-1}$| [Jy$^2$]') 

725 ax3.set_xlabel(r'Baseline [$\lambda$]') 

726 if logx: 

727 ax2.set_xscale('log') 

728 ax3.set_xscale('log') 

729 ax2.set_yscale('log') 

730 ax3.set_yscale('log') 

731 

732 ax4.set_xlabel('Fit iteration') 

733 ax4.set_ylabel('Convergence criterion,\n' + 

734 r'max(|$I_i - I_{i-1}$|) / max($I_i$)') 

735 ax4.set_yscale('log') 

736 

737 xlims = ax2.get_xlim() 

738 ax3.set_xlim(xlims) 

739 

740 plt.setp(ax0.get_xticklabels(), visible=False) 

741 plt.setp(ax2.get_xticklabels(), visible=False) 

742 

743 if save_prefix: 

744 plt.savefig(save_prefix + '_frank_fit_diag.png', dpi=300, 

745 bbox_inches='tight') 

746 plt.close() 

747 

748 return fig, axes, iter_plot_range 

749 

750 

751def make_clean_comparison_fig(u, v, vis, weights, sol, clean_profile, 

752 bin_widths, stretch='power', 

753 gamma=1.0, asinh_a=0.02, MAP_convolved=None, 

754 dist=None, logx=False, logy=False, ylims=None, 

755 force_style=True, save_prefix=None, 

756 figsize=(8, 10)): 

757 r""" 

758 Produce a figure comparing a frank fit to a CLEAN fit, in real space by 

759 convolving the frank fit with the CLEAN beam, and in visibility space by 

760 taking the discrete Hankel transform of the CLEAN profile 

761 

762 Parameters 

763 ---------- 

764 u, v : array, unit = :math:`\lambda` 

765 u and v coordinates of observations 

766 vis : array, unit = Jy 

767 Observed visibilities (complex: real + imag * 1j) 

768 weights : array, unit = Jy^-2 

769 Weights assigned to observed visibilities, of the form 

770 :math:`1 / \sigma^2` 

771 sol : _HankelRegressor object 

772 Reconstructed profile using Maximum a posteriori power spectrum 

773 (see frank.radial_fitters.FrankFitter) 

774 clean_profile : dict 

775 Dictionary with entries 'r' for the radial points [arcsec], 

776 'I' for the brightness [Jy / sr], and optionally the negative and positive 

777 brightness uncertainties 'lo_err' and 'hi_err' [Jy / sr]. If only the 

778 negative uncertainty is provided, the positive uncertainty is assumed 

779 equal to it 

780 bin_widths : list, unit = \lambda 

781 Bin widths in which to bin the observed visibilities 

782 stretch : string, default = 'power' 

783 Transformation to apply to the colorscale. The default 'power' is a 

784 power law stretch. The other option is 'asinh', an arcsinh stretch, 

785 which requires astropy.visualization.mpl_normalize.simple_norm 

786 gamma : float, default = 1.0 

787 Index of power law normalization to apply to swept profile image's 

788 colormap (see matplotlib.colors.PowerNorm). 

789 gamma=1.0 yields a linear colormap 

790 asinh_a : float, default = 0.02 

791 Scale parameter for an asinh stretch 

792 MAP_convolved : None (default) or array, unit = Jy / sr 

793 frank brightness profile convolved with a CLEAN beam 

794 (see utilities.convolve_profile). 

795 The assumed unit is for the x-label 

796 dist : float, optional, unit = AU, default = None 

797 Distance to source, used to show second x-axis in [AU] 

798 logx : bool, default = False 

799 Whether to plot the visibility distributions in log(baseline) 

800 logy : bool, default = False 

801 Whether to plot the visibility distributions in log(flux) 

802 ylims : list of len(2), default = None 

803 Lower and upper y-bounds for the visibility domain plot 

804 force_style: bool, default = True 

805 Whether to use preconfigured matplotlib rcParams in generated figure 

806 save_prefix : string, default = None 

807 Prefix for saved figure name. If None, the figure won't be saved 

808 figsize : tuple = (width, height) of figure, unit = inch 

809 

810 Returns 

811 ------- 

812 fig : Matplotlib `.Figure` instance 

813 The produced figure, including the GridSpec 

814 axes : Matplotlib `~.axes.Axes` class 

815 The axes of the produced figure 

816 """ 

817 

818 logging.info(' Making CLEAN comparison figure') 

819 

820 with frank_plotting_style_context_manager(force_style): 

821 gs = GridSpec(3, 1) 

822 gs2 = GridSpec(3, 3) 

823 

824 fig = plt.figure(figsize=figsize) 

825 

826 ax0 = fig.add_subplot(gs[0]) 

827 ax1 = fig.add_subplot(gs[1]) 

828 

829 ax2 = fig.add_subplot(gs2[6]) 

830 ax3 = fig.add_subplot(gs2[7]) 

831 ax4 = fig.add_subplot(gs2[8]) 

832 

833 axes = [ax0, ax1, ax2, ax3, ax4] 

834 

835 if 'lo_err' in clean_profile.keys(): 

836 low_uncer = clean_profile['lo_err'] 

837 else: 

838 low_uncer = None 

839 if 'hi_err' in clean_profile.keys(): 

840 high_uncer = clean_profile['hi_err'] 

841 else: 

842 high_uncer = None 

843 

844 plot_brightness_profile(clean_profile['r'], clean_profile['I'] / 1e10, ax0, 

845 low_uncer=low_uncer, high_uncer=high_uncer, 

846 c='b', ls='--', label='CLEAN') 

847 

848 plot_brightness_profile(sol.r, sol.I / 1e10, ax0, c='r', ls=':', label='frank') 

849 

850 if MAP_convolved is not None: 

851 plot_brightness_profile(sol.r, MAP_convolved / 1e10, ax0, c='k', ls='-', 

852 label='frank, convolved') 

853 

854 if dist: 

855 ax0_5 = plot_brightness_profile(sol.r, sol.I / 1e10, ax0, dist=dist, c='r', ls=':') 

856 

857 u_deproj, v_deproj, vis_deproj = sol.geometry.apply_correction(u, v, vis) 

858 baselines = (u_deproj**2 + v_deproj**2)**.5 

859 grid = np.logspace(np.log10(min(baselines.min(), sol.q[0])), 

860 np.log10(max(baselines.max(), sol.q[-1])), 10**4 

861 ) 

862 

863 for i in range(len(bin_widths)): 

864 binned_vis = UVDataBinner(baselines, vis_deproj, weights, bin_widths[i]) 

865 vis_re = binned_vis.V.real * 1e3 

866 vis_err_re = binned_vis.error.real * 1e3 

867 

868 if logy: 

869 lab=r'Obs.>0, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3) 

870 else: 

871 lab=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3) 

872 plot_vis_quantity(binned_vis.uv / 1e6, vis_re, ax1, c=cs[i], 

873 marker=ms[i], ls='None', 

874 label=lab) 

875 if logy: 

876 plot_vis_quantity(binned_vis.uv / 1e6, -vis_re, ax1, c=cs2[i], 

877 marker=ms[i], ls='None', 

878 label=r'Obs.<0, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

879 

880 vis_fit = sol.predict_deprojected(grid).real * 1e3 

881 

882 # Take the discrete Hankel transform of the CLEAN profile, using the same 

883 # collocation points for the DHT as those in the frank fit 

884 Inu_interp = np.interp( 

885 sol.r, clean_profile['r'], clean_profile['I'].real) * 1e3 

886 clean_DHT = sol.predict_deprojected(grid, I=Inu_interp) 

887 

888 if logy: 

889 lab = 'frank, >0' 

890 lab2 = 'DHT of CLEAN, >0' 

891 else: 

892 lab = 'frank' 

893 lab2 = 'DHT of CLEAN' 

894 plot_vis_quantity(grid / 1e6, vis_fit, ax1, c='r', label=lab) 

895 plot_vis_quantity(grid / 1e6, clean_DHT, ax1, c='b', label=lab2) 

896 if logy: 

897 plot_vis_quantity(grid / 1e6, -vis_fit, ax1, c='r', ls='--', label='frank, <0') 

898 plot_vis_quantity(grid / 1e6, -clean_DHT, ax1, c='b', ls='--', label='DHT of CLEAN, <0') 

899 

900 if logx: 

901 ax1.set_xscale('log') 

902 ax1.set_xlim(right=1.2 * max(baselines) / 1e6) 

903 

904 if logy: 

905 ax1.set_yscale('log') 

906 ax1.set_ylim(bottom=1e-3) 

907 if ylims: 

908 ax1.set_ylim(ylims) 

909 

910 if MAP_convolved is not None: 

911 vmax = max(sol.I.max(), MAP_convolved.max(), clean_profile['I'].max()) 

912 else: 

913 vmax = max(sol.I.max(), clean_profile['I'].max()) 

914 if stretch == 'asinh': 

915 vmin = max(0, min(sol.I)) 

916 from astropy.visualization.mpl_normalize import simple_norm 

917 norm = simple_norm(sol.I, stretch='asinh', asinh_a=asinh_a, 

918 min_cut=vmin, max_cut=vmax) 

919 elif stretch == 'power': 

920 vmin = 0 

921 norm = PowerNorm(gamma, vmin, vmax) 

922 else: 

923 err = ValueError("Unknown 'stretch'. Should be one of 'power' or 'asinh'") 

924 raise err 

925 

926 plot_2dsweep(sol.r, sol.I, ax=ax2, cmap='inferno', norm=norm, 

927 xmax=sol.Rmax, plot_colorbar=True) 

928 if MAP_convolved is not None: 

929 plot_2dsweep(sol.r, MAP_convolved, ax=ax3, cmap='inferno', norm=norm, 

930 xmax=sol.Rmax, plot_colorbar=True) 

931 

932 # Interpolate the CLEAN profile onto the frank grid to ensure the CLEAN 

933 # swept 'image' has the same pixel resolution as the frank swept 'images' 

934 from scipy.interpolate import interp1d 

935 interp = interp1d(clean_profile['r'], clean_profile['I']) 

936 regrid_I_clean = interp(sol.r) 

937 plot_2dsweep(sol.r, regrid_I_clean, ax=ax4, cmap='inferno', norm=norm, 

938 xmax=sol.Rmax, plot_colorbar=True) 

939 

940 ax0.legend(loc='best') 

941 ax1.legend(loc='best', ncol=2) 

942 

943 ax0.set_xlabel('r ["]') 

944 ax0.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

945 ax1.set_xlabel(r'Baseline [M$\lambda$]') 

946 ax1.set_ylabel(r'Re(V) [mJy]') 

947 ax2.set_xlabel('RA offset ["]') 

948 ax3.set_xlabel('RA offset ["]') 

949 ax4.set_xlabel('RA offset ["]') 

950 ax2.set_ylabel('Dec offset ["]') 

951 

952 ax0.set_xlim(right=sol.Rmax) 

953 if dist: 

954 xlims = ax0.get_xlim() 

955 ax0_5.set_xlim(np.multiply(xlims, dist)) 

956 

957 if logx: 

958 ax1.set_xscale('log') 

959 if logy: 

960 ax1.set_yscale('log') 

961 ax1.set_ylim(bottom=1e-3) 

962 

963 ax2.set_title('Unconvolved frank profile swept') 

964 ax3.set_title('Convolved frank profile swept') 

965 ax4.set_title('CLEAN profile swept') 

966 

967 ax0.text(.5, .9, 'a)', transform=ax0.transAxes) 

968 ax1.text(.5, .9, 'b)', transform=ax1.transAxes) 

969 ax2.text(.1, .9, 'c)', c='w', transform=ax2.transAxes) 

970 ax3.text(.1, .9, 'd)', c='w', transform=ax3.transAxes) 

971 ax4.text(.1, .9, 'e)', c='w', transform=ax4.transAxes) 

972 

973 if save_prefix: 

974 plt.savefig(save_prefix + '_frank_clean_comparison.png', dpi=300) 

975 plt.close() 

976 

977 return fig, axes 

978 

979 

980def make_multifit_fig(u, v, vis, weights, sols, bin_widths, dist=None, 

981 logx=False, force_style=True, 

982 save_prefix=None, figsize=(8, 8) 

983 ): 

984 r""" 

985 Produce a figure overplotting multiple fits 

986 

987 Parameters 

988 ---------- 

989 u, v : array, unit = :math:`\lambda` 

990 u and v coordinates of observations 

991 vis : array, unit = Jy 

992 Observed visibilities (complex: real + imag * 1j) 

993 weights : array, unit = Jy^-2 

994 Weights assigned to observed visibilities, of the form 

995 :math:`1 / \sigma^2` 

996 sols : list of _HankelRegressor objects 

997 Reconstructed profile using Maximum a posteriori power spectrum 

998 (see frank.radial_fitters.FrankFitter), for each of multiple fits 

999 bin_widths : list, unit = \lambda 

1000 Bin widths in which to bin the observed visibilities 

1001 dist : float, optional, unit = AU, default = None 

1002 Distance to source, used to show second x-axis in [AU] 

1003 logx : bool, default = False 

1004 Whether to plot the visibility distributions in log(baseline) 

1005 force_style: bool, default = True 

1006 Whether to use preconfigured matplotlib rcParams in generated figure 

1007 save_prefix : string, default = None 

1008 Prefix for saved figure name. If None, the figure won't be saved 

1009 figsize : tuple = (width, height) of figure, unit = inch 

1010 

1011 Returns 

1012 ------- 

1013 fig : Matplotlib `.Figure` instance 

1014 The produced figure, including the GridSpec 

1015 axes : Matplotlib `~.axes.Axes` class 

1016 The axes of the produced figure 

1017 """ 

1018 

1019 logging.info(' Making multifit figure') 

1020 

1021 with frank_plotting_style_context_manager(force_style): 

1022 gs = GridSpec(3, 2, hspace=0) 

1023 fig = plt.figure(figsize=figsize) 

1024 

1025 ax0 = fig.add_subplot(gs[0]) 

1026 ax1 = fig.add_subplot(gs[2]) 

1027 

1028 ax2 = fig.add_subplot(gs[1]) 

1029 ax3 = fig.add_subplot(gs[3]) 

1030 ax4 = fig.add_subplot(gs[5]) 

1031 

1032 ax0.text(.9, .4, 'a)', transform=ax0.transAxes) 

1033 ax1.text(.5, .9, 'b)', transform=ax1.transAxes) 

1034 

1035 ax2.text(.5, .9, 'c)', transform=ax2.transAxes) 

1036 ax3.text(.5, .9, 'd)', transform=ax3.transAxes) 

1037 ax4.text(.5, .9, 'e)', transform=ax4.transAxes) 

1038 

1039 axes = [ax0, ax1, ax2, ax3, ax4] 

1040 

1041 # Assume the fitted geometry and thus deprojected baseline distribution 

1042 # is the same for all fits, plotting the common dataset 

1043 u_deproj, v_deproj, vis_deproj = sols[0].geometry.apply_correction(u, v, vis) 

1044 baselines = (u_deproj**2 + v_deproj**2)**.5 

1045 grid = np.logspace(np.log10(min(baselines.min(), sols[0].q[0])), 

1046 np.log10(max(baselines.max(), sols[0].q[-1])), 

1047 10**4) 

1048 

1049 for i in range(len(bin_widths)): 

1050 binned_vis = UVDataBinner( 

1051 baselines, vis_deproj, weights, bin_widths[i]) 

1052 vis_re = binned_vis.V.real * 1e3 

1053 vis_err_re = binned_vis.error.real * 1e3 

1054 

1055 plot_vis_quantity(binned_vis.uv, vis_re, ax2, c=cs[i], 

1056 marker=ms[i], ls='None', label=r'Obs., {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

1057 

1058 plot_vis_quantity(binned_vis.uv, vis_re, ax3, c=cs[i], 

1059 marker=ms[i], ls='None', 

1060 label=r'Obs.>0, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

1061 plot_vis_quantity(binned_vis.uv, -vis_re, ax3, c=cs2[i], 

1062 marker=ms[i], ls='None', 

1063 label=r'Obs.<0, {:.0f} k$\lambda$ bins'.format(bin_widths[i]/1e3)) 

1064 

1065 # Overplot the multiple fits 

1066 for ii in range(len(sols)): 

1067 plot_brightness_profile(sols[ii].r, sols[ii].I / 1e10, ax0, c=multifit_cs[ii])#, 

1068 # label='alpha = {}, wsmooth = {}'.format(sols[ii].info['alpha'], sols[ii].info['wsmooth']) 

1069 if dist and ii == len(sols) - 1: 

1070 ax0_5 = plot_brightness_profile(sols[ii].r, sols[ii].I / 1e10, ax0, dist=dist, c=multifit_cs[ii]) 

1071 xlims = ax0.get_xlim() 

1072 ax0_5.set_xlim(np.multiply(xlims, dist)) 

1073 

1074 plot_brightness_profile(sols[ii].r, sols[ii].I / 1e10, ax1, c=multifit_cs[ii]) 

1075 

1076 vis_fit = sols[ii].predict_deprojected(grid).real * 1e3 

1077 plot_vis_quantity(grid, vis_fit, ax2, c=multifit_cs[ii]) 

1078 

1079 plot_vis_quantity(grid, vis_fit, ax3, c=multifit_cs[ii]) 

1080 plot_vis_quantity(grid, -vis_fit, ax3, c=multifit_cs[ii], ls='--') 

1081 

1082 plot_vis_quantity(sols[ii].q, sols[ii].power_spectrum, ax4, c=multifit_cs[ii]) 

1083 

1084 ax1.set_xlabel('r ["]') 

1085 ax0.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

1086 ax1.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

1087 ax1.set_yscale('log') 

1088 ax1.set_ylim(bottom=1e-3) 

1089 

1090 ax4.set_xlabel(r'Baseline [$\lambda$]') 

1091 ax2.set_ylabel('Re(V) [mJy]') 

1092 ax3.set_ylabel('Re(V) [mJy]') 

1093 ax4.set_ylabel(r'Power [Jy$^2$]') 

1094 if logx: 

1095 ax2.set_xscale('log') 

1096 ax3.set_xscale('log') 

1097 ax4.set_xscale('log') 

1098 ax3.set_yscale('log') 

1099 ax4.set_yscale('log') 

1100 ax3.set_ylim(bottom=1e-4) 

1101 

1102 xlims = ax2.get_xlim() 

1103 ax3.set_xlim(xlims) 

1104 ax4.set_xlim(xlims) 

1105 

1106 plt.setp(ax0.get_xticklabels(), visible=False) 

1107 plt.setp(ax2.get_xticklabels(), visible=False) 

1108 

1109 ax0.legend() 

1110 ax2.legend() 

1111 ax3.legend() 

1112 

1113 if save_prefix: 

1114 plt.savefig(save_prefix + '_frank_multifit.png', dpi=300) 

1115 plt.close() 

1116 

1117 return fig, axes 

1118 

1119 

1120def make_bootstrap_fig(r, profiles, force_style=True, 

1121 save_prefix=None, figsize=(8, 6)): 

1122 r""" 

1123 Produce a figure showing a bootstrap analysis for a Frankenstein fit 

1124 

1125 Parameters 

1126 ---------- 

1127 r : array, unit = arcsec 

1128 Single set of radial collocation points used in all bootstrap fits 

1129 profiles : array, unit = Jy / sr 

1130 Brightness profiles of all bootstrap fits 

1131 force_style: bool, default = True 

1132 Whether to use preconfigured matplotlib rcParams in generated figure 

1133 save_prefix : string, default = None 

1134 Prefix for saved figure name. If None, the figure won't be saved 

1135 figsize : tuple = (width, height) of figure, unit = inch 

1136 

1137 Returns 

1138 ------- 

1139 fig : Matplotlib `.Figure` instance 

1140 The produced figure, including the GridSpec 

1141 axes : Matplotlib `~.axes.Axes` class 

1142 The axes of the produced figure 

1143 """ 

1144 

1145 logging.info(' Making bootstrap summary figure') 

1146 

1147 with frank_plotting_style_context_manager(force_style): 

1148 gs = GridSpec(2, 2, hspace=0) 

1149 fig = plt.figure(figsize=figsize) 

1150 

1151 ax0 = fig.add_subplot(gs[0]) 

1152 ax1 = fig.add_subplot(gs[2]) 

1153 

1154 ax2 = fig.add_subplot(gs[1]) 

1155 ax3 = fig.add_subplot(gs[3]) 

1156 

1157 axes = [ax0, ax1, ax2, ax3] 

1158 

1159 ax0.text(.6, .9, 'a) Bootstrap: {} trials'.format(len(profiles)), 

1160 transform=ax0.transAxes) 

1161 ax1.text(.9, .7, 'b)', transform=ax1.transAxes) 

1162 ax2.text(.9, .9, 'c)', transform=ax2.transAxes) 

1163 ax3.text(.9, .7, 'd)', transform=ax3.transAxes) 

1164 

1165 mean_profile = np.mean(profiles, axis=0) 

1166 std = np.std(profiles, axis=0) 

1167 

1168 plot_brightness_profile(r, mean_profile / 1e10, ax1, low_uncer=std / 1e10, 

1169 color='r', alpha=.7, label='Stan. dev. of trials') 

1170 plot_brightness_profile(r, mean_profile / 1e10, ax3, low_uncer=std / 1e10, 

1171 color='r', alpha=.7, label='Stan. dev. of trials') 

1172 

1173 for i in range(len(profiles)): 

1174 plot_brightness_profile(r, profiles[i] / 1e10, ax0, c='k', alpha=.2) 

1175 

1176 plot_brightness_profile(r, profiles[i] / 1e10, ax2, c='k', alpha=.2) 

1177 

1178 plot_brightness_profile(r, mean_profile / 1e10, ax1, 

1179 c='#35F9E1', label='Mean of trials') 

1180 plot_brightness_profile(r, mean_profile / 1e10, ax3, 

1181 c='#35F9E1', label='Mean of trials') 

1182 

1183 ax0.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

1184 ax1.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

1185 ax2.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

1186 ax3.set_ylabel(r'Brightness [$10^{10}$ Jy sr$^{-1}$]') 

1187 ax1.set_xlabel('r ["]') 

1188 ax3.set_xlabel('r ["]') 

1189 

1190 ax2.set_yscale('log') 

1191 ax3.set_yscale('log') 

1192 ax2.set_ylim(bottom=1e-4) 

1193 ax3.set_ylim(bottom=1e-4) 

1194 

1195 if save_prefix: 

1196 plt.savefig(save_prefix + '_frank_bootstrap.png', dpi=300, 

1197 bbox_inches='tight') 

1198 plt.close() 

1199 

1200 return fig, axes