LSSTApplications  11.0-24-g0a022a1,14.0+77,15.0,15.0+1
LSSTDataManagementBasePackage
utils.py
Go to the documentation of this file.
1 # See COPYRIGHT file at the top of the source tree.
2 """
3 Statistics of jointcal vs. single-frame procesing and diagnostic plots.
4 
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
9 """
10 from __future__ import division, print_function, absolute_import
11 from builtins import zip
12 from builtins import object
13 import collections
14 import os
15 
16 import numpy as np
17 from astropy import units as u
18 
19 import lsst.log
20 import lsst.afw.table
21 import lsst.afw.image
22 from lsst.afw.image import abMagFromFlux
23 from lsst.afw.geom import arcseconds
24 
25 MatchDict = collections.namedtuple('MatchDict', ['relative', 'absolute'])
26 
27 
29  """
30  Compute statistics on jointcal-processed data, and optionally generate plots.
31 
32  Notes
33  -----
34  Instantiate JointcalStatistics and call compute_rms() to get the relevant
35  statistics for e.g. unittests, and call make_plots() to generate a suite of
36  diagnostic plots.
37  """
38 
39  def __init__(self, match_radius=0.1*arcseconds, flux_limit=100.0,
40  do_photometry=True, do_astrometry=True,
41  verbose=False):
42  """
43  Parameters
44  ----------
45  match_radius : lsst.afw.Angle
46  match sources within this radius for RMS statistics
47  flux_limit : float
48  Signal/Noise (flux/fluxSigma) for sources to be included in the RMS cross-match.
49  100 is a balance between good centroids and enough sources.
50  do_photometry : bool, optional
51  Perform calculations/make plots for photometric metrics.
52  do_astrometry : bool, optional
53  Perform calculations/make plots for astrometric metrics.
54  verbose : bool, optional
55  Print extra things
56  """
57  self.match_radius = match_radius
58  self.flux_limit = flux_limit
59  self.do_photometry = do_photometry
60  self.do_astrometry = do_astrometry
61  self.verbose = verbose
62  self.log = lsst.log.Log.getLogger('JointcalStatistics')
63 
64  def compute_rms(self, data_refs, reference):
65  """
66  Match all data_refs to compute the RMS, for all detections above self.flux_limit.
67 
68  Parameters
69  ----------
70  data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
71  A list of data refs to do the calculations between.
72  reference : lsst reference catalog
73  reference catalog to do absolute matching against.
74 
75  Return
76  ------
77  namedtuple:
78  astropy.Quantity
79  Post-jointcal relative RMS of the matched sources.
80  astropy.Quantity
81  Post-jointcal absolute RMS of matched sources.
82  float
83  Post-jointcal photometric repeatability (PA1 from the SRD).
84  """
85 
86  # DECAM doesn't have "filter" in its registry, so we have to get the filter names directly.
87  self.filters = [ref.get('calexp_filter').getName() for ref in data_refs]
88  self.visits_per_dataRef = [ref.dataId['visit'] for ref in data_refs]
89 
90  def compute(catalogs, photoCalibs):
91  """Compute the relative and absolute matches in distance and flux."""
92  visit_catalogs = self._make_visit_catalogs(catalogs, self.visits_per_dataRef)
93  catalogs = [visit_catalogs[x] for x in self.visits_per_dataRef]
94  # use the first catalog as the relative reference catalog
95  # NOTE: The "first" catalog depends on the original ordering of the data_refs.
96  # NOTE: Thus, because I'm doing a many-1 match in _make_match_dict,
97  # the number of matches (and thus the details of the match statistics)
98  # will change if the data_refs are ordered differently.
99  # All the more reason to use a proper n-way matcher here. See: DM-8664
100  refcat = catalogs[0]
101  refcalib = photoCalibs[0] if photoCalibs != [] else None
102  dist_rel, flux_rel, ref_flux_rel, source_rel = self._make_match_dict(refcat,
103  catalogs[1:],
104  photoCalibs[1:],
105  refcalib=refcalib)
106  dist_abs, flux_abs, ref_flux_abs, source_abs = self._make_match_dict(reference,
107  catalogs,
108  photoCalibs)
109  dist = MatchDict(dist_rel, dist_abs)
110  flux = MatchDict(flux_rel, flux_abs)
111  ref_flux = MatchDict(ref_flux_rel, ref_flux_abs)
112  source = MatchDict(source_rel, source_abs)
113  return dist, flux, ref_flux, source
114 
115  old_cats = [ref.get('src') for ref in data_refs]
116  # NOTE: build photoCalibs from existing old Calib objects.
117  # TODO: we can make this a listcomp again once DM-10153 is finished.
118  old_calibs = []
119  if self.do_photometry:
120  for ref in data_refs:
121  calib = ref.get('calexp_calib')
122  fluxMag0 = calib.getFluxMag0()
123  old_calibs.append(lsst.afw.image.PhotoCalib(1.0/fluxMag0[0], fluxMag0[1]/fluxMag0[0]**2))
124 
125  self.old_dist, self.old_flux, self.old_ref_flux, self.old_source = compute(old_cats, old_calibs)
126 
127  # Update coordinates with the new wcs, and get the new photoCalibs.
128  new_cats = [ref.get('src') for ref in data_refs]
129  new_wcss = []
130  if self.do_astrometry:
131  new_wcss = [ref.get('jointcal_wcs') for ref in data_refs]
132  new_calibs = []
133  if self.do_photometry:
134  new_calibs = [ref.get('jointcal_photoCalib') for ref in data_refs]
135  if self.do_astrometry:
136  for wcs, cat in zip(new_wcss, new_cats):
137  # update in-place the object coordinates based on the new wcs
139 
140  self.new_dist, self.new_flux, self.new_ref_flux, self.new_source = compute(new_cats, new_calibs)
141 
142  if self.verbose:
143  print('old, new relative distance matches:',
144  len(self.old_dist.relative), len(self.new_dist.relative))
145  print('old, new absolute distance matches:',
146  len(self.old_dist.absolute), len(self.new_dist.absolute))
147  print('old, new relative flux matches:',
148  len(self.old_flux.relative), len(self.new_flux.relative))
149  print('old, new absolute flux matches:',
150  len(self.old_flux.absolute), len(self.new_flux.absolute))
151 
152  if self.do_photometry:
153  self._photometric_rms()
154  else:
155  self.new_PA1 = None
156 
157  def rms_total(data):
158  """Compute the total rms across all sources."""
159  total = sum(sum(dd**2) for dd in data.values())
160  n = sum(len(dd) for dd in data.values())
161  return np.sqrt(total/n)
162 
163  if self.do_astrometry:
164  self.old_dist_total = MatchDict(*(tuple(map(rms_total, self.old_dist))*u.radian).to(u.arcsecond))
165  self.new_dist_total = MatchDict(*(tuple(map(rms_total, self.new_dist))*u.radian).to(u.arcsecond))
166  else:
167  self.old_dist_total = MatchDict(None, None)
168  self.new_dist_total = MatchDict(None, None)
169 
170  Rms_result = collections.namedtuple("Rms_result", ["dist_relative", "dist_absolute", "pa1"])
171  return Rms_result(self.new_dist_total.relative, self.new_dist_total.absolute, self.new_PA1)
172 
173  def make_plots(self, data_refs, old_wcs_list,
174  name='', interactive=False, per_ccd_plot=False, outdir='.plots'):
175  """
176  Make plots of various quantites to help with debugging.
177  Requires that `compute_rms()` was run first.
178 
179  Parameters
180  ----------
181  data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
182  A list of data refs to do the calculations between.
183  old_wcs_list : list of lsst.afw.image.wcs.Wcs
184  A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
185  name : str
186  Name to include in plot titles and save files.
187  interactive : bool
188  Turn on matplotlib interactive mode and drop into a debugger when
189  plotting is finished. Otherwise, use a non-interactive backend.
190  per_ccd_plot : bool
191  Plot the WCS per CCD (takes longer and generates many plots for a large camera)
192  outdir : str
193  directory to save plots to.
194  """
195  import matplotlib
196 
197  if not interactive:
198  # Use a non-interactive backend for faster plotting.
199  matplotlib.use('pdf')
200 
201  import matplotlib.pyplot as plt
202  import astropy.visualization
203  # make quantities behave nicely when plotted.
204  astropy.visualization.quantity_support()
205  if interactive:
206  plt.ion()
207 
208  self.log.info("N data_refs: %d", len(data_refs))
209 
210  if self.do_photometry:
211  plot_flux_distributions(plt, self.old_mag, self.new_mag,
213  self.faint, self.bright, self.old_PA1, self.new_PA1,
214  name=name, outdir=outdir)
215 
216  def rms_per_source(data):
217  """Each element of data must already be the "delta" of whatever measurement."""
218  return (np.sqrt([np.mean(dd**2) for dd in data.values()])*u.radian).to(u.arcsecond)
219 
220  if self.do_astrometry:
221  old_dist_rms = MatchDict(*(tuple(map(rms_per_source, self.old_dist))))
222  new_dist_rms = MatchDict(*(tuple(map(rms_per_source, self.new_dist))))
223 
224  self.log.info("relative RMS (old, new): {:.2e} {:.2e}".format(self.old_dist_total.relative,
225  self.new_dist_total.relative))
226  self.log.info("absolute RMS (old, new): {:.2e} {:.2e}".format(self.old_dist_total.absolute,
227  self.new_dist_total.absolute))
228  plot_rms_histogram(plt, old_dist_rms.relative, old_dist_rms.absolute,
229  new_dist_rms.relative, new_dist_rms.absolute,
230  self.old_dist_total.relative, self.old_dist_total.absolute,
231  self.new_dist_total.relative, self.new_dist_total.absolute,
232  name=name, outdir=outdir)
233 
234  plot_all_wcs_deltas(plt, data_refs, self.visits_per_dataRef, old_wcs_list,
235  per_ccd_plot=per_ccd_plot,
236  name=name, outdir=outdir)
237 
238  if interactive:
239  plt.show()
240  import pdb
241  pdb.set_trace()
242 
243  def _photometric_rms(self, sn_cut=300, magnitude_range=3):
244  """
245  Compute the photometric RMS and the photometric repeatablity values (PA1).
246 
247  Parameters
248  ----------
249  sn_cut : float
250  The minimum signal/noise for sources to be included in the PA1 calculation.
251  magnitude_range : float
252  The range of magnitudes above sn_cut to include in the PA1 calculation.
253  """
254  def rms(flux, ref_flux):
255  return np.sqrt([np.mean((ref_flux[dd] - flux[dd])**2) for dd in flux])
256 
257  self.old_rms = MatchDict(*map(rms, self.old_flux, self.old_ref_flux))
258  self.new_rms = MatchDict(*map(rms, self.new_flux, self.new_ref_flux))
259 
260  # we want to use the absolute fluxes for all of these calculations.
261  self.old_ref = np.fromiter(self.old_ref_flux.absolute.values(), dtype=float)
262  self.new_ref = np.fromiter(self.new_ref_flux.absolute.values(), dtype=float)
263  self.old_mag = np.fromiter((abMagFromFlux(r) for r in self.old_ref), dtype=float)
264  self.new_mag = np.fromiter((abMagFromFlux(r) for r in self.new_ref), dtype=float)
265 
266  def signal_to_noise(sources, flux_key='slot_PsfFlux_flux', sigma_key='slot_PsfFlux_fluxSigma'):
267  """Compute the mean signal/noise per source from a MatchDict of SourceRecords."""
268  result = np.empty(len(sources))
269  for i, src in enumerate(sources.values()):
270  result[i] = np.mean([x[flux_key]/x[sigma_key] for x in src])
271  return result
272 
273  old_sn = signal_to_noise(self.old_source.absolute)
274  # Find the faint/bright magnitude limits that are the "flat" part of the rms/magnitude relation.
275  self.faint = self.old_mag[old_sn > sn_cut].max()
276  self.bright = self.faint - magnitude_range
277  if self.verbose:
278  print("PA1 Magnitude range: {:.3f}, {:.3f}".format(self.bright, self.faint))
279  old_good = (self.old_mag < self.faint) & (self.old_mag > self.bright)
280  new_good = (self.new_mag < self.faint) & (self.new_mag > self.bright)
281  self.old_weighted_rms = self.old_rms.absolute/self.old_ref
282  self.new_weighted_rms = self.new_rms.absolute/self.new_ref
283  self.old_PA1 = np.median(self.old_weighted_rms[old_good])
284  self.new_PA1 = np.median(self.new_weighted_rms[new_good])
285 
286  def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None):
287  """
288  Return several dicts of sourceID:[values] over the catalogs, to be used in RMS calculations.
289 
290  Parameters
291  ----------
292  reference : lsst.afw.table.SourceCatalog
293  Catalog to do the matching against.
294  visit_catalogs : list of lsst.afw.table.SourceCatalog
295  Visit source catalogs (values() produced by _make_visit_catalogs)
296  to cross-match against reference.
297  photoCalibs : list of lsst.afw.image.PhotoCalib
298  Exposure PhotoCalibs, 1-1 coorespondent with visit_catalogs.
299  refcalib : lsst.afw.image.PhotoCalib or None
300  Pass a PhotoCalib here to use it to compute Janskys from the
301  reference catalog ADU slot_flux.
302 
303  Returns
304  -------
305  distances : dict
306  dict of sourceID: array(separation distances for that source)
307  fluxes : dict
308  dict of sourceID: array(fluxes (Jy) for that source)
309  ref_fluxes : dict
310  dict of sourceID: flux (Jy) of the reference object
311  sources : dict
312  dict of sourceID: list(each SourceRecord that was position-matched
313  to this sourceID)
314  """
315  # If we have no photoCalibs, make it the same length as the others for zipping.
316  if photoCalibs == []:
317  photoCalibs = [[]]*len(visit_catalogs)
318 
319  distances = collections.defaultdict(list)
320  fluxes = collections.defaultdict(list)
321  ref_fluxes = {}
322  sources = collections.defaultdict(list)
323  if 'slot_CalibFlux_flux' in reference.schema:
324  ref_flux_key = 'slot_CalibFlux'
325  else:
326  ref_flux_key = '{}_flux'
327 
328  def get_fluxes(photoCalib, match):
329  """Return (flux, ref_flux) or None if either is invalid."""
330  # NOTE: Protect against negative fluxes: ignore this match if we find one.
331  maggiesToJansky = 3631
332  flux = match[1]['slot_CalibFlux_flux']
333  if flux < 0:
334  return None
335  else:
336  flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[1], "slot_CalibFlux").value
337 
338  # NOTE: Have to protect against negative reference fluxes too.
339  if 'slot' in ref_flux_key:
340  ref_flux = match[0][ref_flux_key+'_flux']
341  if ref_flux < 0:
342  return None
343  else:
344  ref_flux = maggiesToJansky * photoCalib.instFluxToMaggies(match[0], ref_flux_key).value
345  else:
346  # a.net fluxes are already in Janskys.
347  ref_flux = match[0][ref_flux_key.format(filt)]
348  if ref_flux < 0:
349  return None
350 
351  Flux = collections.namedtuple('Flux', ('flux', 'ref_flux'))
352  return Flux(flux, ref_flux)
353 
354  for cat, photoCalib, filt in zip(visit_catalogs, photoCalibs, self.filters):
355  good = (cat.get('base_PsfFlux_flux')/cat.get('base_PsfFlux_fluxSigma')) > self.flux_limit
356  # things the classifier called sources are not extended.
357  good &= (cat.get('base_ClassificationExtendedness_value') == 0)
358  matches = lsst.afw.table.matchRaDec(reference, cat[good], self.match_radius)
359  for m in matches:
360  if self.do_photometry:
361  flux = get_fluxes(photoCalib, m)
362  if flux is None:
363  continue
364  else:
365  fluxes[m[0].getId()].append(flux.flux)
366  # we can just use assignment here, since the value is always the same.
367  ref_fluxes[m[0].getId()] = flux.ref_flux
368 
369  if self.do_astrometry:
370  # Just use the computed separation distance directly.
371  distances[m[0].getId()].append(m[2])
372 
373  sources[m[0].getId()].append(m[1])
374  # Convert to numpy array for easier math
375  for source in distances:
376  distances[source] = np.array(distances[source])
377  for source in fluxes:
378  fluxes[source] = np.array(fluxes[source])
379 
380  return distances, fluxes, ref_fluxes, sources
381 
382  def _make_visit_catalogs(self, catalogs, visits):
383  """
384  Merge all catalogs from the each visit.
385  NOTE: creating this structure is somewhat slow, and will be unnecessary
386  once a full-visit composite dataset is available.
387 
388  Parameters
389  ----------
390  catalogs : list of lsst.afw.table.SourceCatalog
391  Catalogs to combine into per-visit catalogs.
392  visits : list of visit id (usually int)
393  list of visit identifiers, one-to-one correspondent with catalogs.
394 
395  Returns
396  -------
397  dict
398  dict of visit: catalog of all sources from all CCDs of that visit.
399  """
400  visit_dict = {v: lsst.afw.table.SourceCatalog(catalogs[0].schema) for v in visits}
401  for v, cat in zip(visits, catalogs):
402  visit_dict[v].extend(cat)
403  # We want catalog contiguity to do object selection later.
404  for v in visit_dict:
405  visit_dict[v] = visit_dict[v].copy(deep=True)
406 
407  return visit_dict
408 
409 
410 def plot_flux_distributions(plt, old_mag, new_mag, old_weighted_rms, new_weighted_rms,
411  faint, bright, old_PA1, new_PA1,
412  name='', outdir='.plots'):
413  """Plot various distributions of fluxes and magnitudes.
414 
415  Parameters
416  ----------
417  plt : matplotlib.pyplot instance
418  pyplot instance to plot with
419  old_mag : np.array
420  old magnitudes
421  new_mag : np.array
422  new magnitudes
423  old_weighted_rms : np.array
424  old rms weighted by the mean (rms(data)/mean(data))
425  new_weighted_rms : np.array
426  old rms weighted by the mean (rms(data)/mean(data))
427  faint : float
428  Faint end of range that PA1 was computed from.
429  bright : float
430  Bright end of range that PA1 was computed from.
431  old_PA1 : float
432  Old value of PA1, to plot as horizontal line.
433  new_PA1 : float
434  New value of PA1, to plot as horizontal line.
435  name : str
436  Name to include in plot titles and save files.
437  outdir : str, optional
438  Directory to write the saved plots to.
439  """
440 
441  import seaborn
442  seaborn.set_style('whitegrid')
443  import scipy.stats
444 
445  old_color = 'blue'
446  new_color = 'red'
447  plt.figure()
448  plt.plot(old_mag, old_weighted_rms, '.', color=old_color, label='old')
449  plt.plot(new_mag, new_weighted_rms, '.', color=new_color, label='new')
450  plt.axvline(faint, ls=':', color=old_color)
451  plt.axvline(bright, ls=':', color=old_color)
452  plt.axhline(old_PA1, ls='--', color=old_color)
453  plt.axhline(new_PA1, ls='--', color=new_color)
454  plt.legend(loc='upper left')
455  plt.title('Where is the systematic flux rms limit?')
456  plt.xlabel('magnitude')
457  plt.ylabel('rms/mean per source')
458  filename = os.path.join(outdir, '{}-photometry-PA1.pdf')
459  plt.savefig(filename.format(name))
460 
461  plt.figure()
462  seaborn.distplot(old_weighted_rms, fit=scipy.stats.lognorm, kde=False, label="old", color=old_color)
463  seaborn.distplot(new_weighted_rms, fit=scipy.stats.lognorm, kde=False, label="new", color=new_color)
464  plt.title('Source RMS pre/post-jointcal')
465  plt.xlabel('rms(flux)/mean(flux)')
466  plt.ylabel('number')
467  plt.legend(loc='upper right')
468  filename = os.path.join(outdir, '{}-photometry-rms.pdf')
469  plt.savefig(filename.format(name))
470 
471 
472 def plot_all_wcs_deltas(plt, data_refs, visits, old_wcs_list, per_ccd_plot=False,
473  name='', outdir='.plots'):
474  """
475  Various plots of the difference between old and new Wcs.
476 
477  Parameters
478  ----------
479  plt : matplotlib.pyplot instance
480  pyplot instance to plot with.
481  data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
482  A list of data refs to plot.
483  visits : list of visit id (usually int)
484  list of visit identifiers, one-to-one correspondent with catalogs.
485  old_wcs_list : list of lsst.afw.image.wcs.Wcs
486  A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
487  per_ccd_plot : bool, optional
488  Make per-ccd plots of the "wcs different" (warning: slow!)
489  name : str
490  Name to include in plot titles and save files.
491  outdir : str, optional
492  Directory to write the saved plots to.
493  """
494 
495  plot_wcs_magnitude(plt, data_refs, visits, old_wcs_list, name, outdir=outdir)
496  plot_all_wcs_quivers(plt, data_refs, visits, old_wcs_list, name, outdir=outdir)
497 
498  if per_ccd_plot:
499  for i, ref in enumerate(data_refs):
500  md = ref.get('calexp_md')
501  dims = lsst.afw.image.bboxFromMetadata(md).getDimensions()
502  plot_wcs(plt, old_wcs_list[i], ref.get('jointcal_wcs'),
503  dims.getX(), dims.getY(),
504  center=(md.get('CRVAL1'), md.get('CRVAL2')), name='dataRef %d'%i,
505  outdir=outdir)
506 
507 
508 def make_xy_wcs_grid(x_dim, y_dim, wcs1, wcs2, num=50):
509  """Return num x/y grid coordinates for wcs1 and wcs2."""
510  x = np.linspace(0, x_dim, num)
511  y = np.linspace(0, y_dim, num)
512  x1, y1 = wcs_convert(x, y, wcs1)
513  x2, y2 = wcs_convert(x, y, wcs2)
514  return x1, y1, x2, y2
515 
516 
517 def wcs_convert(xv, yv, wcs):
518  """Convert two arrays of x/y points into an on-sky grid."""
519  xout = np.zeros((xv.shape[0], yv.shape[0]))
520  yout = np.zeros((xv.shape[0], yv.shape[0]))
521  for i, x in enumerate(xv):
522  for j, y in enumerate(yv):
523  sky = wcs.pixelToSky(x, y).toFk5()
524  xout[i, j] = sky.getRa()
525  yout[i, j] = sky.getDec()
526  return xout, yout
527 
528 
529 def plot_all_wcs_quivers(plt, data_refs, visits, old_wcs_list, name, outdir='.plots'):
530  """
531  Make quiver plots of the WCS deltas for each CCD in each visit.
532 
533  Parameters
534  ----------
535  plt : matplotlib.pyplot instance
536  pyplot instance to plot with.
537  data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
538  A list of data refs to plot.
539  visits : list of visit id (usually int)
540  list of visit identifiers, one-to-one correspondent with catalogs.
541  old_wcs_list : list of lsst.afw.image.wcs.Wcs
542  A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
543  name : str
544  Name to include in plot titles and save files.
545  outdir : str, optional
546  Directory to write the saved plots to.
547  """
548 
549  for visit in visits:
550  fig = plt.figure()
551  # fig.set_tight_layout(True)
552  ax = fig.add_subplot(111)
553  for old_wcs, ref in zip(old_wcs_list, data_refs):
554  if ref.dataId['visit'] != visit:
555  continue
556  md = ref.get('calexp_md')
557  dims = lsst.afw.image.bboxFromMetadata(md).getDimensions()
558  Q = plot_wcs_quivers(ax, old_wcs, ref.get('jointcal_wcs'),
559  dims.getX(), dims.getY())
560  # TODO: add CCD bounding boxes to plot once DM-5503 is finished.
561  # TODO: add a circle for the full focal plane.
562  length = (0.1*u.arcsecond).to(u.radian).value
563  ax.quiverkey(Q, 0.9, 0.95, length, '0.1 arcsec', coordinates='figure', labelpos='W')
564  plt.xlabel('RA')
565  plt.ylabel('Dec')
566  plt.title('visit: {}'.format(visit))
567  filename = os.path.join(outdir, '{}-{}-quivers.pdf')
568  plt.savefig(filename.format(name, visit))
569 
570 
571 def plot_wcs_quivers(ax, wcs1, wcs2, x_dim, y_dim):
572  """
573  Plot the delta between wcs1 and wcs2 as vector arrows.
574 
575  Parameters
576  ----------
577  ax : matplotlib.axis
578  Matplotlib axis instance to plot to.
579  wcs1 : lsst.afw.image.wcs.Wcs
580  First WCS to compare.
581  wcs2 : lsst.afw.image.wcs.Wcs
582  Second WCS to compare.
583  x_dim : int
584  Size of array in X-coordinate to make the grid over.
585  y_dim : int
586  Size of array in Y-coordinate to make the grid over.
587  """
588 
589  x1, y1, x2, y2 = make_xy_wcs_grid(x_dim, y_dim, wcs1, wcs2)
590  uu = x2 - x1
591  vv = y2 - y1
592  return ax.quiver(x1, y1, uu, vv, units='x', pivot='tail', scale=1e-3, width=1e-5)
593 
594 
595 def plot_wcs_magnitude(plt, data_refs, visits, old_wcs_list, name, outdir='.plots'):
596  """Plot the magnitude of the WCS change between old and new visits as a heat map.
597 
598  Parameters
599  ----------
600  plt : matplotlib.pyplot instance
601  pyplot instance to plot with.
602  data_refs : list of lsst.daf.persistence.butlerSubset.ButlerDataRef
603  A list of data refs to plot.
604  visits : list of visit id (usually int)
605  list of visit identifiers, one-to-one correspondent with catalogs.
606  old_wcs_list : list of lsst.afw.image.wcs.Wcs
607  A list of the old (pre-jointcal) WCSs, one-to-one corresponding to data_refs.
608  name : str
609  Name to include in plot titles and save files.
610  outdir : str, optional
611  Directory to write the saved plots to.
612  """
613  for visit in visits:
614  fig = plt.figure()
615  fig.set_tight_layout(True)
616  ax = fig.add_subplot(111)
617  # Start min/max at the "opposite" ends so they always get the first valid value.
618  xmin = np.inf
619  ymin = np.inf
620  xmax = -np.inf
621  ymax = -np.inf
622  for old_wcs, ref in zip(old_wcs_list, data_refs):
623  if ref.dataId['visit'] != visit:
624  continue
625  md = ref.get('calexp_md')
626  dims = lsst.afw.image.bboxFromMetadata(md).getDimensions()
627  x1, y1, x2, y2 = make_xy_wcs_grid(dims.getX(), dims.getY(),
628  old_wcs, ref.get('jointcal_wcs'))
629  uu = x2 - x1
630  vv = y2 - y1
631  extent = (x1[0, 0], x1[-1, -1], y1[0, 0], y1[-1, -1])
632  xmin = min(x1.min(), xmin)
633  ymin = min(y1.min(), ymin)
634  xmax = max(x1.max(), xmax)
635  ymax = max(y1.max(), ymax)
636  magnitude = (np.linalg.norm((uu, vv), axis=0)*u.radian).to(u.arcsecond).value
637  img = ax.imshow(magnitude, vmin=0, vmax=0.3,
638  aspect='auto', extent=extent, cmap=plt.get_cmap('magma'))
639  # TODO: add CCD bounding boxes to the plot once DM-5503 is finished.
640  # TODO: add a circle for the full focal plane.
641 
642  # We're reusing only one of the returned images here for colorbar scaling,
643  # but it doesn't matter because we set vmin/vmax so they are all scaled the same.
644  cbar = plt.colorbar(img)
645  cbar.ax.set_ylabel('distortion (arcseconds)')
646  plt.xlim(xmin, xmax)
647  plt.ylim(ymin, ymax)
648  plt.xlabel('RA')
649  plt.ylabel('Dec')
650  plt.title('visit: {}'.format(visit))
651  filename = os.path.join(outdir, '{}-{}-heatmap.pdf')
652  plt.savefig(filename.format(name, visit))
653 
654 
655 def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name="", outdir='.plots'):
656  """Plot the "distortion map": wcs1-wcs2 delta of points in the CCD grid.
657 
658  Parameters
659  ----------
660  plt : matplotlib.pyplot instance
661  pyplot instance to plot with.
662  wcs1 : lsst.afw.image.wcs.Wcs
663  First WCS to compare.
664  wcs2 : lsst.afw.image.wcs.Wcs
665  Second WCS to compare.
666  x_dim : int
667  Size of array in X-coordinate to make the grid over.
668  y_dim : int
669  Size of array in Y-coordinate to make the grid over.
670  center : tuple, optional
671  Center of the data, in on-chip coordinates.
672  name : str
673  Name to include in plot titles and save files.
674  outdir : str, optional
675  Directory to write the saved plots to.
676  """
677 
678  plt.figure()
679 
680  x1, y1, x2, y2 = make_xy_wcs_grid(x_dim, y_dim, wcs1, wcs2, num=50)
681  plt.plot((x1 - x2) + center[0], (y1 - y2) + center[1], '-')
682  plt.xlabel('delta RA (arcsec)')
683  plt.ylabel('delta Dec (arcsec)')
684  plt.title(name)
685  filename = os.path.join(outdir, '{}-wcs.pdf')
686  plt.savefig(filename.format(name))
687 
688 
689 def plot_rms_histogram(plt, old_rms_relative, old_rms_absolute,
690  new_rms_relative, new_rms_absolute,
691  old_rel_total, old_abs_total, new_rel_total, new_abs_total,
692  name="", outdir='.plots'):
693  """Plot histograms of the source separations and their RMS values.
694 
695  Parameters
696  ----------
697  plt : matplotlib.pyplot instance
698  pyplot instance to plot with.
699  old_rms_relative : np.array
700  old relative rms/star
701  old_rms_absolute : np.array
702  old absolute rms/star
703  new_rms_relative : np.array
704  new relative rms/star
705  new_rms_absolute : np.array
706  new absolute rms/star
707  old_rel_total : float
708  old relative rms over all stars
709  old_abs_total : float
710  old absolute rms over all stars
711  new_rel_total : float
712  new relative rms over all stars
713  new_abs_total : float
714  new absolute rms over all stars
715  name : str
716  Name to include in plot titles and save files.
717  outdir : str, optional
718  Directory to write the saved plots to.
719  """
720  plt.figure()
721 
722  color_rel = 'black'
723  ls_old = 'dotted'
724  color_abs = 'green'
725  ls_new = 'dashed'
726  plotOptions = {'lw': 2, 'range': (0, 0.1)*u.arcsecond, 'normed': True,
727  'bins': 30, 'histtype': 'step'}
728 
729  plt.title('relative vs. absolute: %d vs. %d'%(len(old_rms_relative), len(old_rms_absolute)))
730 
731  plt.hist(old_rms_absolute, color=color_abs, ls=ls_old, label='old abs', **plotOptions)
732  plt.hist(new_rms_absolute, color=color_abs, ls=ls_new, label='new abs', **plotOptions)
733 
734  plt.hist(old_rms_relative, color=color_rel, ls=ls_old, label='old rel', **plotOptions)
735  plt.hist(new_rms_relative, color=color_rel, ls=ls_new, label='new rel', **plotOptions)
736 
737  plt.axvline(x=old_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_old)
738  plt.axvline(x=new_abs_total.value, linewidth=1.5, color=color_abs, ls=ls_new)
739  plt.axvline(x=old_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_old)
740  plt.axvline(x=new_rel_total.value, linewidth=1.5, color=color_rel, ls=ls_new)
741 
742  plt.xlim(plotOptions['range'])
743  plt.xlabel('arcseconds')
744  plt.legend(loc='best')
745  filename = os.path.join(outdir, '{}-histogram.pdf')
746  plt.savefig(filename.format(name))
def _photometric_rms(self, sn_cut=300, magnitude_range=3)
Definition: utils.py:243
def make_plots(self, data_refs, old_wcs_list, name='', interactive=False, per_ccd_plot=False, outdir='.plots')
Definition: utils.py:174
def __init__(self, match_radius=0.1 *arcseconds, flux_limit=100.0, do_photometry=True, do_astrometry=True, verbose=False)
Definition: utils.py:41
The photometric calibration of an exposure.
Definition: PhotoCalib.h:81
def wcs_convert(xv, yv, wcs)
Definition: utils.py:517
int min
Definition: Coord.cc:160
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')
Definition: utils.py:692
def _make_match_dict(self, reference, visit_catalogs, photoCalibs, refcalib=None)
Definition: utils.py:286
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
STL class.
double abMagFromFlux(double flux)
Compute AB magnitude from flux in Janskys.
Definition: Calib.h:58
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
Update sky coordinates in a collection of source objects.
Definition: Log.h:716
def plot_all_wcs_deltas(plt, data_refs, visits, old_wcs_list, per_ccd_plot=False, name='', outdir='.plots')
Definition: utils.py:473
std::vector< Match< typename Cat::Record, typename Cat::Record > > matchRaDec(Cat const &cat, Angle radius, bool symmetric)
Compute all tuples (s1,s2,d) where s1 != s2, s1 and s2 both belong to cat, and d, the distance betwee...
def plot_wcs(plt, wcs1, wcs2, x_dim, y_dim, center=(0, 0), name="", outdir='.plots')
Definition: utils.py:655
def plot_wcs_quivers(ax, wcs1, wcs2, x_dim, y_dim)
Definition: utils.py:571
int max
Definition: BoundedField.cc:99
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:134
def make_xy_wcs_grid(x_dim, y_dim, wcs1, wcs2, num=50)
Definition: utils.py:508
geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
Definition: Image.cc:688
def plot_flux_distributions(plt, old_mag, new_mag, old_weighted_rms, new_weighted_rms, faint, bright, old_PA1, new_PA1, name='', outdir='.plots')
Definition: utils.py:412
def compute_rms(self, data_refs, reference)
Definition: utils.py:64
static Log getLogger(Log const &logger)
Definition: Log.h:772
def plot_wcs_magnitude(plt, data_refs, visits, old_wcs_list, name, outdir='.plots')
Definition: utils.py:595
def plot_all_wcs_quivers(plt, data_refs, visits, old_wcs_list, name, outdir='.plots')
Definition: utils.py:529
def _make_visit_catalogs(self, catalogs, visits)
Definition: utils.py:382