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 17 from astropy
import units
as u
25 __all__ = [
'JointcalStatistics']
27 MatchDict = collections.namedtuple(
'MatchDict', [
'relative',
'absolute'])
32 Compute statistics on jointcal-processed data, and optionally generate plots. 36 Instantiate JointcalStatistics and call compute_rms() to get the relevant 37 statistics for e.g. unittests, and call make_plots() to generate a suite of 41 def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
42 do_photometry=True, do_astrometry=True,
47 match_radius : lsst.afw.geom.Angle 48 match sources within this radius for RMS statistics 50 Signal/Noise (flux/fluxSigma) for sources to be included in the RMS cross-match. 51 100 is a balance between good centroids and enough sources. 52 do_photometry : bool, optional 53 Perform calculations/make plots for photometric metrics. 54 do_astrometry : bool, optional 55 Perform calculations/make plots for astrometric metrics. 56 verbose : bool, optional 68 Match all data_refs to compute the RMS, for all detections above self.flux_limit. 72 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 73 A list of data refs to do the calculations between. 74 reference : lsst reference catalog 75 reference catalog to do absolute matching against. 81 Post-jointcal relative RMS of the matched sources. 83 Post-jointcal absolute RMS of matched sources. 85 Post-jointcal photometric repeatability (PA1 from the SRD). 89 self.
filters = [ref.get(
'calexp_filter').getName()
for ref
in data_refs]
92 def compute(catalogs, photoCalibs):
93 """Compute the relative and absolute matches in distance and flux.""" 103 refcalib = photoCalibs[0]
if photoCalibs != []
else None 104 dist_rel, flux_rel, ref_flux_rel, source_rel = self.
_make_match_dict(refcat,
108 dist_abs, flux_abs, ref_flux_abs, source_abs = self.
_make_match_dict(reference,
113 ref_flux =
MatchDict(ref_flux_rel, ref_flux_abs)
114 source =
MatchDict(source_rel, source_abs)
115 return dist, flux, ref_flux, source
117 old_cats = [ref.get(
'src')
for ref
in data_refs]
122 for ref
in data_refs:
123 calib = ref.get(
'calexp_calib')
124 fluxMag0 = calib.getFluxMag0()
127 self.old_dist, self.old_flux, self.old_ref_flux, self.
old_source = compute(old_cats, old_calibs)
130 new_cats = [ref.get(
'src')
for ref
in data_refs]
133 new_wcss = [ref.get(
'jointcal_wcs')
for ref
in data_refs]
136 new_calibs = [ref.get(
'jointcal_photoCalib')
for ref
in data_refs]
138 for wcs, cat
in zip(new_wcss, new_cats):
142 self.new_dist, self.new_flux, self.new_ref_flux, self.
new_source = compute(new_cats, new_calibs)
145 print(
'old, new relative distance matches:',
146 len(self.old_dist.relative), len(self.new_dist.relative))
147 print(
'old, new absolute distance matches:',
148 len(self.old_dist.absolute), len(self.new_dist.absolute))
149 print(
'old, new relative flux matches:',
150 len(self.old_flux.relative), len(self.new_flux.relative))
151 print(
'old, new absolute flux matches:',
152 len(self.old_flux.absolute), len(self.new_flux.absolute))
160 """Compute the total rms across all sources.""" 161 total = sum(sum(dd**2)
for dd
in data.values())
162 n = sum(len(dd)
for dd
in data.values())
163 return np.sqrt(total/n)
172 Rms_result = collections.namedtuple(
"Rms_result", [
"dist_relative",
"dist_absolute",
"pa1"])
176 name='', interactive=False, per_ccd_plot=False, outdir='.plots'):
178 Make plots of various quantites to help with debugging. 179 Requires that `compute_rms()` was run first. 183 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 184 A list of data refs to do the calculations between. 185 old_wcs_list : list of lsst.afw.image.wcs.Wcs 186 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 188 Name to include in plot titles and save files. 190 Turn on matplotlib interactive mode and drop into a debugger when 191 plotting is finished. Otherwise, use a non-interactive backend. 193 Plot the WCS per CCD (takes longer and generates many plots for a large camera) 195 directory to save plots to. 201 matplotlib.use(
'pdf')
203 import matplotlib.pyplot
as plt
204 import astropy.visualization
206 astropy.visualization.quantity_support()
210 self.
log.
info(
"N data_refs: %d", len(data_refs))
216 name=name, outdir=outdir)
218 def rms_per_source(data):
219 """Each element of data must already be the "delta" of whatever measurement.""" 220 return (np.sqrt([np.mean(dd**2)
for dd
in data.values()])*u.radian).to(u.arcsecond)
231 new_dist_rms.relative, new_dist_rms.absolute,
234 name=name, outdir=outdir)
237 per_ccd_plot=per_ccd_plot,
238 name=name, outdir=outdir)
245 def _photometric_rms(self, sn_cut=300, magnitude_range=3):
247 Compute the photometric RMS and the photometric repeatablity values (PA1). 252 The minimum signal/noise for sources to be included in the PA1 calculation. 253 magnitude_range : float 254 The range of magnitudes above sn_cut to include in the PA1 calculation. 256 def rms(flux, ref_flux):
257 return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2)
for dd
in flux])
263 self.
old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
264 self.
new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
268 def signal_to_noise(sources, flux_key='slot_PsfFlux_flux', sigma_key='slot_PsfFlux_fluxSigma'):
269 """Compute the mean signal/noise per source from a MatchDict of SourceRecords.""" 270 result = np.empty(len(sources))
271 for i, src
in enumerate(sources.values()):
272 result[i] = np.mean([x[flux_key]/x[sigma_key]
for x
in src])
275 old_sn = signal_to_noise(self.
old_source.absolute)
288 def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None):
290 Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations. 294 reference : lsst.afw.table.SourceCatalog 295 Catalog to do the matching against. 296 visit_catalogs : list of lsst.afw.table.SourceCatalog 297 Visit source catalogs (values() produced by _make_visit_catalogs) 298 to cross-match against reference. 299 photoCalibs : list of lsst.afw.image.PhotoCalib 300 Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs. 301 refcalib : lsst.afw.image.PhotoCalib or None 302 Pass a PhotoCalib here to use it to compute Janskys from the 303 reference catalog ADU slot_flux. 308 dict of sourceID: array(separation distances for that source) 310 dict of sourceID: array(fluxes (Jy) for that source) 312 dict of sourceID: flux (Jy) of the reference object 314 dict of sourceID: list(each SourceRecord that was position-matched 318 if photoCalibs == []:
319 photoCalibs = [[]]*len(visit_catalogs)
321 distances = collections.defaultdict(list)
322 fluxes = collections.defaultdict(list)
324 sources = collections.defaultdict(list)
325 if 'slot_CalibFlux_flux' in reference.schema:
326 ref_flux_key =
'slot_CalibFlux' 328 ref_flux_key =
'{}_flux' 330 def get_fluxes(photoCalib, match):
331 """Return (flux, ref_flux) or None if either is invalid.""" 333 maggiesToJansky = 3631
334 flux = match[1][
'slot_CalibFlux_flux']
338 flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[1],
"slot_CalibFlux").value
341 if 'slot' in ref_flux_key:
342 ref_flux = match[0][ref_flux_key+
'_flux']
346 ref_flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[0], ref_flux_key).value
349 ref_flux = match[0][ref_flux_key.format(filt)]
353 Flux = collections.namedtuple(
'Flux', (
'flux',
'ref_flux'))
354 return Flux(flux, ref_flux)
356 for cat, photoCalib, filt
in zip(visit_catalogs, photoCalibs, self.
filters):
357 good = (cat.get(
'base_PsfFlux_flux')/cat.get(
'base_PsfFlux_fluxSigma')) > self.
flux_limit 359 good &= (cat.get(
'base_ClassificationExtendedness_value') == 0)
363 flux = get_fluxes(photoCalib, m)
367 fluxes[m[0].getId()].
append(flux.flux)
369 ref_fluxes[m[0].getId()] = flux.ref_flux
373 distances[m[0].getId()].
append(m[2])
375 sources[m[0].getId()].
append(m[1])
377 for source
in distances:
378 distances[source] = np.array(distances[source])
379 for source
in fluxes:
380 fluxes[source] = np.array(fluxes[source])
382 return distances, fluxes, ref_fluxes, sources
384 def _make_visit_catalogs(self, catalogs, visits):
386 Merge all catalogs from the each visit. 387 NOTE: creating this structure is somewhat slow, and will be unnecessary 388 once a full-visit composite dataset is available. 392 catalogs : list of lsst.afw.table.SourceCatalog 393 Catalogs to combine into per-visit catalogs. 394 visits : list of visit id (usually int) 395 list of visit identifiers, one-to-one correspondent with catalogs. 400 dict of visit: catalog of all sources from all CCDs of that visit. 403 for v, cat
in zip(visits, catalogs):
404 visit_dict[v].extend(cat)
407 visit_dict[v] = visit_dict[v].copy(deep=
True)
413 faint, bright, old_PA1, new_PA1,
414 name='', outdir='.plots'):
415 """Plot various distributions of fluxes and magnitudes. 419 plt : matplotlib.pyplot instance 420 pyplot instance to plot with 425 old_weighted_rms : np.array 426 old rms weighted by the mean (rms(data)/mean(data)) 427 new_weighted_rms : np.array 428 old rms weighted by the mean (rms(data)/mean(data)) 430 Faint end of range that PA1 was computed from. 432 Bright end of range that PA1 was computed from. 434 Old value of PA1, to plot as horizontal line. 436 New value of PA1, to plot as horizontal line. 438 Name to include in plot titles and save files. 439 outdir : str, optional 440 Directory to write the saved plots to. 444 seaborn.set_style(
'whitegrid')
450 plt.plot(old_mag, old_weighted_rms,
'.', color=old_color, label=
'old')
451 plt.plot(new_mag, new_weighted_rms,
'.', color=new_color, label=
'new')
452 plt.axvline(faint, ls=
':', color=old_color)
453 plt.axvline(bright, ls=
':', color=old_color)
454 plt.axhline(old_PA1, ls=
'--', color=old_color)
455 plt.axhline(new_PA1, ls=
'--', color=new_color)
456 plt.legend(loc=
'upper left')
457 plt.title(
'Where is the systematic flux rms limit?')
458 plt.xlabel(
'magnitude')
459 plt.ylabel(
'rms/mean per source')
460 filename = os.path.join(outdir,
'{}-photometry-PA1.pdf')
461 plt.savefig(filename.format(name))
464 seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"old", color=old_color)
465 seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=
False, label=
"new", color=new_color)
466 plt.title(
'Source RMS pre/post-jointcal')
467 plt.xlabel(
'rms(flux)/mean(flux)')
469 plt.legend(loc=
'upper right')
470 filename = os.path.join(outdir,
'{}-photometry-rms.pdf')
471 plt.savefig(filename.format(name))
475 name='', outdir='.plots'):
477 Various plots of the difference between old and new Wcs. 481 plt : matplotlib.pyplot instance 482 pyplot instance to plot with. 483 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 484 A list of data refs to plot. 485 visits : list of visit id (usually int) 486 list of visit identifiers, one-to-one correspondent with catalogs. 487 old_wcs_list : list of lsst.afw.image.wcs.Wcs 488 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 489 per_ccd_plot : bool, optional 490 Make per-ccd plots of the "wcs different" (warning: slow!) 492 Name to include in plot titles and save files. 493 outdir : str, optional 494 Directory to write the saved plots to. 501 for i, ref
in enumerate(data_refs):
502 md = ref.get(
'calexp_md')
504 plot_wcs(plt, old_wcs_list[i], ref.get(
'jointcal_wcs'),
505 dims.getX(), dims.getY(),
506 center=(md.getScalar(
'CRVAL1'), md.getScalar(
'CRVAL2')), name=
'dataRef %d'%i,
511 """Return num x/y grid coordinates for wcs1 and wcs2.""" 512 x = np.linspace(0, x_dim, num)
513 y = np.linspace(0, y_dim, num)
516 return x1, y1, x2, y2
520 """Convert two arrays of x/y points into an on-sky grid.""" 521 xout = np.zeros((xv.shape[0], yv.shape[0]))
522 yout = np.zeros((xv.shape[0], yv.shape[0]))
523 for i, x
in enumerate(xv):
524 for j, y
in enumerate(yv):
525 sky = wcs.pixelToSky(x, y)
526 xout[i, j] = sky.getRa()
527 yout[i, j] = sky.getDec()
533 Make quiver plots of the WCS deltas for each CCD in each visit. 537 plt : matplotlib.pyplot instance 538 pyplot instance to plot with. 539 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 540 A list of data refs to plot. 541 visits : list of visit id (usually int) 542 list of visit identifiers, one-to-one correspondent with catalogs. 543 old_wcs_list : list of lsst.afw.image.wcs.Wcs 544 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 546 Name to include in plot titles and save files. 547 outdir : str, optional 548 Directory to write the saved plots to. 554 ax = fig.add_subplot(111)
555 for old_wcs, ref
in zip(old_wcs_list, data_refs):
556 if ref.dataId[
'visit'] != visit:
558 md = ref.get(
'calexp_md')
561 dims.getX(), dims.getY())
564 length = (0.1*u.arcsecond).to(u.radian).value
565 ax.quiverkey(Q, 0.9, 0.95, length,
'0.1 arcsec', coordinates=
'figure', labelpos=
'W')
568 plt.title(
'visit: {}'.
format(visit))
569 filename = os.path.join(outdir,
'{}-{}-quivers.pdf')
570 plt.savefig(filename.format(name, visit))
575 Plot the delta between wcs1 and wcs2 as vector arrows. 580 Matplotlib axis instance to plot to. 581 wcs1 : lsst.afw.image.wcs.Wcs 582 First WCS to compare. 583 wcs2 : lsst.afw.image.wcs.Wcs 584 Second WCS to compare. 586 Size of array in X-coordinate to make the grid over. 588 Size of array in Y-coordinate to make the grid over. 594 return ax.quiver(x1, y1, uu, vv, units=
'x', pivot=
'tail', scale=1e-3, width=1e-5)
598 """Plot the magnitude of the WCS change between old and new visits as a heat map. 602 plt : matplotlib.pyplot instance 603 pyplot instance to plot with. 604 data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef 605 A list of data refs to plot. 606 visits : list of visit id (usually int) 607 list of visit identifiers, one-to-one correspondent with catalogs. 608 old_wcs_list : list of lsst.afw.image.wcs.Wcs 609 A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs. 611 Name to include in plot titles and save files. 612 outdir : str, optional 613 Directory to write the saved plots to. 617 fig.set_tight_layout(
True)
618 ax = fig.add_subplot(111)
624 for old_wcs, ref
in zip(old_wcs_list, data_refs):
625 if ref.dataId[
'visit'] != visit:
627 md = ref.get(
'calexp_md')
630 old_wcs, ref.get(
'jointcal_wcs'))
633 extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
634 xmin =
min(x1.min(), xmin)
635 ymin =
min(y1.min(), ymin)
636 xmax =
max(x1.max(), xmax)
637 ymax =
max(y1.max(), ymax)
638 magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
639 img = ax.imshow(magnitude, vmin=0, vmax=0.3,
640 aspect=
'auto', extent=extent, cmap=plt.get_cmap(
'magma'))
646 cbar = plt.colorbar(img)
647 cbar.ax.set_ylabel(
'distortion (arcseconds)')
652 plt.title(
'visit: {}'.
format(visit))
653 filename = os.path.join(outdir,
'{}-{}-heatmap.pdf')
654 plt.savefig(filename.format(name, visit))
657 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name=
"", outdir=
'.plots'):
658 """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid. 662 plt : matplotlib.pyplot instance 663 pyplot instance to plot with. 664 wcs1 : lsst.afw.image.wcs.Wcs 665 First WCS to compare. 666 wcs2 : lsst.afw.image.wcs.Wcs 667 Second WCS to compare. 669 Size of array in X-coordinate to make the grid over. 671 Size of array in Y-coordinate to make the grid over. 672 center : tuple, optional 673 Center of the data, in on-chip coordinates. 675 Name to include in plot titles and save files. 676 outdir : str, optional 677 Directory to write the saved plots to. 683 plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1],
'-')
684 plt.xlabel(
'delta RA (arcsec)')
685 plt.ylabel(
'delta Dec (arcsec)')
687 filename = os.path.join(outdir,
'{}-wcs.pdf')
688 plt.savefig(filename.format(name))
692 new_rms_relative, new_rms_absolute,
693 old_rel_total, old_abs_total, new_rel_total, new_abs_total,
694 name="", outdir='.plots'):
695 """Plot histograms of the source separations and their RMS values. 699 plt : matplotlib.pyplot instance 700 pyplot instance to plot with. 701 old_rms_relative : np.array 702 old relative rms/star 703 old_rms_absolute : np.array 704 old absolute rms/star 705 new_rms_relative : np.array 706 new relative rms/star 707 new_rms_absolute : np.array 708 new absolute rms/star 709 old_rel_total : float 710 old relative rms over all stars 711 old_abs_total : float 712 old absolute rms over all stars 713 new_rel_total : float 714 new relative rms over all stars 715 new_abs_total : float 716 new absolute rms over all stars 718 Name to include in plot titles and save files. 719 outdir : str, optional 720 Directory to write the saved plots to. 728 plotOptions = {
'lw': 2,
'range': (0, 0.1)*u.arcsecond,
'normed':
True,
729 'bins': 30,
'histtype':
'step'}
731 plt.title(
'relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
733 plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label=
'old abs', **plotOptions)
734 plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label=
'new abs', **plotOptions)
736 plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label=
'old rel', **plotOptions)
737 plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label=
'new rel', **plotOptions)
739 plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
740 plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
741 plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
742 plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
744 plt.xlim(plotOptions[
'range'])
745 plt.xlabel(
'arcseconds')
746 plt.legend(loc=
'best')
747 filename = os.path.join(outdir,
'{}-histogram.pdf')
748 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)