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
45 __all__ = [
'JointcalStatistics']
47 MatchDict = collections.namedtuple(
'MatchDict', [
'relative',
'absolute'])
52 Compute statistics on jointcal-processed data, and optionally generate plots. 56 Instantiate JointcalStatistics and call compute_rms() to get the relevant 57 statistics for e.g. unittests, and call make_plots() to generate a suite of 61 def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
62 do_photometry=True, do_astrometry=True,
67 match_radius : lsst.afw.geom.Angle 68 match sources within this radius for RMS statistics 70 Signal/Noise (flux/fluxErr) for sources to be included in the RMS cross-match. 71 100 is a balance between good centroids and enough sources. 72 do_photometry : bool, optional 73 Perform calculations/make plots for photometric metrics. 74 do_astrometry : bool, optional 75 Perform calculations/make plots for astrometric metrics. 76 verbose : bool, optional 88 Match all data_refs to compute the RMS, for all detections above self.flux_limit. 92 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 93 A list of data refs to do the calculations between. 94 reference : lsst reference catalog 95 reference catalog to do absolute matching against. 101 Post-jointcal relative RMS of the matched sources. 103 Post-jointcal absolute RMS of matched sources. 105 Post-jointcal photometric repeatability (PA1 from the SRD). 109 self.
filters = [ref.get(
'calexp_filter').getName()
for ref
in data_refs]
112 def compute(catalogs, photoCalibs):
113 """Compute the relative and absolute matches in distance and flux.""" 123 refcalib = photoCalibs[0]
if photoCalibs != []
else None 124 dist_rel, flux_rel, ref_flux_rel, source_rel = self.
_make_match_dict(refcat,
128 dist_abs, flux_abs, ref_flux_abs, source_abs = self.
_make_match_dict(reference,
133 ref_flux =
MatchDict(ref_flux_rel, ref_flux_abs)
134 source =
MatchDict(source_rel, source_abs)
135 return dist, flux, ref_flux, source
137 old_cats = [ref.get(
'src')
for ref
in data_refs]
142 for ref
in data_refs:
143 calib = ref.get(
'calexp_calib')
144 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)
238 def rms_per_source(data):
239 """Each element of data must already be the "delta" of whatever measurement.""" 240 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).
to(u.arcsecond)
251 new_dist_rms.relative, new_dist_rms.absolute,
254 name=name, outdir=outdir)
257 per_ccd_plot=per_ccd_plot,
258 name=name, outdir=outdir)
265 def _photometric_rms(self, sn_cut=300, magnitude_range=3):
267 Compute the photometric RMS and the photometric repeatablity values (PA1). 272 The minimum signal/noise for sources to be included in the PA1 calculation. 273 magnitude_range : float 274 The range of magnitudes above sn_cut to include in the PA1 calculation. 276 def rms(flux, ref_flux):
277 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
283 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
284 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
288 def signal_to_noise(sources, flux_key='slot_PsfFlux_instFlux', sigma_key='slot_PsfFlux_instFluxErr'):
289 """Compute the mean signal/noise per source from a MatchDict of SourceRecords.""" 290 result = np.empty(len(sources))
291 for i, src
in enumerate(sources.values()):
292 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
295 old_sn = signal_to_noise(self.
old_source.absolute)
308 def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None):
310 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations. 314 reference : lsst.afw.table.SourceCatalog 315 Catalog to do the matching against. 316 visit_catalogs : list of lsst.afw.table.SourceCatalog 317 Visit source catalogs (values() produced by _make_visit_catalogs) 318 to cross-match against reference. 319 photoCalibs : list of lsst.afw.image.PhotoCalib 320 Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs. 321 refcalib : lsst.afw.image.PhotoCalib or None 322 Pass a PhotoCalib here to use it to compute Janskys from the 323 reference catalog ADU slot_flux. 328 dict of sourceID: array(separation distances for that source) 330 dict of sourceID: array(fluxes (Jy) for that source) 332 dict of sourceID: flux (Jy) of the reference object 334 dict of sourceID: list(each SourceRecord that was position-matched 338 if photoCalibs == []:
339 photoCalibs = [[]]*len(visit_catalogs)
341 distances = collections.defaultdict(list)
342 fluxes = collections.defaultdict(list)
344 sources = collections.defaultdict(list)
345 if 'slot_CalibFlux_instFlux' in reference.schema:
346 ref_flux_key =
'slot_CalibFlux' 348 ref_flux_key =
'{}_flux' 350 def get_fluxes(photoCalib, match):
351 """Return (flux, ref_flux) or None if either is invalid.""" 353 maggiesToJansky = 3631
354 flux = match[1][
'slot_CalibFlux_instFlux']
358 flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[1],
"slot_CalibFlux").value
361 if 'slot' in ref_flux_key:
362 ref_flux = match[0][ref_flux_key+
'_flux']
366 ref_flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[0], ref_flux_key).value
369 ref_flux = match[0][ref_flux_key.format(filt)]
373 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
374 return Flux(flux, ref_flux)
376 for cat, photoCalib, filt
in zip(visit_catalogs, photoCalibs, self.
filters):
377 good = (cat.get(
'base_PsfFlux_instFlux')/cat.get(
'base_PsfFlux_instFluxErr')) > self.
flux_limit 379 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
383 flux = get_fluxes(photoCalib, m)
387 fluxes[m[0].getId()].
append(flux.flux)
389 ref_fluxes[m[0].getId()] = flux.ref_flux
393 distances[m[0].getId()].
append(m[2])
395 sources[m[0].getId()].
append(m[1])
397 for source
in distances:
398 distances[source] = np.array(distances[source])
399 for source
in fluxes:
400 fluxes[source] = np.array(fluxes[source])
402 return distances, fluxes, ref_fluxes, sources
404 def _make_visit_catalogs(self, catalogs, visits):
406 Merge all catalogs from the each visit. 407 NOTE: creating this structure is somewhat slow, and will be unnecessary 408 once a full-visit composite dataset is available. 412 catalogs : list of lsst.afw.table.SourceCatalog 413 Catalogs to combine into per-visit catalogs. 414 visits : list of visit id (usually int) 415 list of visit identifiers, one-to-one correspondent with catalogs. 420 dict of visit: catalog of all sources from all CCDs of that visit. 423 for v, cat
in zip(visits, catalogs):
424 visit_dict[v].extend(cat)
427 visit_dict[v] = visit_dict[v].copy(deep=
True)
433 faint, bright, old_PA1, new_PA1,
434 name='', outdir='.plots'):
435 """Plot various distributions of fluxes and magnitudes. 439 plt : matplotlib.pyplot instance 440 pyplot instance to plot with 445 old_weighted_rms : np.array 446 old rms weighted by the mean (rms(data)/mean(data)) 447 new_weighted_rms : np.array 448 old rms weighted by the mean (rms(data)/mean(data)) 450 Faint end of range that PA1 was computed from. 452 Bright end of range that PA1 was computed from. 454 Old value of PA1, to plot as horizontal line. 456 New value of PA1, to plot as horizontal line. 458 Name to include in plot titles and save files. 459 outdir : str, optional 460 Directory to write the saved plots to. 464 seaborn.set_style(
'whitegrid')
470 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
471 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
472 plt.axvline(faint, ls=
':', color=old_color)
473 plt.axvline(bright, ls=
':', color=old_color)
474 plt.axhline(old_PA1, ls=
'--', color=old_color)
475 plt.axhline(new_PA1, ls=
'--', color=new_color)
476 plt.legend(loc=
'upper left')
477 plt.title(
'Where is the systematic flux rms limit?')
478 plt.xlabel(
'magnitude')
479 plt.ylabel(
'rms/mean per source')
480 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
481 plt.savefig(filename.format(name))
484 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
485 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
486 plt.title(
'Source RMS pre/post-jointcal')
487 plt.xlabel(
'rms(flux)/mean(flux)')
489 plt.legend(loc=
'upper right')
490 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
491 plt.savefig(filename.format(name))
495 name='', outdir='.plots'):
497 Various plots of the difference between old and new Wcs. 501 plt : matplotlib.pyplot instance 502 pyplot instance to plot with. 503 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 504 A list of data refs to plot. 505 visits : list of visit id (usually int) 506 list of visit identifiers, one-to-one correspondent with catalogs. 507 old_wcs_list : list of lsst.afw.image.wcs.Wcs 508 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 509 per_ccd_plot : bool, optional 510 Make per-ccd plots of the "wcs different" (warning: slow!) 512 Name to include in plot titles and save files. 513 outdir : str, optional 514 Directory to write the saved plots to. 521 for i, ref
in enumerate(data_refs):
522 md = ref.get(
'calexp_md')
524 plot_wcs(plt, old_wcs_list[i], ref.get(
'jointcal_wcs'),
525 dims.getX(), dims.getY(),
526 center=(md.getScalar(
'CRVAL1'), md.getScalar(
'CRVAL2')), name=
'dataRef %d'%i,
531 """Return num x/y grid coordinates for wcs1 and wcs2.""" 532 x = np.linspace(0, x_dim, num)
533 y = np.linspace(0, y_dim, num)
536 return x1, y1, x2, y2
540 """Convert two arrays of x/y points into an on-sky grid.""" 541 xout = np.zeros((xv.shape[0], yv.shape[0]))
542 yout = np.zeros((xv.shape[0], yv.shape[0]))
543 for i, x
in enumerate(xv):
544 for j, y
in enumerate(yv):
545 sky = wcs.pixelToSky(x, y)
546 xout[i, j] = sky.getRa()
547 yout[i, j] = sky.getDec()
553 Make quiver plots of the WCS deltas for each CCD in each visit. 557 plt : matplotlib.pyplot instance 558 pyplot instance to plot with. 559 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 560 A list of data refs to plot. 561 visits : list of visit id (usually int) 562 list of visit identifiers, one-to-one correspondent with catalogs. 563 old_wcs_list : list of lsst.afw.image.wcs.Wcs 564 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 566 Name to include in plot titles and save files. 567 outdir : str, optional 568 Directory to write the saved plots to. 574 ax = fig.add_subplot(111)
575 for old_wcs, ref
in zip(old_wcs_list, data_refs):
576 if ref.dataId[
'visit'] != visit:
578 md = ref.get(
'calexp_md')
581 dims.getX(), dims.getY())
584 length = (0.1*u.arcsecond).
to(u.radian).value
585 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
588 plt.title(
'visit: {}'.
format(visit))
589 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
590 plt.savefig(filename.format(name, visit))
595 Plot the delta between wcs1 and wcs2 as vector arrows. 600 Matplotlib axis instance to plot to. 601 wcs1 : lsst.afw.image.wcs.Wcs 602 First WCS to compare. 603 wcs2 : lsst.afw.image.wcs.Wcs 604 Second WCS to compare. 606 Size of array in X-coordinate to make the grid over. 608 Size of array in Y-coordinate to make the grid over. 614 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
618 """Plot the magnitude of the WCS change between old and new visits as a heat map. 622 plt : matplotlib.pyplot instance 623 pyplot instance to plot with. 624 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 625 A list of data refs to plot. 626 visits : list of visit id (usually int) 627 list of visit identifiers, one-to-one correspondent with catalogs. 628 old_wcs_list : list of lsst.afw.image.wcs.Wcs 629 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 631 Name to include in plot titles and save files. 632 outdir : str, optional 633 Directory to write the saved plots to. 637 fig.set_tight_layout(
True)
638 ax = fig.add_subplot(111)
644 for old_wcs, ref
in zip(old_wcs_list, data_refs):
645 if ref.dataId[
'visit'] != visit:
647 md = ref.get(
'calexp_md')
650 old_wcs, ref.get(
'jointcal_wcs'))
653 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
654 xmin =
min(x1.min(), xmin)
655 ymin =
min(y1.min(), ymin)
656 xmax =
max(x1.max(), xmax)
657 ymax =
max(y1.max(), ymax)
658 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).
to(u.arcsecond).value
659 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
660 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
666 cbar = plt.colorbar(img)
667 cbar.ax.set_ylabel(
'distortion (arcseconds)')
672 plt.title(
'visit: {}'.
format(visit))
673 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
674 plt.savefig(filename.format(name, visit))
677 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
678 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid. 682 plt : matplotlib.pyplot instance 683 pyplot instance to plot with. 684 wcs1 : lsst.afw.image.wcs.Wcs 685 First WCS to compare. 686 wcs2 : lsst.afw.image.wcs.Wcs 687 Second WCS to compare. 689 Size of array in X-coordinate to make the grid over. 691 Size of array in Y-coordinate to make the grid over. 692 center : tuple, optional 693 Center of the data, in on-chip coordinates. 695 Name to include in plot titles and save files. 696 outdir : str, optional 697 Directory to write the saved plots to. 703 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
704 plt.xlabel(
'delta RA (arcsec)')
705 plt.ylabel(
'delta Dec (arcsec)')
707 filename = os.path.join(outdir,
'{}-wcs.pdf')
708 plt.savefig(filename.format(name))
712 new_rms_relative, new_rms_absolute,
713 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
714 name="", outdir='.plots'):
715 """Plot histograms of the source separations and their RMS values. 719 plt : matplotlib.pyplot instance 720 pyplot instance to plot with. 721 old_rms_relative : np.array 722 old relative rms/star 723 old_rms_absolute : np.array 724 old absolute rms/star 725 new_rms_relative : np.array 726 new relative rms/star 727 new_rms_absolute : np.array 728 new absolute rms/star 729 old_rel_total : float 730 old relative rms over all stars 731 old_abs_total : float 732 old absolute rms over all stars 733 new_rel_total : float 734 new relative rms over all stars 735 new_abs_total : float 736 new absolute rms over all stars 738 Name to include in plot titles and save files. 739 outdir : str, optional 740 Directory to write the saved plots to. 748 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
749 'bins': 30,
'histtype':
'step'}
751 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
753 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
754 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
756 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
757 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
759 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
760 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
761 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
762 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
764 plt.xlim(plotOptions[
'range'])
765 plt.xlabel(
'arcseconds')
766 plt.legend(loc=
'best')
767 filename = os.path.join(outdir,
'{}-histogram.pdf')
768 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.
template SourceMatchVector matchRaDec(SourceCatalog const &, lsst::geom::Angle, MatchControl const &)
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')
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)
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')
lsst::geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
def _make_visit_catalogs(self, catalogs, visits)