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