3 Statistics of jointcal vs. single-frame procesing and diagnostic plots. 7 Some of the algorithms and data structures in this code are temporary 8 kludges and will no longer be necessary once the following are available: 10 - a composite data structure that contains all ccds from a single visit 11 - an n-way matching system that preserves the separations between sources 13 from __future__
import division, print_function, absolute_import
14 from builtins
import zip
15 from builtins
import object
20 from astropy
import units
as u
28 __all__ = [
'JointcalStatistics']
30 MatchDict = collections.namedtuple(
'MatchDict', [
'relative',
'absolute'])
35 Compute statistics on jointcal-processed data, and optionally generate plots. 39 Instantiate JointcalStatistics and call compute_rms() to get the relevant 40 statistics for e.g. unittests, and call make_plots() to generate a suite of 44 def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
45 do_photometry=True, do_astrometry=True,
50 match_radius : lsst.afw.geom.Angle 51 match sources within this radius for RMS statistics 53 Signal/Noise (flux/fluxSigma) for sources to be included in the RMS cross-match. 54 100 is a balance between good centroids and enough sources. 55 do_photometry : bool, optional 56 Perform calculations/make plots for photometric metrics. 57 do_astrometry : bool, optional 58 Perform calculations/make plots for astrometric metrics. 59 verbose : bool, optional 71 Match all data_refs to compute the RMS, for all detections above self.flux_limit. 75 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 76 A list of data refs to do the calculations between. 77 reference : lsst reference catalog 78 reference catalog to do absolute matching against. 84 Post-jointcal relative RMS of the matched sources. 86 Post-jointcal absolute RMS of matched sources. 88 Post-jointcal photometric repeatability (PA1 from the SRD). 92 self.
filters = [ref.get(
'calexp_filter').getName()
for ref
in data_refs]
95 def compute(catalogs, photoCalibs):
96 """Compute the relative and absolute matches in distance and flux.""" 106 refcalib = photoCalibs[0]
if photoCalibs != []
else None 107 dist_rel, flux_rel, ref_flux_rel, source_rel = self.
_make_match_dict(refcat,
111 dist_abs, flux_abs, ref_flux_abs, source_abs = self.
_make_match_dict(reference,
116 ref_flux =
MatchDict(ref_flux_rel, ref_flux_abs)
117 source =
MatchDict(source_rel, source_abs)
118 return dist, flux, ref_flux, source
120 old_cats = [ref.get(
'src')
for ref
in data_refs]
125 for ref
in data_refs:
126 calib = ref.get(
'calexp_calib')
127 fluxMag0 = calib.getFluxMag0()
130 self.old_dist, self.old_flux, self.old_ref_flux, self.
old_source = compute(old_cats, old_calibs)
133 new_cats = [ref.get(
'src')
for ref
in data_refs]
136 new_wcss = [ref.get(
'jointcal_wcs')
for ref
in data_refs]
139 new_calibs = [ref.get(
'jointcal_photoCalib')
for ref
in data_refs]
141 for wcs, cat
in zip(new_wcss, new_cats):
145 self.new_dist, self.new_flux, self.new_ref_flux, self.
new_source = compute(new_cats, new_calibs)
148 print(
'old, new relative distance matches:',
149 len(self.old_dist.relative), len(self.new_dist.relative))
150 print(
'old, new absolute distance matches:',
151 len(self.old_dist.absolute), len(self.new_dist.absolute))
152 print(
'old, new relative flux matches:',
153 len(self.old_flux.relative), len(self.new_flux.relative))
154 print(
'old, new absolute flux matches:',
155 len(self.old_flux.absolute), len(self.new_flux.absolute))
163 """Compute the total rms across all sources.""" 164 total = sum(sum(dd**2)
for dd
in data.values())
165 n = sum(len(dd)
for dd
in data.values())
166 return np.sqrt(total/n)
175 Rms_result = collections.namedtuple(
"Rms_result", [
"dist_relative",
"dist_absolute",
"pa1"])
179 name='', interactive=False, per_ccd_plot=False, outdir='.plots'):
181 Make plots of various quantites to help with debugging. 182 Requires that `compute_rms()` was run first. 186 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 187 A list of data refs to do the calculations between. 188 old_wcs_list : list of lsst.afw.image.wcs.Wcs 189 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 191 Name to include in plot titles and save files. 193 Turn on matplotlib interactive mode and drop into a debugger when 194 plotting is finished. Otherwise, use a non-interactive backend. 196 Plot the WCS per CCD (takes longer and generates many plots for a large camera) 198 directory to save plots to. 204 matplotlib.use(
'pdf')
206 import matplotlib.pyplot
as plt
207 import astropy.visualization
209 astropy.visualization.quantity_support()
213 self.
log.
info(
"N data_refs: %d", len(data_refs))
219 name=name, outdir=outdir)
221 def rms_per_source(data):
222 """Each element of data must already be the "delta" of whatever measurement.""" 223 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).to(u.arcsecond)
234 new_dist_rms.relative, new_dist_rms.absolute,
237 name=name, outdir=outdir)
240 per_ccd_plot=per_ccd_plot,
241 name=name, outdir=outdir)
248 def _photometric_rms(self, sn_cut=300, magnitude_range=3):
250 Compute the photometric RMS and the photometric repeatablity values (PA1). 255 The minimum signal/noise for sources to be included in the PA1 calculation. 256 magnitude_range : float 257 The range of magnitudes above sn_cut to include in the PA1 calculation. 259 def rms(flux, ref_flux):
260 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
266 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
267 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
271 def signal_to_noise(sources, flux_key='slot_PsfFlux_flux', sigma_key='slot_PsfFlux_fluxSigma'):
272 """Compute the mean signal/noise per source from a MatchDict of SourceRecords.""" 273 result = np.empty(len(sources))
274 for i, src
in enumerate(sources.values()):
275 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
278 old_sn = signal_to_noise(self.
old_source.absolute)
291 def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None):
293 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations. 297 reference : lsst.afw.table.SourceCatalog 298 Catalog to do the matching against. 299 visit_catalogs : list of lsst.afw.table.SourceCatalog 300 Visit source catalogs (values() produced by _make_visit_catalogs) 301 to cross-match against reference. 302 photoCalibs : list of lsst.afw.image.PhotoCalib 303 Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs. 304 refcalib : lsst.afw.image.PhotoCalib or None 305 Pass a PhotoCalib here to use it to compute Janskys from the 306 reference catalog ADU slot_flux. 311 dict of sourceID: array(separation distances for that source) 313 dict of sourceID: array(fluxes (Jy) for that source) 315 dict of sourceID: flux (Jy) of the reference object 317 dict of sourceID: list(each SourceRecord that was position-matched 321 if photoCalibs == []:
322 photoCalibs = [[]]*len(visit_catalogs)
324 distances = collections.defaultdict(list)
325 fluxes = collections.defaultdict(list)
327 sources = collections.defaultdict(list)
328 if 'slot_CalibFlux_flux' in reference.schema:
329 ref_flux_key =
'slot_CalibFlux' 331 ref_flux_key =
'{}_flux' 333 def get_fluxes(photoCalib, match):
334 """Return (flux, ref_flux) or None if either is invalid.""" 336 maggiesToJansky = 3631
337 flux = match[1][
'slot_CalibFlux_flux']
341 flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[1],
"slot_CalibFlux").value
344 if 'slot' in ref_flux_key:
345 ref_flux = match[0][ref_flux_key+
'_flux']
349 ref_flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[0], ref_flux_key).value
352 ref_flux = match[0][ref_flux_key.format(filt)]
356 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
357 return Flux(flux, ref_flux)
359 for cat, photoCalib, filt
in zip(visit_catalogs, photoCalibs, self.
filters):
360 good = (cat.get(
'base_PsfFlux_flux')/cat.get(
'base_PsfFlux_fluxSigma')) > self.
flux_limit 362 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
366 flux = get_fluxes(photoCalib, m)
370 fluxes[m[0].getId()].
append(flux.flux)
372 ref_fluxes[m[0].getId()] = flux.ref_flux
376 distances[m[0].getId()].
append(m[2])
378 sources[m[0].getId()].
append(m[1])
380 for source
in distances:
381 distances[source] = np.array(distances[source])
382 for source
in fluxes:
383 fluxes[source] = np.array(fluxes[source])
385 return distances, fluxes, ref_fluxes, sources
387 def _make_visit_catalogs(self, catalogs, visits):
389 Merge all catalogs from the each visit. 390 NOTE: creating this structure is somewhat slow, and will be unnecessary 391 once a full-visit composite dataset is available. 395 catalogs : list of lsst.afw.table.SourceCatalog 396 Catalogs to combine into per-visit catalogs. 397 visits : list of visit id (usually int) 398 list of visit identifiers, one-to-one correspondent with catalogs. 403 dict of visit: catalog of all sources from all CCDs of that visit. 406 for v, cat
in zip(visits, catalogs):
407 visit_dict[v].extend(cat)
410 visit_dict[v] = visit_dict[v].copy(deep=
True)
416 faint, bright, old_PA1, new_PA1,
417 name='', outdir='.plots'):
418 """Plot various distributions of fluxes and magnitudes. 422 plt : matplotlib.pyplot instance 423 pyplot instance to plot with 428 old_weighted_rms : np.array 429 old rms weighted by the mean (rms(data)/mean(data)) 430 new_weighted_rms : np.array 431 old rms weighted by the mean (rms(data)/mean(data)) 433 Faint end of range that PA1 was computed from. 435 Bright end of range that PA1 was computed from. 437 Old value of PA1, to plot as horizontal line. 439 New value of PA1, to plot as horizontal line. 441 Name to include in plot titles and save files. 442 outdir : str, optional 443 Directory to write the saved plots to. 447 seaborn.set_style(
'whitegrid')
453 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
454 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
455 plt.axvline(faint, ls=
':', color=old_color)
456 plt.axvline(bright, ls=
':', color=old_color)
457 plt.axhline(old_PA1, ls=
'--', color=old_color)
458 plt.axhline(new_PA1, ls=
'--', color=new_color)
459 plt.legend(loc=
'upper left')
460 plt.title(
'Where is the systematic flux rms limit?')
461 plt.xlabel(
'magnitude')
462 plt.ylabel(
'rms/mean per source')
463 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
464 plt.savefig(filename.format(name))
467 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
468 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
469 plt.title(
'Source RMS pre/post-jointcal')
470 plt.xlabel(
'rms(flux)/mean(flux)')
472 plt.legend(loc=
'upper right')
473 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
474 plt.savefig(filename.format(name))
478 name='', outdir='.plots'):
480 Various plots of the difference between old and new Wcs. 484 plt : matplotlib.pyplot instance 485 pyplot instance to plot with. 486 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 487 A list of data refs to plot. 488 visits : list of visit id (usually int) 489 list of visit identifiers, one-to-one correspondent with catalogs. 490 old_wcs_list : list of lsst.afw.image.wcs.Wcs 491 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 492 per_ccd_plot : bool, optional 493 Make per-ccd plots of the "wcs different" (warning: slow!) 495 Name to include in plot titles and save files. 496 outdir : str, optional 497 Directory to write the saved plots to. 504 for i, ref
in enumerate(data_refs):
505 md = ref.get(
'calexp_md')
507 plot_wcs(plt, old_wcs_list[i], ref.get(
'jointcal_wcs'),
508 dims.getX(), dims.getY(),
509 center=(md.get(
'CRVAL1'), md.get(
'CRVAL2')), name=
'dataRef %d'%i,
514 """Return num x/y grid coordinates for wcs1 and wcs2.""" 515 x = np.linspace(0, x_dim, num)
516 y = np.linspace(0, y_dim, num)
519 return x1, y1, x2, y2
523 """Convert two arrays of x/y points into an on-sky grid.""" 524 xout = np.zeros((xv.shape[0], yv.shape[0]))
525 yout = np.zeros((xv.shape[0], yv.shape[0]))
526 for i, x
in enumerate(xv):
527 for j, y
in enumerate(yv):
528 sky = wcs.pixelToSky(x, y).toFk5()
529 xout[i, j] = sky.getRa()
530 yout[i, j] = sky.getDec()
536 Make quiver plots of the WCS deltas for each CCD in each visit. 540 plt : matplotlib.pyplot instance 541 pyplot instance to plot with. 542 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 543 A list of data refs to plot. 544 visits : list of visit id (usually int) 545 list of visit identifiers, one-to-one correspondent with catalogs. 546 old_wcs_list : list of lsst.afw.image.wcs.Wcs 547 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 549 Name to include in plot titles and save files. 550 outdir : str, optional 551 Directory to write the saved plots to. 557 ax = fig.add_subplot(111)
558 for old_wcs, ref
in zip(old_wcs_list, data_refs):
559 if ref.dataId[
'visit'] != visit:
561 md = ref.get(
'calexp_md')
564 dims.getX(), dims.getY())
567 length = (0.1*u.arcsecond).to(u.radian).value
568 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
571 plt.title(
'visit: {}'.
format(visit))
572 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
573 plt.savefig(filename.format(name, visit))
578 Plot the delta between wcs1 and wcs2 as vector arrows. 583 Matplotlib axis instance to plot to. 584 wcs1 : lsst.afw.image.wcs.Wcs 585 First WCS to compare. 586 wcs2 : lsst.afw.image.wcs.Wcs 587 Second WCS to compare. 589 Size of array in X-coordinate to make the grid over. 591 Size of array in Y-coordinate to make the grid over. 597 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
601 """Plot the magnitude of the WCS change between old and new visits as a heat map. 605 plt : matplotlib.pyplot instance 606 pyplot instance to plot with. 607 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 608 A list of data refs to plot. 609 visits : list of visit id (usually int) 610 list of visit identifiers, one-to-one correspondent with catalogs. 611 old_wcs_list : list of lsst.afw.image.wcs.Wcs 612 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 614 Name to include in plot titles and save files. 615 outdir : str, optional 616 Directory to write the saved plots to. 620 fig.set_tight_layout(
True)
621 ax = fig.add_subplot(111)
627 for old_wcs, ref
in zip(old_wcs_list, data_refs):
628 if ref.dataId[
'visit'] != visit:
630 md = ref.get(
'calexp_md')
633 old_wcs, ref.get(
'jointcal_wcs'))
636 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
637 xmin =
min(x1.min(), xmin)
638 ymin =
min(y1.min(), ymin)
639 xmax =
max(x1.max(), xmax)
640 ymax =
max(y1.max(), ymax)
641 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
642 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
643 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
649 cbar = plt.colorbar(img)
650 cbar.ax.set_ylabel(
'distortion (arcseconds)')
655 plt.title(
'visit: {}'.
format(visit))
656 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
657 plt.savefig(filename.format(name, visit))
660 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
661 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid. 665 plt : matplotlib.pyplot instance 666 pyplot instance to plot with. 667 wcs1 : lsst.afw.image.wcs.Wcs 668 First WCS to compare. 669 wcs2 : lsst.afw.image.wcs.Wcs 670 Second WCS to compare. 672 Size of array in X-coordinate to make the grid over. 674 Size of array in Y-coordinate to make the grid over. 675 center : tuple, optional 676 Center of the data, in on-chip coordinates. 678 Name to include in plot titles and save files. 679 outdir : str, optional 680 Directory to write the saved plots to. 686 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
687 plt.xlabel(
'delta RA (arcsec)')
688 plt.ylabel(
'delta Dec (arcsec)')
690 filename = os.path.join(outdir,
'{}-wcs.pdf')
691 plt.savefig(filename.format(name))
695 new_rms_relative, new_rms_absolute,
696 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
697 name="", outdir='.plots'):
698 """Plot histograms of the source separations and their RMS values. 702 plt : matplotlib.pyplot instance 703 pyplot instance to plot with. 704 old_rms_relative : np.array 705 old relative rms/star 706 old_rms_absolute : np.array 707 old absolute rms/star 708 new_rms_relative : np.array 709 new relative rms/star 710 new_rms_absolute : np.array 711 new absolute rms/star 712 old_rel_total : float 713 old relative rms over all stars 714 old_abs_total : float 715 old absolute rms over all stars 716 new_rel_total : float 717 new relative rms over all stars 718 new_abs_total : float 719 new absolute rms over all stars 721 Name to include in plot titles and save files. 722 outdir : str, optional 723 Directory to write the saved plots to. 731 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
732 'bins': 30,
'histtype':
'step'}
734 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
736 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
737 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
739 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
740 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
742 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
743 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
744 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
745 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
747 plt.xlim(plotOptions[
'range'])
748 plt.xlabel(
'arcseconds')
749 plt.legend(loc=
'best')
750 filename = os.path.join(outdir,
'{}-histogram.pdf')
751 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.
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)
geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
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')
template SourceMatchVector matchRaDec(SourceCatalog const &, Angle, MatchControl const &)
def _make_visit_catalogs(self, catalogs, visits)