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