lsst.pipe.tasks  14.0-55-g35e83f69+3
photoCal.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 # \package lsst.pipe.tasks.
23 
24 from __future__ import absolute_import, division, print_function
25 from builtins import zip
26 from builtins import input
27 from builtins import range
28 
29 import math
30 import sys
31 
32 import numpy as np
33 
34 import lsst.pex.config as pexConf
35 import lsst.pipe.base as pipeBase
36 from lsst.afw.image import abMagFromFlux, abMagErrFromFluxErr, fluxFromABMag, Calib
37 import lsst.afw.math as afwMath
38 from lsst.meas.astrom import DirectMatchTask, DirectMatchConfigWithoutLoader
39 import lsst.afw.display.ds9 as ds9
40 from lsst.meas.algorithms import getRefFluxField, ReserveSourcesTask
41 from .colorterms import ColortermLibrary
42 
43 __all__ = ["PhotoCalTask", "PhotoCalConfig"]
44 
45 
46 class PhotoCalConfig(pexConf.Config):
47  """Config for PhotoCal"""
48  match = pexConf.ConfigField("Match to reference catalog",
49  DirectMatchConfigWithoutLoader)
50  reserve = pexConf.ConfigurableField(target=ReserveSourcesTask, doc="Reserve sources from fitting")
51  fluxField = pexConf.Field(
52  dtype=str,
53  default="slot_CalibFlux_flux",
54  doc=("Name of the source flux field to use. The associated flag field\n"
55  "('<name>_flags') will be implicitly included in badFlags."),
56  )
57  applyColorTerms = pexConf.Field(
58  dtype=bool,
59  default=None,
60  doc=("Apply photometric color terms to reference stars? One of:\n"
61  "None: apply if colorterms and photoCatName are not None;\n"
62  " fail if color term data is not available for the specified ref catalog and filter.\n"
63  "True: always apply colorterms; fail if color term data is not available for the\n"
64  " specified reference catalog and filter.\n"
65  "False: do not apply."),
66  optional=True,
67  )
68  sigmaMax = pexConf.Field(
69  dtype=float,
70  default=0.25,
71  doc="maximum sigma to use when clipping",
72  optional=True,
73  )
74  nSigma = pexConf.Field(
75  dtype=float,
76  default=3.0,
77  doc="clip at nSigma",
78  )
79  useMedian = pexConf.Field(
80  dtype=bool,
81  default=True,
82  doc="use median instead of mean to compute zeropoint",
83  )
84  nIter = pexConf.Field(
85  dtype=int,
86  default=20,
87  doc="number of iterations",
88  )
89  colorterms = pexConf.ConfigField(
90  dtype=ColortermLibrary,
91  doc="Library of photometric reference catalog name: color term dict",
92  )
93  photoCatName = pexConf.Field(
94  dtype=str,
95  optional=True,
96  doc=("Name of photometric reference catalog; used to select a color term dict in colorterms."
97  " see also applyColorTerms"),
98  )
99  magErrFloor = pexConf.RangeField(
100  dtype=float,
101  default=0.0,
102  doc="Additional magnitude uncertainty to be added in quadrature with measurement errors.",
103  min=0.0,
104  )
105 
106  def validate(self):
107  pexConf.Config.validate(self)
108  if self.applyColorTerms and self.photoCatName is None:
109  raise RuntimeError("applyColorTerms=True requires photoCatName is non-None")
110  if self.applyColorTerms and len(self.colorterms.data) == 0:
111  raise RuntimeError("applyColorTerms=True requires colorterms be provided")
112 
113  def setDefaults(self):
114  pexConf.Config.setDefaults(self)
115  self.match.sourceSelection.doFlags = True
116  self.match.sourceSelection.flags.bad = [
117  "base_PixelFlags_flag_edge",
118  "base_PixelFlags_flag_interpolated",
119  "base_PixelFlags_flag_saturated",
120  ]
121  self.match.sourceSelection.doUnresolved = True
122 
123 
124 
130 
131 class PhotoCalTask(pipeBase.Task):
132  r"""!
133 \anchor PhotoCalTask_
134 
135 \brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
136 
137 \section pipe_tasks_photocal_Contents Contents
138 
139  - \ref pipe_tasks_photocal_Purpose
140  - \ref pipe_tasks_photocal_Initialize
141  - \ref pipe_tasks_photocal_IO
142  - \ref pipe_tasks_photocal_Config
143  - \ref pipe_tasks_photocal_Debug
144  - \ref pipe_tasks_photocal_Example
145 
146 \section pipe_tasks_photocal_Purpose Description
147 
148 \copybrief PhotoCalTask
149 
150 Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue.
151 The type of flux to use is specified by PhotoCalConfig.fluxField.
152 
153 The algorithm clips outliers iteratively, with parameters set in the configuration.
154 
155 \note This task can adds fields to the schema, so any code calling this task must ensure that
156 these columns are indeed present in the input match list; see \ref pipe_tasks_photocal_Example
157 
158 \section pipe_tasks_photocal_Initialize Task initialisation
159 
160 \copydoc \_\_init\_\_
161 
162 \section pipe_tasks_photocal_IO Inputs/Outputs to the run method
163 
164 \copydoc run
165 
166 \section pipe_tasks_photocal_Config Configuration parameters
167 
168 See \ref PhotoCalConfig
169 
170 \section pipe_tasks_photocal_Debug Debug variables
171 
172 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
173 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
174 
175 The available variables in PhotoCalTask are:
176 <DL>
177  <DT> \c display
178  <DD> If True enable other debug outputs
179  <DT> \c displaySources
180  <DD> If True, display the exposure on ds9's frame 1 and overlay the source catalogue.
181  <DL>
182  <DT> red o
183  <DD> Reserved objects
184  <DT> green o
185  <DD> Objects used in the photometric calibration
186  </DL>
187  <DT> \c scatterPlot
188  <DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude.
189  - good objects in blue
190  - rejected objects in red
191  (if \c scatterPlot is 2 or more, prompt to continue after each iteration)
192 </DL>
193 
194 \section pipe_tasks_photocal_Example A complete example of using PhotoCalTask
195 
196 This code is in \link examples/photoCalTask.py\endlink, and can be run as \em e.g.
197 \code
198 examples/photoCalTask.py
199 \endcode
200 \dontinclude photoCalTask.py
201 
202 Import the tasks (there are some other standard imports; read the file for details)
203 \skipline from lsst.pipe.tasks.astrometry
204 \skipline measPhotocal
205 
206 We need to create both our tasks before processing any data as the task constructors
207 can add extra columns to the schema which we get from the input catalogue, \c scrCat:
208 \skipline getSchema
209 
210 Astrometry first:
211 \skip AstrometryTask.ConfigClass
212 \until aTask
213 (that \c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises,
214 so we tell it to use the \c r band)
215 
216 Then photometry:
217 \skip measPhotocal
218 \until pTask
219 
220 If the schema has indeed changed we need to add the new columns to the source table
221 (yes; this should be easier!)
222 \skip srcCat
223 \until srcCat = cat
224 
225 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
226 task objects):
227 \skip matches
228 \until result
229 
230 We can then unpack and use the results:
231 \skip calib
232 \until np.log
233 
234 <HR>
235 To investigate the \ref pipe_tasks_photocal_Debug, put something like
236 \code{.py}
237  import lsstDebug
238  def DebugInfo(name):
239  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
240  if name.endswith(".PhotoCal"):
241  di.display = 1
242 
243  return di
244 
245  lsstDebug.Info = DebugInfo
246 \endcode
247 into your debug.py file and run photoCalTask.py with the \c --debug flag.
248  """
249  ConfigClass = PhotoCalConfig
250  _DefaultName = "photoCal"
251 
252  def __init__(self, refObjLoader, schema=None, **kwds):
253  """!Create the photometric calibration task. See PhotoCalTask.init for documentation
254  """
255  pipeBase.Task.__init__(self, **kwds)
256  self.scatterPlot = None
257  self.fig = None
258  if schema is not None:
259  self.usedKey = schema.addField("calib_photometry_used", type="Flag",
260  doc="set if source was used in photometric calibration")
261  else:
262  self.usedKey = None
263  self.match = DirectMatchTask(self.config.match, refObjLoader=refObjLoader,
264  name="match", parentTask=self)
265  self.makeSubtask("reserve", columnName="calib_photometry", schema=schema,
266  doc="set if source was reserved from photometric calibration")
267 
268  def getSourceKeys(self, schema):
269  """!Return a struct containing the source catalog keys for fields used by PhotoCalTask.
270 
271  Returned fields include:
272  - flux
273  - fluxErr
274  """
275  flux = schema.find(self.config.fluxField).key
276  fluxErr = schema.find(self.config.fluxField + "Sigma").key
277  return pipeBase.Struct(flux=flux, fluxErr=fluxErr)
278 
279  @pipeBase.timeMethod
280  def extractMagArrays(self, matches, filterName, sourceKeys):
281  """!Extract magnitude and magnitude error arrays from the given matches.
282 
283  \param[in] matches Reference/source matches, a \link lsst::afw::table::ReferenceMatchVector\endlink
284  \param[in] filterName Name of filter being calibrated
285  \param[in] sourceKeys Struct of source catalog keys, as returned by getSourceKeys()
286 
287  \return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays
288  where magErr is an error in the magnitude; the error in srcMag - refMag
289  If nonzero, config.magErrFloor will be added to magErr *only* (not srcMagErr or refMagErr), as
290  magErr is what is later used to determine the zero point.
291  Struct also contains refFluxFieldList: a list of field names of the reference catalog used for fluxes
292  (1 or 2 strings)
293  \note These magnitude arrays are the \em inputs to the photometric calibration, some may have been
294  discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813)
295  """
296  srcFluxArr = np.array([m.second.get(sourceKeys.flux) for m in matches])
297  srcFluxErrArr = np.array([m.second.get(sourceKeys.fluxErr) for m in matches])
298  if not np.all(np.isfinite(srcFluxErrArr)):
299  # this is an unpleasant hack; see DM-2308 requesting a better solution
300  self.log.warn("Source catalog does not have flux uncertainties; using sqrt(flux).")
301  srcFluxErrArr = np.sqrt(srcFluxArr)
302 
303  # convert source flux from DN to an estimate of Jy
304  JanskysPerABFlux = 3631.0
305  srcFluxArr = srcFluxArr * JanskysPerABFlux
306  srcFluxErrArr = srcFluxErrArr * JanskysPerABFlux
307 
308  if not matches:
309  raise RuntimeError("No reference stars are available")
310  refSchema = matches[0].first.schema
311 
312  applyColorTerms = self.config.applyColorTerms
313  applyCTReason = "config.applyColorTerms is %s" % (self.config.applyColorTerms,)
314  if self.config.applyColorTerms is None:
315  # apply color terms if color term data is available and photoCatName specified
316  ctDataAvail = len(self.config.colorterms.data) > 0
317  photoCatSpecified = self.config.photoCatName is not None
318  applyCTReason += " and data %s available" % ("is" if ctDataAvail else "is not")
319  applyCTReason += " and photoRefCat %s provided" % ("is" if photoCatSpecified else "is not")
320  applyColorTerms = ctDataAvail and photoCatSpecified
321 
322  if applyColorTerms:
323  self.log.info("Applying color terms for filterName=%r, config.photoCatName=%s because %s",
324  filterName, self.config.photoCatName, applyCTReason)
325  ct = self.config.colorterms.getColorterm(
326  filterName=filterName, photoCatName=self.config.photoCatName, doRaise=True)
327  else:
328  self.log.info("Not applying color terms because %s", applyCTReason)
329  ct = None
330 
331  if ct: # we have a color term to worry about
332  fluxFieldList = [getRefFluxField(refSchema, filt) for filt in (ct.primary, ct.secondary)]
333  missingFluxFieldList = []
334  for fluxField in fluxFieldList:
335  try:
336  refSchema.find(fluxField).key
337  except KeyError:
338  missingFluxFieldList.append(fluxField)
339 
340  if missingFluxFieldList:
341  self.log.warn("Source catalog does not have fluxes for %s; ignoring color terms",
342  " ".join(missingFluxFieldList))
343  ct = None
344 
345  if not ct:
346  fluxFieldList = [getRefFluxField(refSchema, filterName)]
347 
348  refFluxArrList = [] # list of ref arrays, one per flux field
349  refFluxErrArrList = [] # list of ref flux arrays, one per flux field
350  for fluxField in fluxFieldList:
351  fluxKey = refSchema.find(fluxField).key
352  refFluxArr = np.array([m.first.get(fluxKey) for m in matches])
353  try:
354  fluxErrKey = refSchema.find(fluxField + "Sigma").key
355  refFluxErrArr = np.array([m.first.get(fluxErrKey) for m in matches])
356  except KeyError:
357  # Reference catalogue may not have flux uncertainties; HACK
358  self.log.warn("Reference catalog does not have flux uncertainties for %s; using sqrt(flux).",
359  fluxField)
360  refFluxErrArr = np.sqrt(refFluxArr)
361 
362  refFluxArrList.append(refFluxArr)
363  refFluxErrArrList.append(refFluxErrArr)
364 
365  if ct: # we have a color term to worry about
366  refMagArr1 = np.array([abMagFromFlux(rf1) for rf1 in refFluxArrList[0]]) # primary
367  refMagArr2 = np.array([abMagFromFlux(rf2) for rf2 in refFluxArrList[1]]) # secondary
368 
369  refMagArr = ct.transformMags(refMagArr1, refMagArr2)
370  refFluxErrArr = ct.propagateFluxErrors(refFluxErrArrList[0], refFluxErrArrList[1])
371  else:
372  refMagArr = np.array([abMagFromFlux(rf) for rf in refFluxArrList[0]])
373 
374  srcMagArr = np.array([abMagFromFlux(sf) for sf in srcFluxArr])
375 
376  # Fitting with error bars in both axes is hard
377  # for now ignore reference flux error, but ticket DM-2308 is a request for a better solution
378  magErrArr = np.array([abMagErrFromFluxErr(fe, sf) for fe, sf in zip(srcFluxErrArr, srcFluxArr)])
379  if self.config.magErrFloor != 0.0:
380  magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
381 
382  srcMagErrArr = np.array([abMagErrFromFluxErr(sfe, sf) for sfe, sf in zip(srcFluxErrArr, srcFluxArr)])
383  refMagErrArr = np.array([abMagErrFromFluxErr(rfe, rf) for rfe, rf in zip(refFluxErrArr, refFluxArr)])
384 
385  good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
386 
387  return pipeBase.Struct(
388  srcMag=srcMagArr[good],
389  refMag=refMagArr[good],
390  magErr=magErrArr[good],
391  srcMagErr=srcMagErrArr[good],
392  refMagErr=refMagErrArr[good],
393  refFluxFieldList=fluxFieldList,
394  )
395 
396  @pipeBase.timeMethod
397  def run(self, exposure, sourceCat, expId=0):
398  """!Do photometric calibration - select matches to use and (possibly iteratively) compute
399  the zero point.
400 
401  \param[in] exposure Exposure upon which the sources in the matches were detected.
402  \param[in] sourceCat A catalog of sources to use in the calibration
403  (\em i.e. a list of lsst.afw.table.Match with
404  \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord ---
405  the reference object and matched object respectively).
406  (will not be modified except to set the outputField if requested.).
407 
408  \return Struct of:
409  - calib ------- \link lsst::afw::image::Calib\endlink object containing the zero point
410  - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays
411  - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.
412  - zp ---------- Photometric zero point (mag)
413  - sigma ------- Standard deviation of fit of photometric zero point (mag)
414  - ngood ------- Number of sources used to fit photometric zero point
415 
416  The exposure is only used to provide the name of the filter being calibrated (it may also be
417  used to generate debugging plots).
418 
419  The reference objects:
420  - Must include a field \c photometric; True for objects which should be considered as
421  photometric standards
422  - Must include a field \c flux; the flux used to impose a magnitude limit and also to calibrate
423  the data to (unless a color term is specified, in which case ColorTerm.primary is used;
424  See https://jira.lsstcorp.org/browse/DM-933)
425  - May include a field \c stargal; if present, True means that the object is a star
426  - May include a field \c var; if present, True means that the object is variable
427 
428  The measured sources:
429  - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration
430 
431  \throws RuntimeError with the following strings:
432 
433  <DL>
434  <DT> No matches to use for photocal
435  <DD> No matches are available (perhaps no sources/references were selected by the matcher).
436  <DT> No reference stars are available
437  <DD> No matches are available from which to extract magnitudes.
438  </DL>
439  """
440  import lsstDebug
441 
442  display = lsstDebug.Info(__name__).display
443  displaySources = display and lsstDebug.Info(__name__).displaySources
444  self.scatterPlot = display and lsstDebug.Info(__name__).scatterPlot
445 
446  if self.scatterPlot:
447  from matplotlib import pyplot
448  try:
449  self.fig.clf()
450  except:
451  self.fig = pyplot.figure()
452 
453  filterName = exposure.getFilter().getName()
454 
455  # Match sources
456  matchResults = self.match.run(sourceCat, filterName)
457  matches = matchResults.matches
458  reserveResults = self.reserve.run([mm.second for mm in matches], expId=expId)
459  if displaySources:
460  self.displaySources(exposure, matches, reserveResults.reserved)
461  if reserveResults.reserved.sum() > 0:
462  matches = [mm for mm, use in zip(matches, reserveResults.use) if use]
463  if len(matches) == 0:
464  raise RuntimeError("No matches to use for photocal")
465  if self.usedKey is not None:
466  for mm in matches:
467  mm.second.set(self.usedKey, True)
468 
469  # Prepare for fitting
470  sourceKeys = self.getSourceKeys(matches[0].second.schema)
471  arrays = self.extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys)
472 
473  # Fit for zeropoint
474  r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
475  self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
476 
477  # Prepare the results
478  flux0 = 10**(0.4*r.zp) # Flux of mag=0 star
479  flux0err = 0.4*math.log(10)*flux0*r.sigma # Error in flux0
480  calib = Calib()
481  calib.setFluxMag0(flux0, flux0err)
482 
483  return pipeBase.Struct(
484  calib=calib,
485  arrays=arrays,
486  matches=matches,
487  zp=r.zp,
488  sigma=r.sigma,
489  ngood=r.ngood,
490  )
491 
492  def displaySources(self, exposure, matches, reserved, frame=1):
493  """Display sources we'll use for photocal
494 
495  Sources that will be actually used will be green.
496  Sources reserved from the fit will be red.
497 
498  Parameters
499  ----------
500  exposure : `lsst.afw.image.ExposureF`
501  Exposure to display.
502  matches : `list` of `lsst.afw.table.RefMatch`
503  Matches used for photocal.
504  reserved : `numpy.ndarray` of type `bool`
505  Boolean array indicating sources that are reserved.
506  frame : `int`
507  Frame number for display.
508  """
509  ds9.mtv(exposure, frame=frame, title="photocal")
510  with ds9.Buffering():
511  for mm, rr in zip(matches, reserved):
512  x, y = mm.second.getCentroid()
513  ctype = ds9.RED if rr else ds9.GREEN
514  ds9.dot("o", x, y, size=4, frame=frame, ctype=ctype)
515 
516 
517  def getZeroPoint(self, src, ref, srcErr=None, zp0=None):
518  """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
519 
520  We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
521  1. We use the median/interquartile range to estimate the position to clip around, and the
522  "sigma" to use.
523  2. We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
524  large estimate will prevent the clipping from ever taking effect.
525  3. Rather than start with the median we start with a crude mode. This means that a set of magnitude
526  residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
527  this core to set our maximum sigma (see 2.)
528 
529  \return Struct of:
530  - zp ---------- Photometric zero point (mag)
531  - sigma ------- Standard deviation of fit of zero point (mag)
532  - ngood ------- Number of sources used to fit zero point
533  """
534  sigmaMax = self.config.sigmaMax
535 
536  dmag = ref - src
537 
538  indArr = np.argsort(dmag)
539  dmag = dmag[indArr]
540 
541  if srcErr is not None:
542  dmagErr = srcErr[indArr]
543  else:
544  dmagErr = np.ones(len(dmag))
545 
546  # need to remove nan elements to avoid errors in stats calculation with numpy
547  ind_noNan = np.array([i for i in range(len(dmag))
548  if (not np.isnan(dmag[i]) and not np.isnan(dmagErr[i]))])
549  dmag = dmag[ind_noNan]
550  dmagErr = dmagErr[ind_noNan]
551 
552  IQ_TO_STDEV = 0.741301109252802 # 1 sigma in units of interquartile (assume Gaussian)
553 
554  npt = len(dmag)
555  ngood = npt
556  good = None # set at end of first iteration
557  for i in range(self.config.nIter):
558  if i > 0:
559  npt = sum(good)
560 
561  center = None
562  if i == 0:
563  #
564  # Start by finding the mode
565  #
566  nhist = 20
567  try:
568  hist, edges = np.histogram(dmag, nhist, new=True)
569  except TypeError:
570  hist, edges = np.histogram(dmag, nhist) # they removed new=True around numpy 1.5
571  imode = np.arange(nhist)[np.where(hist == hist.max())]
572 
573  if imode[-1] - imode[0] + 1 == len(imode): # Multiple modes, but all contiguous
574  if zp0:
575  center = zp0
576  else:
577  center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
578 
579  peak = sum(hist[imode])/len(imode) # peak height
580 
581  # Estimate FWHM of mode
582  j = imode[0]
583  while j >= 0 and hist[j] > 0.5*peak:
584  j -= 1
585  j = max(j, 0)
586  q1 = dmag[sum(hist[range(j)])]
587 
588  j = imode[-1]
589  while j < nhist and hist[j] > 0.5*peak:
590  j += 1
591  j = min(j, nhist - 1)
592  j = min(sum(hist[range(j)]), npt - 1)
593  q3 = dmag[j]
594 
595  if q1 == q3:
596  q1 = dmag[int(0.25*npt)]
597  q3 = dmag[int(0.75*npt)]
598 
599  sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
600 
601  if sigmaMax is None:
602  sigmaMax = 2*sig # upper bound on st. dev. for clipping. multiplier is a heuristic
603 
604  self.log.debug("Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
605 
606  else:
607  if sigmaMax is None:
608  sigmaMax = dmag[-1] - dmag[0]
609 
610  center = np.median(dmag)
611  q1 = dmag[int(0.25*npt)]
612  q3 = dmag[int(0.75*npt)]
613  sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
614 
615  if center is None: # usually equivalent to (i > 0)
616  gdmag = dmag[good]
617  if self.config.useMedian:
618  center = np.median(gdmag)
619  else:
620  gdmagErr = dmagErr[good]
621  center = np.average(gdmag, weights=gdmagErr)
622 
623  q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
624  q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
625 
626  sig = IQ_TO_STDEV*(q3 - q1) # estimate of standard deviation
627 
628  good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax) # don't clip too softly
629 
630  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
631  if self.scatterPlot:
632  try:
633  self.fig.clf()
634 
635  axes = self.fig.add_axes((0.1, 0.1, 0.85, 0.80))
636 
637  axes.plot(ref[good], dmag[good] - center, "b+")
638  axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
639  linestyle='', color='b')
640 
641  bad = np.logical_not(good)
642  if len(ref[bad]) > 0:
643  axes.plot(ref[bad], dmag[bad] - center, "r+")
644  axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
645  linestyle='', color='r')
646 
647  axes.plot((-100, 100), (0, 0), "g-")
648  for x in (-1, 1):
649  axes.plot((-100, 100), x*0.05*np.ones(2), "g--")
650 
651  axes.set_ylim(-1.1, 1.1)
652  axes.set_xlim(24, 13)
653  axes.set_xlabel("Reference")
654  axes.set_ylabel("Reference - Instrumental")
655 
656  self.fig.show()
657 
658  if self.scatterPlot > 1:
659  reply = None
660  while i == 0 or reply != "c":
661  try:
662  reply = input("Next iteration? [ynhpc] ")
663  except EOFError:
664  reply = "n"
665 
666  if reply == "h":
667  print("Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
668  continue
669 
670  if reply in ("", "c", "n", "p", "y"):
671  break
672  else:
673  print("Unrecognised response: %s" % reply, file=sys.stderr)
674 
675  if reply == "n":
676  break
677  elif reply == "p":
678  import pdb
679  pdb.set_trace()
680  except Exception as e:
681  print("Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
682 
683  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
684 
685  old_ngood = ngood
686  ngood = sum(good)
687  if ngood == 0:
688  msg = "PhotoCal.getZeroPoint: no good stars remain"
689 
690  if i == 0: # failed the first time round -- probably all fell in one bin
691  center = np.average(dmag, weights=dmagErr)
692  msg += " on first iteration; using average of all calibration stars"
693 
694  self.log.warn(msg)
695 
696  return pipeBase.Struct(
697  zp=center,
698  sigma=sig,
699  ngood=len(dmag))
700  elif ngood == old_ngood:
701  break
702 
703  if False:
704  ref = ref[good]
705  dmag = dmag[good]
706  dmagErr = dmagErr[good]
707 
708  dmag = dmag[good]
709  dmagErr = dmagErr[good]
710  zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=True)
711  sigma = np.sqrt(1.0/weightSum)
712  return pipeBase.Struct(
713  zp=zp,
714  sigma=sigma,
715  ngood=len(dmag),
716  )
def __init__(self, refObjLoader, schema=None, kwds)
Create the photometric calibration task.
Definition: photoCal.py:252
def run(self, exposure, sourceCat, expId=0)
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
Definition: photoCal.py:397
def getSourceKeys(self, schema)
Return a struct containing the source catalog keys for fields used by PhotoCalTask.
Definition: photoCal.py:268
def extractMagArrays(self, matches, filterName, sourceKeys)
Extract magnitude and magnitude error arrays from the given matches.
Definition: photoCal.py:280
Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
Definition: photoCal.py:131
def getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...
Definition: photoCal.py:517
def displaySources(self, exposure, matches, reserved, frame=1)
Definition: photoCal.py:492