23 Statistics of jointcal vs. single-frame procesing and diagnostic plots.
27 Some of the algorithms and data structures in this code are temporary
28 kludges and will no longer be necessary once the following are available:
30 - a composite data structure that contains all ccds from a single visit
31 - an n-way matching system that preserves the separations between sources
37 from astropy
import units
as u
44 __all__ = [
'JointcalStatistics']
46 MatchDict = collections.namedtuple(
'MatchDict', [
'relative',
'absolute'])
51 Compute statistics on jointcal-processed data, and optionally generate plots.
55 Instantiate JointcalStatistics and call compute_rms() to get the relevant
56 statistics for e.g. unittests, and call make_plots() to generate a suite of
60 def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
61 do_photometry=True, do_astrometry=True,
66 match_radius : lsst.geom.Angle
67 match sources within this radius for RMS statistics
69 Signal/Noise (flux/fluxErr) for sources to be included in the RMS cross-match.
70 100 is a balance between good centroids and enough sources.
71 do_photometry : bool, optional
72 Perform calculations/make plots for photometric metrics.
73 do_astrometry : bool, optional
74 Perform calculations/make plots for astrometric metrics.
75 verbose : bool, optional
87 Match all data_refs to compute the RMS, for all detections above self.flux_limit.
91 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
92 A list of data refs to do the calculations between.
93 reference : lsst reference catalog
94 reference catalog to do absolute matching against.
100 Post-jointcal relative RMS of the matched sources.
102 Post-jointcal absolute RMS of matched sources.
104 Post-jointcal photometric repeatability (PA1 from the SRD).
108 self.
filters = [ref.get(
'calexp_filter').getName()
for ref
in data_refs]
111 def compute(catalogs, photoCalibs):
112 """Compute the relative and absolute matches in distance and flux."""
122 refcalib = photoCalibs[0]
if photoCalibs != []
else None
123 dist_rel, flux_rel, ref_flux_rel, source_rel = self.
_make_match_dict(refcat,
127 dist_abs, flux_abs, ref_flux_abs, source_abs = self.
_make_match_dict(reference,
132 ref_flux =
MatchDict(ref_flux_rel, ref_flux_abs)
133 source =
MatchDict(source_rel, source_abs)
134 return dist, flux, ref_flux, source
136 old_cats = [ref.get(
'src')
for ref
in data_refs]
141 old_calibs = [ref.get(
'calexp_photoCalib')
for ref
in data_refs]
143 self.old_dist, self.old_flux, self.old_ref_flux, self.
old_source = compute(old_cats, old_calibs)
146 new_cats = [ref.get(
'src')
for ref
in data_refs]
149 new_wcss = [ref.get(
'jointcal_wcs')
for ref
in data_refs]
152 new_calibs = [ref.get(
'jointcal_photoCalib')
for ref
in data_refs]
154 for wcs, cat
in zip(new_wcss, new_cats):
158 self.new_dist, self.new_flux, self.new_ref_flux, self.
new_source = compute(new_cats, new_calibs)
161 print(
'old, new relative distance matches:',
162 len(self.old_dist.relative), len(self.new_dist.relative))
163 print(
'old, new absolute distance matches:',
164 len(self.old_dist.absolute), len(self.new_dist.absolute))
165 print(
'old, new relative flux matches:',
166 len(self.old_flux.relative), len(self.new_flux.relative))
167 print(
'old, new absolute flux matches:',
168 len(self.old_flux.absolute), len(self.new_flux.absolute))
176 """Compute the total rms across all sources."""
177 total = sum(sum(dd**2)
for dd
in data.values())
178 n = sum(len(dd)
for dd
in data.values())
179 return np.sqrt(total/n)
188 Rms_result = collections.namedtuple(
"Rms_result", [
"dist_relative",
"dist_absolute",
"pa1"])
192 name='', interactive=False, per_ccd_plot=False, outdir='.plots'):
194 Make plots of various quantites to help with debugging.
195 Requires that `compute_rms()` was run first.
199 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
200 A list of data refs to do the calculations between.
201 old_wcs_list : list of lsst.afw.image.wcs.Wcs
202 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
204 Name to include in plot titles and save files.
206 Turn on matplotlib interactive mode and drop into a debugger when
207 plotting is finished. Otherwise, use a non-interactive backend.
209 Plot the WCS per CCD (takes longer and generates many plots for a large camera)
211 directory to save plots to.
217 matplotlib.use(
'pdf')
219 import matplotlib.pyplot
as plt
220 import astropy.visualization
222 astropy.visualization.quantity_support()
226 self.
log.info(
"N data_refs: %d", len(data_refs))
232 name=name, outdir=outdir)
233 self.
log.info(
"Photometric accuracy (old, new): {:.2e} {:.2e}".format(self.
old_PA1,
236 def rms_per_source(data):
237 """Each element of data must already be the "delta" of whatever measurement."""
238 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).to(u.arcsecond)
244 self.
log.info(
"relative RMS (old, new): {:.2e} {:.2e}".format(self.
old_dist_total.relative,
246 self.
log.info(
"absolute RMS (old, new): {:.2e} {:.2e}".format(self.
old_dist_total.absolute,
249 new_dist_rms.relative, new_dist_rms.absolute,
252 name=name, outdir=outdir)
255 per_ccd_plot=per_ccd_plot,
256 name=name, outdir=outdir)
263 def _photometric_rms(self, sn_cut=300, magnitude_range=3):
265 Compute the photometric RMS and the photometric repeatablity values (PA1).
270 The minimum signal/noise for sources to be included in the PA1 calculation.
271 magnitude_range : float
272 The range of magnitudes above sn_cut to include in the PA1 calculation.
274 def rms(flux, ref_flux):
275 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
281 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
282 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
286 def signal_to_noise(sources, flux_key='slot_PsfFlux_instFlux', sigma_key='slot_PsfFlux_instFluxErr'):
287 """Compute the mean signal/noise per source from a MatchDict of SourceRecords."""
288 result = np.empty(len(sources))
289 for i, src
in enumerate(sources.values()):
290 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
293 old_sn = signal_to_noise(self.
old_source.absolute)
298 print(
"PA1 Magnitude range: {:.3f}, {:.3f}".format(self.
bright, self.
faint))
306 def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None):
308 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations.
312 reference : lsst.afw.table.SourceCatalog
313 Catalog to do the matching against.
314 visit_catalogs : list of lsst.afw.table.SourceCatalog
315 Visit source catalogs (values() produced by _make_visit_catalogs)
316 to cross-match against reference.
317 photoCalibs : list of lsst.afw.image.PhotoCalib
318 Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs.
319 refcalib : lsst.afw.image.PhotoCalib or None
320 Pass a PhotoCalib here to use it to compute nanojansky from the
321 reference catalog ADU slot_flux.
326 dict of sourceID: array(separation distances for that source)
328 dict of sourceID: array(fluxes (nJy) for that source)
330 dict of sourceID: flux (nJy) of the reference object
332 dict of sourceID: list(each SourceRecord that was position-matched
336 if photoCalibs == []:
337 photoCalibs = [[]]*len(visit_catalogs)
339 distances = collections.defaultdict(list)
340 fluxes = collections.defaultdict(list)
342 sources = collections.defaultdict(list)
343 if 'slot_CalibFlux_instFlux' in reference.schema:
344 ref_flux_key =
'slot_CalibFlux'
346 ref_flux_key =
'{}_flux'
348 def get_fluxes(photoCalib, match):
349 """Return (flux, ref_flux) or None if either is invalid."""
351 flux = match[1][
'slot_CalibFlux_instFlux']
355 flux = photoCalib.instFluxToNanojansky(match[1],
"slot_CalibFlux").value
358 if 'slot' in ref_flux_key:
359 ref_flux = match[0][ref_flux_key+
'_instFlux']
363 ref_flux = refcalib.instFluxToNanojansky(match[0], ref_flux_key).value
366 ref_flux = match[0][ref_flux_key.format(filt)]
370 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
371 return Flux(flux, ref_flux)
373 for cat, photoCalib, filt
in zip(visit_catalogs, photoCalibs, self.
filters):
374 good = (cat.get(
'base_PsfFlux_instFlux')/cat.get(
'base_PsfFlux_instFluxErr')) > self.
flux_limit
376 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
380 flux = get_fluxes(photoCalib, m)
384 fluxes[m[0].getId()].append(flux.flux)
386 ref_fluxes[m[0].getId()] = flux.ref_flux
390 distances[m[0].getId()].append(m[2])
392 sources[m[0].getId()].append(m[1])
394 for source
in distances:
395 distances[source] = np.array(distances[source])
396 for source
in fluxes:
397 fluxes[source] = np.array(fluxes[source])
399 return distances, fluxes, ref_fluxes, sources
401 def _make_visit_catalogs(self, catalogs, visits):
403 Merge all catalogs from the each visit.
404 NOTE: creating this structure is somewhat slow, and will be unnecessary
405 once a full-visit composite dataset is available.
409 catalogs : list of lsst.afw.table.SourceCatalog
410 Catalogs to combine into per-visit catalogs.
411 visits : list of visit id (usually int)
412 list of visit identifiers, one-to-one correspondent with catalogs.
417 dict of visit: catalog of all sources from all CCDs of that visit.
420 for v, cat
in zip(visits, catalogs):
421 visit_dict[v].extend(cat)
424 visit_dict[v] = visit_dict[v].copy(deep=
True)
430 faint, bright, old_PA1, new_PA1,
431 name='', outdir='.plots'):
432 """Plot various distributions of fluxes and magnitudes.
436 plt : matplotlib.pyplot instance
437 pyplot instance to plot with
442 old_weighted_rms : np.array
443 old rms weighted by the mean (rms(data)/mean(data))
444 new_weighted_rms : np.array
445 old rms weighted by the mean (rms(data)/mean(data))
447 Faint end of range that PA1 was computed from.
449 Bright end of range that PA1 was computed from.
451 Old value of PA1, to plot as horizontal line.
453 New value of PA1, to plot as horizontal line.
455 Name to include in plot titles and save files.
456 outdir : str, optional
457 Directory to write the saved plots to.
461 seaborn.set_style(
'whitegrid')
467 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
468 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
469 plt.axvline(faint, ls=
':', color=old_color)
470 plt.axvline(bright, ls=
':', color=old_color)
471 plt.axhline(old_PA1, ls=
'--', color=old_color)
472 plt.axhline(new_PA1, ls=
'--', color=new_color)
473 plt.legend(loc=
'upper left')
474 plt.title(
'Where is the systematic flux rms limit?')
475 plt.xlabel(
'magnitude')
476 plt.ylabel(
'rms/mean per source')
477 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
478 plt.savefig(filename.format(name))
481 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
482 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
483 plt.title(
'Source RMS pre/post-jointcal')
484 plt.xlabel(
'rms(flux)/mean(flux)')
486 plt.legend(loc=
'upper right')
487 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
488 plt.savefig(filename.format(name))
492 name='', outdir='.plots'):
494 Various plots of the difference between old and new Wcs.
498 plt : matplotlib.pyplot instance
499 pyplot instance to plot with.
500 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
501 A list of data refs to plot.
502 visits : list of visit id (usually int)
503 list of visit identifiers, one-to-one correspondent with catalogs.
504 old_wcs_list : list of lsst.afw.image.wcs.Wcs
505 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
506 per_ccd_plot : bool, optional
507 Make per-ccd plots of the "wcs different" (warning: slow!)
509 Name to include in plot titles and save files.
510 outdir : str, optional
511 Directory to write the saved plots to.
518 for i, ref
in enumerate(data_refs):
519 md = ref.get(
'calexp_md')
521 plot_wcs(plt, old_wcs_list[i], ref.get(
'jointcal_wcs'),
522 dims.getX(), dims.getY(),
523 center=(md.getScalar(
'CRVAL1'), md.getScalar(
'CRVAL2')), name=
'dataRef %d'%i,
528 """Return num x/y grid coordinates for wcs1 and wcs2."""
529 x = np.linspace(0, x_dim, num)
530 y = np.linspace(0, y_dim, num)
533 return x1, y1, x2, y2
537 """Convert two arrays of x/y points into an on-sky grid."""
538 xout = np.zeros((xv.shape[0], yv.shape[0]))
539 yout = np.zeros((xv.shape[0], yv.shape[0]))
540 for i, x
in enumerate(xv):
541 for j, y
in enumerate(yv):
542 sky = wcs.pixelToSky(x, y)
543 xout[i, j] = sky.getRa()
544 yout[i, j] = sky.getDec()
550 Make quiver plots of the WCS deltas for each CCD in each visit.
554 plt : matplotlib.pyplot instance
555 pyplot instance to plot with.
556 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
557 A list of data refs to plot.
558 visits : list of visit id (usually int)
559 list of visit identifiers, one-to-one correspondent with catalogs.
560 old_wcs_list : list of lsst.afw.image.wcs.Wcs
561 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
563 Name to include in plot titles and save files.
564 outdir : str, optional
565 Directory to write the saved plots to.
571 ax = fig.add_subplot(111)
572 for old_wcs, ref
in zip(old_wcs_list, data_refs):
573 if ref.dataId[
'visit'] != visit:
575 md = ref.get(
'calexp_md')
578 dims.getX(), dims.getY())
581 length = (0.1*u.arcsecond).to(u.radian).value
582 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
585 plt.title(
'visit: {}'.format(visit))
586 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
587 plt.savefig(filename.format(name, visit))
592 Plot the delta between wcs1 and wcs2 as vector arrows.
597 Matplotlib axis instance to plot to.
598 wcs1 : lsst.afw.image.wcs.Wcs
599 First WCS to compare.
600 wcs2 : lsst.afw.image.wcs.Wcs
601 Second WCS to compare.
603 Size of array in X-coordinate to make the grid over.
605 Size of array in Y-coordinate to make the grid over.
611 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
615 """Plot the magnitude of the WCS change between old and new visits as a heat map.
619 plt : matplotlib.pyplot instance
620 pyplot instance to plot with.
621 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
622 A list of data refs to plot.
623 visits : list of visit id (usually int)
624 list of visit identifiers, one-to-one correspondent with catalogs.
625 old_wcs_list : list of lsst.afw.image.wcs.Wcs
626 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
628 Name to include in plot titles and save files.
629 outdir : str, optional
630 Directory to write the saved plots to.
634 fig.set_tight_layout(
True)
635 ax = fig.add_subplot(111)
641 for old_wcs, ref
in zip(old_wcs_list, data_refs):
642 if ref.dataId[
'visit'] != visit:
644 md = ref.get(
'calexp_md')
647 old_wcs, ref.get(
'jointcal_wcs'))
650 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
651 xmin = min(x1.min(), xmin)
652 ymin = min(y1.min(), ymin)
653 xmax = max(x1.max(), xmax)
654 ymax = max(y1.max(), ymax)
655 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
656 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
657 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
663 cbar = plt.colorbar(img)
664 cbar.ax.set_ylabel(
'distortion (arcseconds)')
669 plt.title(
'visit: {}'.format(visit))
670 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
671 plt.savefig(filename.format(name, visit))
674 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
675 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid.
679 plt : matplotlib.pyplot instance
680 pyplot instance to plot with.
681 wcs1 : lsst.afw.image.wcs.Wcs
682 First WCS to compare.
683 wcs2 : lsst.afw.image.wcs.Wcs
684 Second WCS to compare.
686 Size of array in X-coordinate to make the grid over.
688 Size of array in Y-coordinate to make the grid over.
689 center : tuple, optional
690 Center of the data, in on-chip coordinates.
692 Name to include in plot titles and save files.
693 outdir : str, optional
694 Directory to write the saved plots to.
700 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
701 plt.xlabel(
'delta RA (arcsec)')
702 plt.ylabel(
'delta Dec (arcsec)')
704 filename = os.path.join(outdir,
'{}-wcs.pdf')
705 plt.savefig(filename.format(name))
709 new_rms_relative, new_rms_absolute,
710 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
711 name="", outdir='.plots'):
712 """Plot histograms of the source separations and their RMS values.
716 plt : matplotlib.pyplot instance
717 pyplot instance to plot with.
718 old_rms_relative : np.array
719 old relative rms/star
720 old_rms_absolute : np.array
721 old absolute rms/star
722 new_rms_relative : np.array
723 new relative rms/star
724 new_rms_absolute : np.array
725 new absolute rms/star
726 old_rel_total : float
727 old relative rms over all stars
728 old_abs_total : float
729 old absolute rms over all stars
730 new_rel_total : float
731 new relative rms over all stars
732 new_abs_total : float
733 new absolute rms over all stars
735 Name to include in plot titles and save files.
736 outdir : str, optional
737 Directory to write the saved plots to.
745 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
746 'bins': 30,
'histtype':
'step'}
748 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
750 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
751 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
753 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
754 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
756 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
757 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
758 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
759 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
761 plt.xlim(plotOptions[
'range'])
762 plt.xlabel(
'arcseconds')
763 plt.legend(loc=
'best')
764 filename = os.path.join(outdir,
'{}-histogram.pdf')
765 plt.savefig(filename.format(name))