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
21 from lsst.afw.image
import fluxFromABMag, abMagFromFlux
22 from lsst.afw.geom
import arcseconds
24 MatchDict = collections.namedtuple(
'MatchDict', [
'relative',
'absolute'])
29 Compute statistics on jointcal-processed data, and optionally generate plots.
33 Instantiate JointcalStatistics and call compute_rms() to get the relevant
34 statistics for e.g. unittests, and call make_plots() to generate a suite of
38 def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
39 do_photometry=
True, do_astrometry=
True,
44 match_radius : lsst.afw.Angle
45 match sources within this radius for RMS statistics
47 Signal/Noise (flux/fluxSigma) for sources to be included in the RMS cross-match.
48 100 is a balance between good centroids and enough sources.
49 do_photometry : bool, optional
50 Perform calculations/make plots for photometric metrics.
51 do_astrometry : bool, optional
52 Perform calculations/make plots for astrometric metrics.
53 verbose : bool, optional
61 self.
log = lsst.log.Log.getLogger(
'JointcalStatistics')
65 Match all data_refs to compute the RMS, for all detections above self.flux_limit.
69 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
70 A list of data refs to do the calculations between.
71 reference : lsst reference catalog
72 reference catalog to do absolute matching against.
78 Post-jointcal relative RMS of the matched sources.
80 Post-jointcal absolute RMS of matched sources.
82 Post-jointcal photometric repeatability (PA1 from the SRD).
86 self.
filters = [ref.get(
'calexp').getInfo().getFilter().getName()
for ref
in data_refs]
89 def compute(catalogs, calibs):
90 """Compute the relative and absolute matches in distance and flux."""
101 dist_rel, flux_rel, ref_flux_rel, source_rel = self.
_make_match_dict(refcat,
105 dist_abs, flux_abs, ref_flux_abs, source_abs = self.
_make_match_dict(reference, catalogs, calibs)
108 ref_flux =
MatchDict(ref_flux_rel, ref_flux_abs)
109 source =
MatchDict(source_rel, source_abs)
110 return dist, flux, ref_flux, source
112 old_cats = [ref.get(
'src')
for ref
in data_refs]
113 old_calibs = [ref.get(
'calexp').getCalib()
for ref
in data_refs]
114 self.old_dist, self.old_flux, self.old_ref_flux, self.
old_source = compute(old_cats, old_calibs)
117 new_cats = [ref.get(
'src')
for ref
in data_refs]
118 new_wcss = [ref.get(
'wcs')
for ref
in data_refs]
119 new_calibs = [wcs.getCalib()
for wcs
in new_wcss]
121 for wcs, cat
in zip(new_wcss, new_cats):
123 lsst.afw.table.utils.updateSourceCoords(wcs.getWcs(), cat)
125 self.new_dist, self.new_flux, self.new_ref_flux, self.
new_source = compute(new_cats, new_calibs)
128 print(
'old, new relative distance matches:',
129 len(self.old_dist.relative), len(self.new_dist.relative))
130 print(
'old, new absolute distance matches:',
131 len(self.old_dist.absolute), len(self.new_dist.absolute))
132 print(
'old, new relative flux matches:',
133 len(self.old_flux.relative), len(self.new_flux.relative))
134 print(
'old, new absolute flux matches:',
135 len(self.old_flux.absolute), len(self.new_flux.absolute))
140 print(
'"photometric factor" for each data ref:')
141 for ref, old, new
in zip(data_refs, old_calibs, new_calibs):
142 print(tuple(ref.dataId.values()), new.getFluxMag0()[0]/old.getFluxMag0()[0])
147 """Compute the total rms across all sources."""
148 total = sum(sum(dd**2)
for dd
in data.values())
149 n = sum(len(dd)
for dd
in data.values())
150 return np.sqrt(total/n)
159 Rms_result = collections.namedtuple(
"Rms_result", [
"dist_relative",
"dist_absolute",
"pa1"])
160 return Rms_result(self.new_dist_total.relative, self.new_dist_total.absolute, self.
new_PA1)
163 name=
'', interactive=
False, per_ccd_plot=
False, outdir=
'.plots'):
165 Make plots of various quantites to help with debugging.
166 Requires that `compute_rms()` was run first.
170 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
171 A list of data refs to do the calculations between.
172 old_wcs_list : list of lsst.afw.image.wcs.Wcs
173 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
175 Name to include in plot titles and save files.
177 Turn on matplotlib interactive mode and drop into a debugger when
178 plotting is finished. Otherwise, use a non-interactive backend.
180 Plot the WCS per CCD (takes longer and generates many plots for a large camera)
182 directory to save plots to.
188 matplotlib.use(
'pdf')
190 import matplotlib.pyplot
as plt
191 import astropy.visualization
193 astropy.visualization.quantity_support()
197 self.log.info(
"N data_refs: %d", len(data_refs))
203 name=name, outdir=outdir)
205 def rms_per_source(data):
206 """Each element of data must already be the "delta" of whatever measurement."""
207 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).to(u.arcsecond)
210 old_dist_rms =
MatchDict(*(tuple(map(rms_per_source, self.old_dist))))
211 new_dist_rms =
MatchDict(*(tuple(map(rms_per_source, self.new_dist))))
213 self.log.info(
"relative RMS (old, new): {:.2e} {:.2e}".format(self.old_dist_total.relative,
214 self.new_dist_total.relative))
215 self.log.info(
"absolute RMS (old, new): {:.2e} {:.2e}".format(self.old_dist_total.absolute,
216 self.new_dist_total.absolute))
218 new_dist_rms.relative, new_dist_rms.absolute,
219 self.old_dist_total.relative, self.old_dist_total.absolute,
220 self.new_dist_total.relative, self.new_dist_total.absolute,
221 name=name, outdir=outdir)
224 per_ccd_plot=per_ccd_plot,
225 name=name, outdir=outdir)
232 def _photometric_rms(self, sn_cut=300, magnitude_range=3):
234 Compute the photometric RMS and the photometric repeatablity values (PA1).
239 The minimum signal/noise for sources to be included in the PA1 calculation.
240 magnitude_range : float
241 The range of magnitudes above sn_cut to include in the PA1 calculation.
243 def rms(flux, ref_flux):
244 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
250 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
251 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
252 self.
old_mag = np.fromiter((abMagFromFlux(r)
for r
in self.
old_ref), dtype=float)
253 self.
new_mag = np.fromiter((abMagFromFlux(r)
for r
in self.
new_ref), dtype=float)
255 def signal_to_noise(sources, flux_key='slot_PsfFlux_flux', sigma_key='slot_PsfFlux_fluxSigma'):
256 """Compute the mean signal/noise per source from a MatchDict of SourceRecords."""
257 result = np.empty(len(sources))
258 for i, src
in enumerate(sources.values()):
259 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
262 old_sn = signal_to_noise(self.old_source.absolute)
267 print(
"PA1 Magnitude range: {:.3f}, {:.3f}".format(self.
bright, self.
faint))
275 def _make_match_dict(self, reference, visit_catalogs, calibs, refcalib=None):
277 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations.
281 reference : lsst.afw.table.SourceCatalog
282 Catalog to do the matching against.
283 visit_catalogs : list of lsst.afw.table.SourceCatalog
284 Visit source catalogs (values() produced by _make_visit_catalogs)
285 to cross-match against reference.
286 calibs : list of lsst.afw.image.Calib
287 Exposure calibs, 1-1 coorespondent with visit_catalogs.
288 refcalib : lsst.afw.image.Calib or None
289 Pass a Calib here to use it to compute Janskys from the reference catalog ADU slot_flux.
294 dict of sourceID: array(separation distances for that source)
296 dict of sourceID: array(fluxes (Jy) for that source)
298 dict of sourceID: flux (Jy) of the reference object
300 dict of sourceID: list(each SourceRecord that was position-matched to this sourceID)
303 distances = collections.defaultdict(list)
304 fluxes = collections.defaultdict(list)
306 sources = collections.defaultdict(list)
307 if 'slot_CalibFlux_flux' in reference.schema:
308 ref_flux_key =
'slot_CalibFlux_flux'
310 ref_flux_key =
'{}_flux'
312 def get_fluxes(match):
313 """Return (flux, ref_flux) or None if either is invalid."""
315 flux = match[1][
'slot_CalibFlux_flux']
320 flux = fluxFromABMag(calib.getMagnitude(flux))
323 if 'slot' in ref_flux_key:
324 ref_flux = match[0][ref_flux_key]
328 ref_flux = fluxFromABMag(refcalib.getMagnitude(ref_flux))
331 ref_flux = match[0][ref_flux_key.format(filt)]
335 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
336 return Flux(flux, ref_flux)
338 for cat, calib, filt
in zip(visit_catalogs, calibs, self.
filters):
339 good = (cat.get(
'base_PsfFlux_flux')/cat.get(
'base_PsfFlux_fluxSigma')) > self.
flux_limit
341 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
342 matches = lsst.afw.table.matchRaDec(reference, cat[good], self.
match_radius)
349 fluxes[m[0].getId()].append(flux.flux)
351 ref_fluxes[m[0].getId()] = flux.ref_flux
355 distances[m[0].getId()].append(m[2])
357 sources[m[0].getId()].append(m[1])
359 for source
in distances:
360 distances[source] = np.array(distances[source])
361 for source
in fluxes:
362 fluxes[source] = np.array(fluxes[source])
364 return distances, fluxes, ref_fluxes, sources
366 def _make_visit_catalogs(self, catalogs, visits):
368 Merge all catalogs from the each visit.
369 NOTE: creating this structure is somewhat slow, and will be unnecessary
370 once a full-visit composite dataset is available.
374 catalogs : list of lsst.afw.table.SourceCatalog
375 Catalogs to combine into per-visit catalogs.
376 visits : list of visit id (usually int)
377 list of visit identifiers, one-to-one correspondent with catalogs.
382 dict of visit: catalog of all sources from all CCDs of that visit.
384 visit_dict = {v: lsst.afw.table.SourceCatalog(catalogs[0].schema)
for v
in visits}
385 for v, cat
in zip(visits, catalogs):
386 visit_dict[v].extend(cat)
389 visit_dict[v] = visit_dict[v].copy(deep=
True)
395 faint, bright, old_PA1, new_PA1,
396 name=
'', outdir=
'.plots'):
397 """Plot various distributions of fluxes and magnitudes.
401 plt : matplotlib.pyplot instance
402 pyplot instance to plot with
407 old_weighted_rms : np.array
408 old rms weighted by the mean (rms(data)/mean(data))
409 new_weighted_rms : np.array
410 old rms weighted by the mean (rms(data)/mean(data))
412 Faint end of range that PA1 was computed from.
414 Bright end of range that PA1 was computed from.
416 Old value of PA1, to plot as horizontal line.
418 New value of PA1, to plot as horizontal line.
420 Name to include in plot titles and save files.
421 outdir : str, optional
422 Directory to write the saved plots to.
426 seaborn.set_style(
'whitegrid')
432 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
433 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
434 plt.axvline(faint, ls=
':', color=old_color)
435 plt.axvline(bright, ls=
':', color=old_color)
436 plt.axhline(old_PA1, ls=
'--', color=old_color)
437 plt.axhline(new_PA1, ls=
'--', color=new_color)
438 plt.legend(loc=
'upper left')
439 plt.title(
'Where is the systematic flux rms limit?')
440 plt.xlabel(
'magnitude')
441 plt.ylabel(
'rms/mean per source')
442 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
443 plt.savefig(filename.format(name))
446 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
447 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
448 plt.title(
'Source RMS pre/post-jointcal')
449 plt.xlabel(
'rms(flux)/mean(flux)')
451 plt.legend(loc=
'upper right')
452 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
453 plt.savefig(filename.format(name))
457 name=
'', outdir=
'.plots'):
459 Various plots of the difference between old and new Wcs.
463 plt : matplotlib.pyplot instance
464 pyplot instance to plot with.
465 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
466 A list of data refs to plot.
467 visits : list of visit id (usually int)
468 list of visit identifiers, one-to-one correspondent with catalogs.
469 old_wcs_list : list of lsst.afw.image.wcs.Wcs
470 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
471 per_ccd_plot : bool, optional
472 Make per-ccd plots of the "wcs different" (warning: slow!)
474 Name to include in plot titles and save files.
475 outdir : str, optional
476 Directory to write the saved plots to.
483 for i, ref
in enumerate(data_refs):
484 md = ref.get(
'calexp_md')
485 plot_wcs(plt, old_wcs_list[i], ref.get(
'wcs').getWcs(),
486 md.get(
'NAXIS1'), md.get(
'NAXIS1'),
487 center=(md.get(
'CRVAL1'), md.get(
'CRVAL2')), name=
'dataRef %d'%i,
492 """Return num x/y grid coordinates for wcs1 and wcs2."""
493 x = np.linspace(0, x_dim, num)
494 y = np.linspace(0, y_dim, num)
497 return x1, y1, x2, y2
501 """Convert two arrays of x/y points into an on-sky grid."""
502 xout = np.zeros((xv.shape[0], yv.shape[0]))
503 yout = np.zeros((xv.shape[0], yv.shape[0]))
504 for i, x
in enumerate(xv):
505 for j, y
in enumerate(yv):
506 sky = wcs.pixelToSky(x, y).toFk5()
507 xout[i, j] = sky.getRa()
508 yout[i, j] = sky.getDec()
514 Make quiver plots of the WCS deltas for each CCD in each visit.
518 plt : matplotlib.pyplot instance
519 pyplot instance to plot with.
520 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
521 A list of data refs to plot.
522 visits : list of visit id (usually int)
523 list of visit identifiers, one-to-one correspondent with catalogs.
524 old_wcs_list : list of lsst.afw.image.wcs.Wcs
525 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
527 Name to include in plot titles and save files.
528 outdir : str, optional
529 Directory to write the saved plots to.
535 ax = fig.add_subplot(111)
536 for old_wcs, ref
in zip(old_wcs_list, data_refs):
537 if ref.dataId[
'visit'] != visit:
539 md = ref.get(
'calexp_md')
541 md.get(
'NAXIS1'), md.get(
'NAXIS2'))
544 length = (0.1*u.arcsecond).to(u.radian).value
545 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
548 plt.title(
'visit: {}'.format(visit))
549 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
550 plt.savefig(filename.format(name, visit))
555 Plot the delta between wcs1 and wcs2 as vector arrows.
560 Matplotlib axis instance to plot to.
561 wcs1 : lsst.afw.image.wcs.Wcs
562 First WCS to compare.
563 wcs2 : lsst.afw.image.wcs.Wcs
564 Second WCS to compare.
566 Size of array in X-coordinate to make the grid over.
568 Size of array in Y-coordinate to make the grid over.
574 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
578 """Plot the magnitude of the WCS change between old and new visits as a heat map.
582 plt : matplotlib.pyplot instance
583 pyplot instance to plot with.
584 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
585 A list of data refs to plot.
586 visits : list of visit id (usually int)
587 list of visit identifiers, one-to-one correspondent with catalogs.
588 old_wcs_list : list of lsst.afw.image.wcs.Wcs
589 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
591 Name to include in plot titles and save files.
592 outdir : str, optional
593 Directory to write the saved plots to.
597 fig.set_tight_layout(
True)
598 ax = fig.add_subplot(111)
604 for old_wcs, ref
in zip(old_wcs_list, data_refs):
605 if ref.dataId[
'visit'] != visit:
607 md = ref.get(
'calexp_md')
609 old_wcs, ref.get(
'wcs').getWcs())
612 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
613 xmin = min(x1.min(), xmin)
614 ymin = min(y1.min(), ymin)
615 xmax = max(x1.max(), xmax)
616 ymax = max(y1.max(), ymax)
617 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
618 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
619 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
625 cbar = plt.colorbar(img)
626 cbar.ax.set_ylabel(
'distortion (arcseconds)')
631 plt.title(
'visit: {}'.format(visit))
632 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
633 plt.savefig(filename.format(name, visit))
636 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
637 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid.
641 plt : matplotlib.pyplot instance
642 pyplot instance to plot with.
643 wcs1 : lsst.afw.image.wcs.Wcs
644 First WCS to compare.
645 wcs2 : lsst.afw.image.wcs.Wcs
646 Second WCS to compare.
648 Size of array in X-coordinate to make the grid over.
650 Size of array in Y-coordinate to make the grid over.
651 center : tuple, optional
652 Center of the data, in on-chip coordinates.
654 Name to include in plot titles and save files.
655 outdir : str, optional
656 Directory to write the saved plots to.
662 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
663 plt.xlabel(
'delta RA (arcsec)')
664 plt.ylabel(
'delta Dec (arcsec)')
666 filename = os.path.join(outdir,
'{}-wcs.pdf')
667 plt.savefig(filename.format(name))
671 new_rms_relative, new_rms_absolute,
672 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
673 name=
"", outdir=
'.plots'):
674 """Plot histograms of the source separations and their RMS values.
678 plt : matplotlib.pyplot instance
679 pyplot instance to plot with.
680 old_rms_relative : np.array
681 old relative rms/star
682 old_rms_absolute : np.array
683 old absolute rms/star
684 new_rms_relative : np.array
685 new relative rms/star
686 new_rms_absolute : np.array
687 new absolute rms/star
688 old_rel_total : float
689 old relative rms over all stars
690 old_abs_total : float
691 old absolute rms over all stars
692 new_rel_total : float
693 new relative rms over all stars
694 new_abs_total : float
695 new absolute rms over all stars
697 Name to include in plot titles and save files.
698 outdir : str, optional
699 Directory to write the saved plots to.
707 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
708 'bins': 30,
'histtype':
'step'}
710 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
712 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
713 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
715 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
716 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
718 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
719 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
720 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
721 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
723 plt.xlim(plotOptions[
'range'])
724 plt.xlabel(
'arcseconds')
725 plt.legend(loc=
'best')
726 filename = os.path.join(outdir,
'{}-histogram.pdf')
727 plt.savefig(filename.format(name))
def plot_flux_distributions