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)
447 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False)
449 plt.xlabel(
'rms(flux)/mean(flux)')
451 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
452 plt.savefig(filename.format(name))
456 name=
'', outdir=
'.plots'):
458 Various plots of the difference between old and new Wcs.
462 plt : matplotlib.pyplot instance
463 pyplot instance to plot with.
464 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
465 A list of data refs to plot.
466 visits : list of visit id (usually int)
467 list of visit identifiers, one-to-one correspondent with catalogs.
468 old_wcs_list : list of lsst.afw.image.wcs.Wcs
469 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
470 per_ccd_plot : bool, optional
471 Make per-ccd plots of the "wcs different" (warning: slow!)
473 Name to include in plot titles and save files.
474 outdir : str, optional
475 Directory to write the saved plots to.
482 for i, ref
in enumerate(data_refs):
483 md = ref.get(
'calexp_md')
484 plot_wcs(plt, old_wcs_list[i], ref.get(
'wcs').getWcs(),
485 md.get(
'NAXIS1'), md.get(
'NAXIS1'),
486 center=(md.get(
'CRVAL1'), md.get(
'CRVAL2')), name=
'dataRef %d'%i,
491 """Return num x/y grid coordinates for wcs1 and wcs2."""
492 x = np.linspace(0, x_dim, num)
493 y = np.linspace(0, y_dim, num)
496 return x1, y1, x2, y2
500 """Convert two arrays of x/y points into an on-sky grid."""
501 xout = np.zeros((xv.shape[0], yv.shape[0]))
502 yout = np.zeros((xv.shape[0], yv.shape[0]))
503 for i, x
in enumerate(xv):
504 for j, y
in enumerate(yv):
505 sky = wcs.pixelToSky(x, y).toFk5()
506 xout[i, j] = sky.getRa()
507 yout[i, j] = sky.getDec()
513 Make quiver plots of the WCS deltas for each CCD in each visit.
517 plt : matplotlib.pyplot instance
518 pyplot instance to plot with.
519 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
520 A list of data refs to plot.
521 visits : list of visit id (usually int)
522 list of visit identifiers, one-to-one correspondent with catalogs.
523 old_wcs_list : list of lsst.afw.image.wcs.Wcs
524 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
526 Name to include in plot titles and save files.
527 outdir : str, optional
528 Directory to write the saved plots to.
534 ax = fig.add_subplot(111)
535 for old_wcs, ref
in zip(old_wcs_list, data_refs):
536 if ref.dataId[
'visit'] != visit:
538 md = ref.get(
'calexp_md')
540 md.get(
'NAXIS1'), md.get(
'NAXIS2'))
543 length = (0.1*u.arcsecond).to(u.radian).value
544 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
547 plt.title(
'visit: {}'.format(visit))
548 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
549 plt.savefig(filename.format(name, visit))
554 Plot the delta between wcs1 and wcs2 as vector arrows.
559 Matplotlib axis instance to plot to.
560 wcs1 : lsst.afw.image.wcs.Wcs
561 First WCS to compare.
562 wcs2 : lsst.afw.image.wcs.Wcs
563 Second WCS to compare.
565 Size of array in X-coordinate to make the grid over.
567 Size of array in Y-coordinate to make the grid over.
573 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
577 """Plot the magnitude of the WCS change between old and new visits as a heat map.
581 plt : matplotlib.pyplot instance
582 pyplot instance to plot with.
583 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
584 A list of data refs to plot.
585 visits : list of visit id (usually int)
586 list of visit identifiers, one-to-one correspondent with catalogs.
587 old_wcs_list : list of lsst.afw.image.wcs.Wcs
588 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
590 Name to include in plot titles and save files.
591 outdir : str, optional
592 Directory to write the saved plots to.
596 fig.set_tight_layout(
True)
597 ax = fig.add_subplot(111)
603 for old_wcs, ref
in zip(old_wcs_list, data_refs):
604 if ref.dataId[
'visit'] != visit:
606 md = ref.get(
'calexp_md')
608 old_wcs, ref.get(
'wcs').getWcs())
611 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
612 xmin = min(x1.min(), xmin)
613 ymin = min(y1.min(), ymin)
614 xmax = max(x1.max(), xmax)
615 ymax = max(y1.max(), ymax)
616 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
617 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
618 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
624 cbar = plt.colorbar(img)
625 cbar.ax.set_ylabel(
'distortion (arcseconds)')
630 plt.title(
'visit: {}'.format(visit))
631 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
632 plt.savefig(filename.format(name, visit))
635 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
636 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid.
640 plt : matplotlib.pyplot instance
641 pyplot instance to plot with.
642 wcs1 : lsst.afw.image.wcs.Wcs
643 First WCS to compare.
644 wcs2 : lsst.afw.image.wcs.Wcs
645 Second WCS to compare.
647 Size of array in X-coordinate to make the grid over.
649 Size of array in Y-coordinate to make the grid over.
650 center : tuple, optional
651 Center of the data, in on-chip coordinates.
653 Name to include in plot titles and save files.
654 outdir : str, optional
655 Directory to write the saved plots to.
661 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
662 plt.xlabel(
'delta RA (arcsec)')
663 plt.ylabel(
'delta Dec (arcsec)')
665 filename = os.path.join(outdir,
'{}-wcs.pdf')
666 plt.savefig(filename.format(name))
670 new_rms_relative, new_rms_absolute,
671 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
672 name=
"", outdir=
'.plots'):
673 """Plot histograms of the source separations and their RMS values.
677 plt : matplotlib.pyplot instance
678 pyplot instance to plot with.
679 old_rms_relative : np.array
680 old relative rms/star
681 old_rms_absolute : np.array
682 old absolute rms/star
683 new_rms_relative : np.array
684 new relative rms/star
685 new_rms_absolute : np.array
686 new absolute rms/star
687 old_rel_total : float
688 old relative rms over all stars
689 old_abs_total : float
690 old absolute rms over all stars
691 new_rel_total : float
692 new relative rms over all stars
693 new_abs_total : float
694 new absolute rms over all stars
696 Name to include in plot titles and save files.
697 outdir : str, optional
698 Directory to write the saved plots to.
706 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
707 'bins': 30,
'histtype':
'step'}
709 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
711 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
712 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
714 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
715 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
717 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
718 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
719 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
720 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
722 plt.xlim(plotOptions[
'range'])
723 plt.xlabel(
'arcseconds')
724 plt.legend(loc=
'best')
725 filename = os.path.join(outdir,
'{}-histogram.pdf')
726 plt.savefig(filename.format(name))
def plot_flux_distributions