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.afw.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 for ref
in data_refs:
142 calib = ref.get(
'calexp_calib')
143 fluxMag0 = calib.getFluxMag0()
147 self.old_dist, self.old_flux, self.old_ref_flux, self.
old_source = compute(old_cats, old_calibs)
150 new_cats = [ref.get(
'src')
for ref
in data_refs]
153 new_wcss = [ref.get(
'jointcal_wcs')
for ref
in data_refs]
156 new_calibs = [ref.get(
'jointcal_photoCalib')
for ref
in data_refs]
158 for wcs, cat
in zip(new_wcss, new_cats):
162 self.new_dist, self.new_flux, self.new_ref_flux, self.
new_source = compute(new_cats, new_calibs)
165 print(
'old, new relative distance matches:',
166 len(self.old_dist.relative), len(self.new_dist.relative))
167 print(
'old, new absolute distance matches:',
168 len(self.old_dist.absolute), len(self.new_dist.absolute))
169 print(
'old, new relative flux matches:',
170 len(self.old_flux.relative), len(self.new_flux.relative))
171 print(
'old, new absolute flux matches:',
172 len(self.old_flux.absolute), len(self.new_flux.absolute))
180 """Compute the total rms across all sources.""" 181 total = sum(sum(dd**2)
for dd
in data.values())
182 n = sum(len(dd)
for dd
in data.values())
183 return np.sqrt(total/n)
192 Rms_result = collections.namedtuple(
"Rms_result", [
"dist_relative",
"dist_absolute",
"pa1"])
196 name='', interactive=False, per_ccd_plot=False, outdir='.plots'):
198 Make plots of various quantites to help with debugging. 199 Requires that `compute_rms()` was run first. 203 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 204 A list of data refs to do the calculations between. 205 old_wcs_list : list of lsst.afw.image.wcs.Wcs 206 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 208 Name to include in plot titles and save files. 210 Turn on matplotlib interactive mode and drop into a debugger when 211 plotting is finished. Otherwise, use a non-interactive backend. 213 Plot the WCS per CCD (takes longer and generates many plots for a large camera) 215 directory to save plots to. 221 matplotlib.use(
'pdf')
223 import matplotlib.pyplot
as plt
224 import astropy.visualization
226 astropy.visualization.quantity_support()
230 self.
log.info(
"N data_refs: %d", len(data_refs))
236 name=name, outdir=outdir)
237 self.
log.info(
"Photometric accuracy (old, new): {:.2e} {:.2e}".format(self.
old_PA1,
240 def rms_per_source(data):
241 """Each element of data must already be the "delta" of whatever measurement.""" 242 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).to(u.arcsecond)
248 self.
log.info(
"relative RMS (old, new): {:.2e} {:.2e}".format(self.
old_dist_total.relative,
250 self.
log.info(
"absolute RMS (old, new): {:.2e} {:.2e}".format(self.
old_dist_total.absolute,
253 new_dist_rms.relative, new_dist_rms.absolute,
256 name=name, outdir=outdir)
259 per_ccd_plot=per_ccd_plot,
260 name=name, outdir=outdir)
267 def _photometric_rms(self, sn_cut=300, magnitude_range=3):
269 Compute the photometric RMS and the photometric repeatablity values (PA1). 274 The minimum signal/noise for sources to be included in the PA1 calculation. 275 magnitude_range : float 276 The range of magnitudes above sn_cut to include in the PA1 calculation. 278 def rms(flux, ref_flux):
279 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
285 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
286 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
290 def signal_to_noise(sources, flux_key='slot_PsfFlux_instFlux', sigma_key='slot_PsfFlux_instFluxErr'):
291 """Compute the mean signal/noise per source from a MatchDict of SourceRecords.""" 292 result = np.empty(len(sources))
293 for i, src
in enumerate(sources.values()):
294 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
297 old_sn = signal_to_noise(self.
old_source.absolute)
302 print(
"PA1 Magnitude range: {:.3f}, {:.3f}".format(self.
bright, self.
faint))
310 def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None):
312 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations. 316 reference : lsst.afw.table.SourceCatalog 317 Catalog to do the matching against. 318 visit_catalogs : list of lsst.afw.table.SourceCatalog 319 Visit source catalogs (values() produced by _make_visit_catalogs) 320 to cross-match against reference. 321 photoCalibs : list of lsst.afw.image.PhotoCalib 322 Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs. 323 refcalib : lsst.afw.image.PhotoCalib or None 324 Pass a PhotoCalib here to use it to compute nanojansky from the 325 reference catalog ADU slot_flux. 330 dict of sourceID: array(separation distances for that source) 332 dict of sourceID: array(fluxes (nJy) for that source) 334 dict of sourceID: flux (nJy) of the reference object 336 dict of sourceID: list(each SourceRecord that was position-matched 340 if photoCalibs == []:
341 photoCalibs = [[]]*len(visit_catalogs)
343 distances = collections.defaultdict(list)
344 fluxes = collections.defaultdict(list)
346 sources = collections.defaultdict(list)
347 if 'slot_CalibFlux_instFlux' in reference.schema:
348 ref_flux_key =
'slot_CalibFlux' 350 ref_flux_key =
'{}_flux' 352 def get_fluxes(photoCalib, match):
353 """Return (flux, ref_flux) or None if either is invalid.""" 355 flux = match[1][
'slot_CalibFlux_instFlux']
359 flux = photoCalib.instFluxToNanojansky(match[1],
"slot_CalibFlux").value
362 if 'slot' in ref_flux_key:
363 ref_flux = match[0][ref_flux_key+
'_instFlux']
367 ref_flux = refcalib.instFluxToNanojansky(match[0], ref_flux_key).value
371 ref_flux = 1e9 * match[0][ref_flux_key.format(filt)]
375 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
376 return Flux(flux, ref_flux)
378 for cat, photoCalib, filt
in zip(visit_catalogs, photoCalibs, self.
filters):
379 good = (cat.get(
'base_PsfFlux_instFlux')/cat.get(
'base_PsfFlux_instFluxErr')) > self.
flux_limit 381 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
385 flux = get_fluxes(photoCalib, m)
389 fluxes[m[0].getId()].append(flux.flux)
391 ref_fluxes[m[0].getId()] = flux.ref_flux
395 distances[m[0].getId()].append(m[2])
397 sources[m[0].getId()].append(m[1])
399 for source
in distances:
400 distances[source] = np.array(distances[source])
401 for source
in fluxes:
402 fluxes[source] = np.array(fluxes[source])
404 return distances, fluxes, ref_fluxes, sources
406 def _make_visit_catalogs(self, catalogs, visits):
408 Merge all catalogs from the each visit. 409 NOTE: creating this structure is somewhat slow, and will be unnecessary 410 once a full-visit composite dataset is available. 414 catalogs : list of lsst.afw.table.SourceCatalog 415 Catalogs to combine into per-visit catalogs. 416 visits : list of visit id (usually int) 417 list of visit identifiers, one-to-one correspondent with catalogs. 422 dict of visit: catalog of all sources from all CCDs of that visit. 425 for v, cat
in zip(visits, catalogs):
426 visit_dict[v].extend(cat)
429 visit_dict[v] = visit_dict[v].copy(deep=
True)
435 faint, bright, old_PA1, new_PA1,
436 name='', outdir='.plots'):
437 """Plot various distributions of fluxes and magnitudes. 441 plt : matplotlib.pyplot instance 442 pyplot instance to plot with 447 old_weighted_rms : np.array 448 old rms weighted by the mean (rms(data)/mean(data)) 449 new_weighted_rms : np.array 450 old rms weighted by the mean (rms(data)/mean(data)) 452 Faint end of range that PA1 was computed from. 454 Bright end of range that PA1 was computed from. 456 Old value of PA1, to plot as horizontal line. 458 New value of PA1, to plot as horizontal line. 460 Name to include in plot titles and save files. 461 outdir : str, optional 462 Directory to write the saved plots to. 466 seaborn.set_style(
'whitegrid')
472 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
473 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
474 plt.axvline(faint, ls=
':', color=old_color)
475 plt.axvline(bright, ls=
':', color=old_color)
476 plt.axhline(old_PA1, ls=
'--', color=old_color)
477 plt.axhline(new_PA1, ls=
'--', color=new_color)
478 plt.legend(loc=
'upper left')
479 plt.title(
'Where is the systematic flux rms limit?')
480 plt.xlabel(
'magnitude')
481 plt.ylabel(
'rms/mean per source')
482 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
483 plt.savefig(filename.format(name))
486 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
487 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
488 plt.title(
'Source RMS pre/post-jointcal')
489 plt.xlabel(
'rms(flux)/mean(flux)')
491 plt.legend(loc=
'upper right')
492 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
493 plt.savefig(filename.format(name))
497 name='', outdir='.plots'):
499 Various plots of the difference between old and new Wcs. 503 plt : matplotlib.pyplot instance 504 pyplot instance to plot with. 505 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 506 A list of data refs to plot. 507 visits : list of visit id (usually int) 508 list of visit identifiers, one-to-one correspondent with catalogs. 509 old_wcs_list : list of lsst.afw.image.wcs.Wcs 510 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 511 per_ccd_plot : bool, optional 512 Make per-ccd plots of the "wcs different" (warning: slow!) 514 Name to include in plot titles and save files. 515 outdir : str, optional 516 Directory to write the saved plots to. 523 for i, ref
in enumerate(data_refs):
524 md = ref.get(
'calexp_md')
526 plot_wcs(plt, old_wcs_list[i], ref.get(
'jointcal_wcs'),
527 dims.getX(), dims.getY(),
528 center=(md.getScalar(
'CRVAL1'), md.getScalar(
'CRVAL2')), name=
'dataRef %d'%i,
533 """Return num x/y grid coordinates for wcs1 and wcs2.""" 534 x = np.linspace(0, x_dim, num)
535 y = np.linspace(0, y_dim, num)
538 return x1, y1, x2, y2
542 """Convert two arrays of x/y points into an on-sky grid.""" 543 xout = np.zeros((xv.shape[0], yv.shape[0]))
544 yout = np.zeros((xv.shape[0], yv.shape[0]))
545 for i, x
in enumerate(xv):
546 for j, y
in enumerate(yv):
547 sky = wcs.pixelToSky(x, y)
548 xout[i, j] = sky.getRa()
549 yout[i, j] = sky.getDec()
555 Make quiver plots of the WCS deltas for each CCD in each visit. 559 plt : matplotlib.pyplot instance 560 pyplot instance to plot with. 561 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 562 A list of data refs to plot. 563 visits : list of visit id (usually int) 564 list of visit identifiers, one-to-one correspondent with catalogs. 565 old_wcs_list : list of lsst.afw.image.wcs.Wcs 566 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 568 Name to include in plot titles and save files. 569 outdir : str, optional 570 Directory to write the saved plots to. 576 ax = fig.add_subplot(111)
577 for old_wcs, ref
in zip(old_wcs_list, data_refs):
578 if ref.dataId[
'visit'] != visit:
580 md = ref.get(
'calexp_md')
583 dims.getX(), dims.getY())
586 length = (0.1*u.arcsecond).to(u.radian).value
587 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
590 plt.title(
'visit: {}'.format(visit))
591 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
592 plt.savefig(filename.format(name, visit))
597 Plot the delta between wcs1 and wcs2 as vector arrows. 602 Matplotlib axis instance to plot to. 603 wcs1 : lsst.afw.image.wcs.Wcs 604 First WCS to compare. 605 wcs2 : lsst.afw.image.wcs.Wcs 606 Second WCS to compare. 608 Size of array in X-coordinate to make the grid over. 610 Size of array in Y-coordinate to make the grid over. 616 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
620 """Plot the magnitude of the WCS change between old and new visits as a heat map. 624 plt : matplotlib.pyplot instance 625 pyplot instance to plot with. 626 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 627 A list of data refs to plot. 628 visits : list of visit id (usually int) 629 list of visit identifiers, one-to-one correspondent with catalogs. 630 old_wcs_list : list of lsst.afw.image.wcs.Wcs 631 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 633 Name to include in plot titles and save files. 634 outdir : str, optional 635 Directory to write the saved plots to. 639 fig.set_tight_layout(
True)
640 ax = fig.add_subplot(111)
646 for old_wcs, ref
in zip(old_wcs_list, data_refs):
647 if ref.dataId[
'visit'] != visit:
649 md = ref.get(
'calexp_md')
652 old_wcs, ref.get(
'jointcal_wcs'))
655 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
656 xmin = min(x1.min(), xmin)
657 ymin = min(y1.min(), ymin)
658 xmax = max(x1.max(), xmax)
659 ymax = max(y1.max(), ymax)
660 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
661 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
662 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
668 cbar = plt.colorbar(img)
669 cbar.ax.set_ylabel(
'distortion (arcseconds)')
674 plt.title(
'visit: {}'.format(visit))
675 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
676 plt.savefig(filename.format(name, visit))
679 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
680 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid. 684 plt : matplotlib.pyplot instance 685 pyplot instance to plot with. 686 wcs1 : lsst.afw.image.wcs.Wcs 687 First WCS to compare. 688 wcs2 : lsst.afw.image.wcs.Wcs 689 Second WCS to compare. 691 Size of array in X-coordinate to make the grid over. 693 Size of array in Y-coordinate to make the grid over. 694 center : tuple, optional 695 Center of the data, in on-chip coordinates. 697 Name to include in plot titles and save files. 698 outdir : str, optional 699 Directory to write the saved plots to. 705 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
706 plt.xlabel(
'delta RA (arcsec)')
707 plt.ylabel(
'delta Dec (arcsec)')
709 filename = os.path.join(outdir,
'{}-wcs.pdf')
710 plt.savefig(filename.format(name))
714 new_rms_relative, new_rms_absolute,
715 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
716 name="", outdir='.plots'):
717 """Plot histograms of the source separations and their RMS values. 721 plt : matplotlib.pyplot instance 722 pyplot instance to plot with. 723 old_rms_relative : np.array 724 old relative rms/star 725 old_rms_absolute : np.array 726 old absolute rms/star 727 new_rms_relative : np.array 728 new relative rms/star 729 new_rms_absolute : np.array 730 new absolute rms/star 731 old_rel_total : float 732 old relative rms over all stars 733 old_abs_total : float 734 old absolute rms over all stars 735 new_rel_total : float 736 new relative rms over all stars 737 new_abs_total : float 738 new absolute rms over all stars 740 Name to include in plot titles and save files. 741 outdir : str, optional 742 Directory to write the saved plots to. 750 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
751 'bins': 30,
'histtype':
'step'}
753 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
755 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
756 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
758 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
759 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
761 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
762 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
763 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
764 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
766 plt.xlim(plotOptions[
'range'])
767 plt.xlabel(
'arcseconds')
768 plt.legend(loc=
'best')
769 filename = os.path.join(outdir,
'{}-histogram.pdf')
770 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)
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)
template SourceMatchVector matchRaDec(SourceCatalog const &, lsst::geom::Angle, MatchControl const &)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
def plot_all_wcs_deltas(plt, data_refs, visits, old_wcs_list, per_ccd_plot=False, name='', outdir='.plots')
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 make_xy_wcs_grid(x_dim, y_dim, wcs1, wcs2, num=50)
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)
def plot_wcs_magnitude(plt, data_refs, visits, old_wcs_list, name, outdir='.plots')
static Log getLogger(std::string const &loggername)
def plot_all_wcs_quivers(plt, data_refs, visits, old_wcs_list, name, outdir='.plots')
lsst::geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
def _make_visit_catalogs(self, catalogs, visits)