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