3 Statistics of jointcal vs. single-frame procesing and diagnostic plots. 5 NOTE: some of the algorithms and data structures in this code are temporary 6 kludges and will no longer be necessary once the following are available: 7 * a composite data structure that contains all ccds from a single visit 8 * an n-way matching system that preserves the separations between sources 10 from __future__
import division, print_function, absolute_import
11 from builtins
import zip
12 from builtins
import object
17 from astropy
import units
as u
25 MatchDict = collections.namedtuple(
'MatchDict', [
'relative',
'absolute'])
30 Compute statistics on jointcal-processed data, and optionally generate plots. 34 Instantiate JointcalStatistics and call compute_rms() to get the relevant 35 statistics for e.g. unittests, and call make_plots() to generate a suite of 39 def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
40 do_photometry=True, do_astrometry=True,
45 match_radius : lsst.afw.Angle 46 match sources within this radius for RMS statistics 48 Signal/Noise (flux/fluxSigma) for sources to be included in the RMS cross-match. 49 100 is a balance between good centroids and enough sources. 50 do_photometry : bool, optional 51 Perform calculations/make plots for photometric metrics. 52 do_astrometry : bool, optional 53 Perform calculations/make plots for astrometric metrics. 54 verbose : bool, optional 66 Match all data_refs to compute the RMS, for all detections above self.flux_limit. 70 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 71 A list of data refs to do the calculations between. 72 reference : lsst reference catalog 73 reference catalog to do absolute matching against. 79 Post-jointcal relative RMS of the matched sources. 81 Post-jointcal absolute RMS of matched sources. 83 Post-jointcal photometric repeatability (PA1 from the SRD). 87 self.
filters = [ref.get(
'calexp_filter').getName()
for ref
in data_refs]
90 def compute(catalogs, photoCalibs):
91 """Compute the relative and absolute matches in distance and flux.""" 101 refcalib = photoCalibs[0]
if photoCalibs != []
else None 102 dist_rel, flux_rel, ref_flux_rel, source_rel = self.
_make_match_dict(refcat,
106 dist_abs, flux_abs, ref_flux_abs, source_abs = self.
_make_match_dict(reference,
111 ref_flux =
MatchDict(ref_flux_rel, ref_flux_abs)
112 source =
MatchDict(source_rel, source_abs)
113 return dist, flux, ref_flux, source
115 old_cats = [ref.get(
'src')
for ref
in data_refs]
120 for ref
in data_refs:
121 calib = ref.get(
'calexp_calib')
122 fluxMag0 = calib.getFluxMag0()
125 self.old_dist, self.old_flux, self.old_ref_flux, self.
old_source = compute(old_cats, old_calibs)
128 new_cats = [ref.get(
'src')
for ref
in data_refs]
131 new_wcss = [ref.get(
'jointcal_wcs')
for ref
in data_refs]
134 new_calibs = [ref.get(
'jointcal_photoCalib')
for ref
in data_refs]
136 for wcs, cat
in zip(new_wcss, new_cats):
140 self.new_dist, self.new_flux, self.new_ref_flux, self.
new_source = compute(new_cats, new_calibs)
143 print(
'old, new relative distance matches:',
144 len(self.old_dist.relative), len(self.new_dist.relative))
145 print(
'old, new absolute distance matches:',
146 len(self.old_dist.absolute), len(self.new_dist.absolute))
147 print(
'old, new relative flux matches:',
148 len(self.old_flux.relative), len(self.new_flux.relative))
149 print(
'old, new absolute flux matches:',
150 len(self.old_flux.absolute), len(self.new_flux.absolute))
158 """Compute the total rms across all sources.""" 159 total = sum(sum(dd**2)
for dd
in data.values())
160 n = sum(len(dd)
for dd
in data.values())
161 return np.sqrt(total/n)
170 Rms_result = collections.namedtuple(
"Rms_result", [
"dist_relative",
"dist_absolute",
"pa1"])
174 name='', interactive=False, per_ccd_plot=False, outdir='.plots'):
176 Make plots of various quantites to help with debugging. 177 Requires that `compute_rms()` was run first. 181 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 182 A list of data refs to do the calculations between. 183 old_wcs_list : list of lsst.afw.image.wcs.Wcs 184 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 186 Name to include in plot titles and save files. 188 Turn on matplotlib interactive mode and drop into a debugger when 189 plotting is finished. Otherwise, use a non-interactive backend. 191 Plot the WCS per CCD (takes longer and generates many plots for a large camera) 193 directory to save plots to. 199 matplotlib.use(
'pdf')
201 import matplotlib.pyplot
as plt
202 import astropy.visualization
204 astropy.visualization.quantity_support()
208 self.
log.
info(
"N data_refs: %d", len(data_refs))
214 name=name, outdir=outdir)
216 def rms_per_source(data):
217 """Each element of data must already be the "delta" of whatever measurement.""" 218 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).to(u.arcsecond)
229 new_dist_rms.relative, new_dist_rms.absolute,
232 name=name, outdir=outdir)
235 per_ccd_plot=per_ccd_plot,
236 name=name, outdir=outdir)
245 Compute the photometric RMS and the photometric repeatablity values (PA1). 250 The minimum signal/noise for sources to be included in the PA1 calculation. 251 magnitude_range : float 252 The range of magnitudes above sn_cut to include in the PA1 calculation. 254 def rms(flux, ref_flux):
255 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
261 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
262 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
266 def signal_to_noise(sources, flux_key='slot_PsfFlux_flux', sigma_key='slot_PsfFlux_fluxSigma'):
267 """Compute the mean signal/noise per source from a MatchDict of SourceRecords.""" 268 result = np.empty(len(sources))
269 for i, src
in enumerate(sources.values()):
270 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
273 old_sn = signal_to_noise(self.
old_source.absolute)
288 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations. 292 reference : lsst.afw.table.SourceCatalog 293 Catalog to do the matching against. 294 visit_catalogs : list of lsst.afw.table.SourceCatalog 295 Visit source catalogs (values() produced by _make_visit_catalogs) 296 to cross-match against reference. 297 photoCalibs : list of lsst.afw.image.PhotoCalib 298 Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs. 299 refcalib : lsst.afw.image.PhotoCalib or None 300 Pass a PhotoCalib here to use it to compute Janskys from the 301 reference catalog ADU slot_flux. 306 dict of sourceID: array(separation distances for that source) 308 dict of sourceID: array(fluxes (Jy) for that source) 310 dict of sourceID: flux (Jy) of the reference object 312 dict of sourceID: list(each SourceRecord that was position-matched 316 if photoCalibs == []:
317 photoCalibs = [[]]*len(visit_catalogs)
319 distances = collections.defaultdict(list)
320 fluxes = collections.defaultdict(list)
322 sources = collections.defaultdict(list)
323 if 'slot_CalibFlux_flux' in reference.schema:
324 ref_flux_key =
'slot_CalibFlux' 326 ref_flux_key =
'{}_flux' 328 def get_fluxes(photoCalib, match):
329 """Return (flux, ref_flux) or None if either is invalid.""" 331 maggiesToJansky = 3631
332 flux = match[1][
'slot_CalibFlux_flux']
336 flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[1],
"slot_CalibFlux").value
339 if 'slot' in ref_flux_key:
340 ref_flux = match[0][ref_flux_key+
'_flux']
344 ref_flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[0], ref_flux_key).value
347 ref_flux = match[0][ref_flux_key.format(filt)]
351 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
352 return Flux(flux, ref_flux)
354 for cat, photoCalib, filt
in zip(visit_catalogs, photoCalibs, self.
filters):
355 good = (cat.get(
'base_PsfFlux_flux')/cat.get(
'base_PsfFlux_fluxSigma')) > self.
flux_limit 357 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
361 flux = get_fluxes(photoCalib, m)
365 fluxes[m[0].getId()].
append(flux.flux)
367 ref_fluxes[m[0].getId()] = flux.ref_flux
371 distances[m[0].getId()].
append(m[2])
373 sources[m[0].getId()].
append(m[1])
375 for source
in distances:
376 distances[source] = np.array(distances[source])
377 for source
in fluxes:
378 fluxes[source] = np.array(fluxes[source])
380 return distances, fluxes, ref_fluxes, sources
384 Merge all catalogs from the each visit. 385 NOTE: creating this structure is somewhat slow, and will be unnecessary 386 once a full-visit composite dataset is available. 390 catalogs : list of lsst.afw.table.SourceCatalog 391 Catalogs to combine into per-visit catalogs. 392 visits : list of visit id (usually int) 393 list of visit identifiers, one-to-one correspondent with catalogs. 398 dict of visit: catalog of all sources from all CCDs of that visit. 401 for v, cat
in zip(visits, catalogs):
402 visit_dict[v].extend(cat)
405 visit_dict[v] = visit_dict[v].copy(deep=
True)
411 faint, bright, old_PA1, new_PA1,
412 name='', outdir='.plots'):
413 """Plot various distributions of fluxes and magnitudes. 417 plt : matplotlib.pyplot instance 418 pyplot instance to plot with 423 old_weighted_rms : np.array 424 old rms weighted by the mean (rms(data)/mean(data)) 425 new_weighted_rms : np.array 426 old rms weighted by the mean (rms(data)/mean(data)) 428 Faint end of range that PA1 was computed from. 430 Bright end of range that PA1 was computed from. 432 Old value of PA1, to plot as horizontal line. 434 New value of PA1, to plot as horizontal line. 436 Name to include in plot titles and save files. 437 outdir : str, optional 438 Directory to write the saved plots to. 442 seaborn.set_style(
'whitegrid')
448 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
449 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
450 plt.axvline(faint, ls=
':', color=old_color)
451 plt.axvline(bright, ls=
':', color=old_color)
452 plt.axhline(old_PA1, ls=
'--', color=old_color)
453 plt.axhline(new_PA1, ls=
'--', color=new_color)
454 plt.legend(loc=
'upper left')
455 plt.title(
'Where is the systematic flux rms limit?')
456 plt.xlabel(
'magnitude')
457 plt.ylabel(
'rms/mean per source')
458 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
459 plt.savefig(filename.format(name))
462 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
463 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
464 plt.title(
'Source RMS pre/post-jointcal')
465 plt.xlabel(
'rms(flux)/mean(flux)')
467 plt.legend(loc=
'upper right')
468 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
469 plt.savefig(filename.format(name))
473 name='', outdir='.plots'):
475 Various plots of the difference between old and new Wcs. 479 plt : matplotlib.pyplot instance 480 pyplot instance to plot with. 481 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 482 A list of data refs to plot. 483 visits : list of visit id (usually int) 484 list of visit identifiers, one-to-one correspondent with catalogs. 485 old_wcs_list : list of lsst.afw.image.wcs.Wcs 486 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 487 per_ccd_plot : bool, optional 488 Make per-ccd plots of the "wcs different" (warning: slow!) 490 Name to include in plot titles and save files. 491 outdir : str, optional 492 Directory to write the saved plots to. 499 for i, ref
in enumerate(data_refs):
500 md = ref.get(
'calexp_md')
502 plot_wcs(plt, old_wcs_list[i], ref.get(
'jointcal_wcs'),
503 dims.getX(), dims.getY(),
504 center=(md.get(
'CRVAL1'), md.get(
'CRVAL2')), name=
'dataRef %d'%i,
509 """Return num x/y grid coordinates for wcs1 and wcs2.""" 510 x = np.linspace(0, x_dim, num)
511 y = np.linspace(0, y_dim, num)
514 return x1, y1, x2, y2
518 """Convert two arrays of x/y points into an on-sky grid.""" 519 xout = np.zeros((xv.shape[0], yv.shape[0]))
520 yout = np.zeros((xv.shape[0], yv.shape[0]))
521 for i, x
in enumerate(xv):
522 for j, y
in enumerate(yv):
523 sky = wcs.pixelToSky(x, y).toFk5()
524 xout[i, j] = sky.getRa()
525 yout[i, j] = sky.getDec()
531 Make quiver plots of the WCS deltas for each CCD in each visit. 535 plt : matplotlib.pyplot instance 536 pyplot instance to plot with. 537 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 538 A list of data refs to plot. 539 visits : list of visit id (usually int) 540 list of visit identifiers, one-to-one correspondent with catalogs. 541 old_wcs_list : list of lsst.afw.image.wcs.Wcs 542 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 544 Name to include in plot titles and save files. 545 outdir : str, optional 546 Directory to write the saved plots to. 552 ax = fig.add_subplot(111)
553 for old_wcs, ref
in zip(old_wcs_list, data_refs):
554 if ref.dataId[
'visit'] != visit:
556 md = ref.get(
'calexp_md')
559 dims.getX(), dims.getY())
562 length = (0.1*u.arcsecond).to(u.radian).value
563 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
566 plt.title(
'visit: {}'.
format(visit))
567 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
568 plt.savefig(filename.format(name, visit))
573 Plot the delta between wcs1 and wcs2 as vector arrows. 578 Matplotlib axis instance to plot to. 579 wcs1 : lsst.afw.image.wcs.Wcs 580 First WCS to compare. 581 wcs2 : lsst.afw.image.wcs.Wcs 582 Second WCS to compare. 584 Size of array in X-coordinate to make the grid over. 586 Size of array in Y-coordinate to make the grid over. 592 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
596 """Plot the magnitude of the WCS change between old and new visits as a heat map. 600 plt : matplotlib.pyplot instance 601 pyplot instance to plot with. 602 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 603 A list of data refs to plot. 604 visits : list of visit id (usually int) 605 list of visit identifiers, one-to-one correspondent with catalogs. 606 old_wcs_list : list of lsst.afw.image.wcs.Wcs 607 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 609 Name to include in plot titles and save files. 610 outdir : str, optional 611 Directory to write the saved plots to. 615 fig.set_tight_layout(
True)
616 ax = fig.add_subplot(111)
622 for old_wcs, ref
in zip(old_wcs_list, data_refs):
623 if ref.dataId[
'visit'] != visit:
625 md = ref.get(
'calexp_md')
628 old_wcs, ref.get(
'jointcal_wcs'))
631 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
632 xmin =
min(x1.min(), xmin)
633 ymin =
min(y1.min(), ymin)
634 xmax =
max(x1.max(), xmax)
635 ymax =
max(y1.max(), ymax)
636 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
637 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
638 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
644 cbar = plt.colorbar(img)
645 cbar.ax.set_ylabel(
'distortion (arcseconds)')
650 plt.title(
'visit: {}'.
format(visit))
651 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
652 plt.savefig(filename.format(name, visit))
655 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
656 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid. 660 plt : matplotlib.pyplot instance 661 pyplot instance to plot with. 662 wcs1 : lsst.afw.image.wcs.Wcs 663 First WCS to compare. 664 wcs2 : lsst.afw.image.wcs.Wcs 665 Second WCS to compare. 667 Size of array in X-coordinate to make the grid over. 669 Size of array in Y-coordinate to make the grid over. 670 center : tuple, optional 671 Center of the data, in on-chip coordinates. 673 Name to include in plot titles and save files. 674 outdir : str, optional 675 Directory to write the saved plots to. 681 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
682 plt.xlabel(
'delta RA (arcsec)')
683 plt.ylabel(
'delta Dec (arcsec)')
685 filename = os.path.join(outdir,
'{}-wcs.pdf')
686 plt.savefig(filename.format(name))
690 new_rms_relative, new_rms_absolute,
691 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
692 name="", outdir='.plots'):
693 """Plot histograms of the source separations and their RMS values. 697 plt : matplotlib.pyplot instance 698 pyplot instance to plot with. 699 old_rms_relative : np.array 700 old relative rms/star 701 old_rms_absolute : np.array 702 old absolute rms/star 703 new_rms_relative : np.array 704 new relative rms/star 705 new_rms_absolute : np.array 706 new absolute rms/star 707 old_rel_total : float 708 old relative rms over all stars 709 old_abs_total : float 710 old absolute rms over all stars 711 new_rel_total : float 712 new relative rms over all stars 713 new_abs_total : float 714 new absolute rms over all stars 716 Name to include in plot titles and save files. 717 outdir : str, optional 718 Directory to write the saved plots to. 726 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
727 'bins': 30,
'histtype':
'step'}
729 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
731 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
732 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
734 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
735 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
737 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
738 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
739 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
740 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
742 plt.xlim(plotOptions[
'range'])
743 plt.xlabel(
'arcseconds')
744 plt.legend(loc=
'best')
745 filename = os.path.join(outdir,
'{}-histogram.pdf')
746 plt.savefig(filename.format(name))
def _photometric_rms(self, sn_cut=300, magnitude_range=3)
def make_plots(self, data_refs, old_wcs_list, name='', interactive=False, per_ccd_plot=False, outdir='.plots')
def __init__(self, match_radius=0.1 *arcseconds, flux_limit=100.0, do_photometry=True, do_astrometry=True, verbose=False)
The photometric calibration of an exposure.
def wcs_convert(xv, yv, wcs)
def plot_rms_histogram(plt, old_rms_relative, old_rms_absolute, new_rms_relative, new_rms_absolute, old_rel_total, old_abs_total, new_rel_total, new_abs_total, name="", outdir='.plots')
def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None)
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
double abMagFromFlux(double flux)
Compute AB magnitude from flux in Janskys.
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
Update sky coordinates in a collection of source objects.
def plot_all_wcs_deltas(plt, data_refs, visits, old_wcs_list, per_ccd_plot=False, name='', outdir='.plots')
std::vector< Match< typename Cat::Record, typename Cat::Record > > matchRaDec(Cat const &cat, Angle radius, bool symmetric)
Compute all tuples (s1,s2,d) where s1 != s2, s1 and s2 both belong to cat, and d, the distance betwee...
def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name="", outdir='.plots')
def plot_wcs_quivers(ax, wcs1, wcs2, x_dim, y_dim)
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
def make_xy_wcs_grid(x_dim, y_dim, wcs1, wcs2, num=50)
geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
def plot_flux_distributions(plt, old_mag, new_mag, old_weighted_rms, new_weighted_rms, faint, bright, old_PA1, new_PA1, name='', outdir='.plots')
def compute_rms(self, data_refs, reference)
static Log getLogger(Log const &logger)
def plot_wcs_magnitude(plt, data_refs, visits, old_wcs_list, name, outdir='.plots')
def plot_all_wcs_quivers(plt, data_refs, visits, old_wcs_list, name, outdir='.plots')
def _make_visit_catalogs(self, catalogs, visits)