LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
LSSTDataManagementBasePackage
PhotoCal.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
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 <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 # \package lsst.meas.photocal
23 import math, os, sys
24 import numpy as np
25 
26 from lsst.meas.photocal.colorterms import Colorterm
27 import lsst.meas.algorithms.utils as malgUtil
28 import lsst.pex.logging as pexLog
29 import lsst.pex.config as pexConf
30 import lsst.pipe.base as pipeBase
31 import lsst.afw.table as afwTable
32 import lsst.afw.image as afwImage
33 import lsst.afw.display.ds9 as ds9
34 from lsst.meas.base.base import Version0FlagMapper
35 
36 def checkSourceFlags(source, keys):
37  """!Return True if the given source has all good flags set and none of the bad flags set.
38 
39  \param[in] source SourceRecord object to process.
40  \param[in] keys Struct of source catalog keys, as returned by PhotCalTask.getKeys()
41  """
42  for k in keys.goodFlags:
43  if not source.get(k): return False
44  if source.getPsfFluxFlag(): return False
45  for k in keys.badFlags:
46  if source.get(k): return False
47  return True
48 
49 class PhotoCalConfig(pexConf.Config):
50 
51  magLimit = pexConf.Field(dtype=float, doc="Don't use objects fainter than this magnitude", default=22.0)
52  doWriteOutput = pexConf.Field(
53  dtype=bool, default=True,
54  doc= "Write a field name astrom_usedByPhotoCal to the schema",
55  )
56  fluxField = pexConf.Field(
57  dtype=str, default="base_PsfFlux_flux", optional=False,
58  doc="Name of the source flux field to use. The associated flag field\n"\
59  "('<name>.flags') will be implicitly included in badFlags.\n"
60  )
61  applyColorTerms = pexConf.Field(
62  dtype=bool, default=True,
63  doc= "Apply photometric colour terms (if available) to reference stars",
64  )
65  goodFlags = pexConf.ListField(
66  dtype=str, optional=False,
67  default=[],
68  doc="List of source flag fields that must be set for a source to be used."
69  )
70  badFlags = pexConf.ListField(
71  dtype=str, optional=False,
72  default=["base_PixelFlags_flag_edge", "base_PixelFlags_flag_interpolated", "base_PixelFlags_flag_saturated"],
73  doc="List of source flag fields that will cause a source to be rejected when they are set."
74  )
75  sigmaMax = pexConf.Field(dtype=float, default=0.25, optional=True,
76  doc="maximum sigma to use when clipping")
77  nSigma = pexConf.Field(dtype=float, default=3.0, optional=False, doc="clip at nSigma")
78  useMedian = pexConf.Field(dtype=bool, default=True,
79  doc="use median instead of mean to compute zeropoint")
80  nIter = pexConf.Field(dtype=int, default=20, optional=False, doc="number of iterations")
81 
82 ## \addtogroup LSST_task_documentation
83 ## \{
84 ## \page photoCalTask
85 ## \ref PhotoCalTask_ "PhotoCalTask"
86 ## Detect positive and negative sources on an exposure and return a new SourceCatalog.
87 ## \}
88 
89 class PhotoCalTask(pipeBase.Task):
90  """!
91 \anchor PhotoCalTask_
92 
93 \brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
94 
95 \section meas_photocal_photocal_Contents Contents
96 
97  - \ref meas_photocal_photocal_Purpose
98  - \ref meas_photocal_photocal_Initialize
99  - \ref meas_photocal_photocal_IO
100  - \ref meas_photocal_photocal_Config
101  - \ref meas_photocal_photocal_Debug
102  - \ref meas_photocal_photocal_Example
103 
104 \section meas_photocal_photocal_Purpose Description
105 
106 \copybrief PhotoCalTask
107 
108 Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue.
109 The type of flux to use is specified by PhotoCalConfig.fluxField.
110 
111 The algorithm clips outliers iteratively, with parameters set in the configuration.
112 
113 \note This task can adds fields to the schema, so any code calling this task must ensure that
114 these columns are indeed present in the input match list; see \ref meas_photocal_photocal_Example
115 
116 \section meas_photocal_photocal_Initialize Task initialisation
117 
118 \copydoc init
119 
120 \section meas_photocal_photocal_IO Inputs/Outputs to the run method
121 
122 \copydoc run
123 
124 \section meas_photocal_photocal_Config Configuration parameters
125 
126 See \ref PhotoCalConfig
127 
128 \section meas_photocal_photocal_Debug Debug variables
129 
130 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
131 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
132 
133 The available variables in PhotoCalTask are:
134 <DL>
135  <DT> \c display
136  <DD> If True enable other debug outputs
137  <DT> \c displaySources
138  <DD> If True, display the exposure on ds9's frame 1 and overlay the source catalogue:
139  <DL>
140  <DT> red x
141  <DD> Bad objects
142  <DT> blue +
143  <DD> Matched objects deemed unsuitable for photometric calibration.
144  Additional information is:
145  - a cyan o for galaxies
146  - a magenta o for variables
147  <DT> magenta *
148  <DD> Objects that failed the flux cut
149  <DT> green o
150  <DD> Objects used in the photometric calibration
151  </DL>
152  <DT> \c scatterPlot
153  <DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude.
154  - good objects in blue
155  - rejected objects in red
156  (if \c scatterPlot is 2 or more, prompt to continue after each iteration)
157 </DL>
158 
159 \section meas_photocal_photocal_Example A complete example of using PhotoCalTask
160 
161 This code is in \link photoCalTask.py\endlink in the examples directory, and can be run as \em e.g.
162 \code
163 examples/photoCalTask.py
164 \endcode
165 \dontinclude photoCalTask.py
166 
167 Import the tasks (there are some other standard imports; read the file for details)
168 \skipline from lsst.pipe.tasks.astrometry
169 \skipline measPhotocal
170 
171 We need to create both our tasks before processing any data as the task constructors
172 can add extra columns to the schema which we get from the input catalogue, \c scrCat:
173 \skipline getSchema
174 
175 Astrometry first:
176 \skip AstrometryTask.ConfigClass
177 \until aTask
178 (that \c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises,
179 so we tell it to use the \c r band)
180 
181 Then photometry:
182 \skip measPhotocal
183 \until pTask
184 
185 If the schema has indeed changed we need to add the new columns to the source table
186 (yes; this should be easier!)
187 \skip srcCat
188 \until srcCat = cat
189 
190 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
191 task objects):
192 \skip matches
193 \until result
194 
195 We can then unpack and use the results:
196 \skip calib
197 \until np.log
198 
199 <HR>
200 To investigate the \ref meas_photocal_photocal_Debug, put something like
201 \code{.py}
202  import lsstDebug
203  def DebugInfo(name):
204  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
205  if name == "lsst.meas.photocal.PhotoCal":
206  di.display = 1
207 
208  return di
209 
210  lsstDebug.Info = DebugInfo
211 \endcode
212 into your debug.py file and run photoCalTask.py with the \c --debug flag.
213  """
214  ConfigClass = PhotoCalConfig
215  _DefaultName = "photoCal"
216 
217  # Need init as well as __init__ because "\copydoc __init__" fails (doxygen bug 732264)
218  def init(self, schema, **kwds):
219  """!Create the photometric calibration task. Most arguments are simply passed onto pipe.base.Task.
220 
221  \param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog
222  \param **kwds keyword arguments to be passed to the lsst.pipe.base.task.Task constructor
223 
224  """
225  self.__init__(schema, **kwds)
226 
227  def __init__(self, schema, **kwds):
228  """!Create the photometric calibration task. See PhotoCalTask.init for documentation
229  """
230  pipeBase.Task.__init__(self, **kwds)
231  if self.config.doWriteOutput:
232  if schema.getVersion() == 0:
233  self.outputField = schema.addField("classification.photometric", type="Flag",
234  doc="set if source was used in photometric calibration")
235  else:
236  self.outputField = schema.addField("photocal_photometricStandard", type="Flag",
237  doc="set if source was used in photometric calibration")
238  else:
239  self.outputField = None
240 
241  def getKeys(self, schema):
242  """!Return a struct containing the source catalog keys for fields used by PhotoCalTask."""
243  fluxField = self.config.fluxField
244  if schema.getVersion() == 0:
245  if fluxField == 'base_PsfFlux_flux': fluxField = "flux.psf"
246  flux = schema.find(fluxField).key
247  fluxErr = schema.find(fluxField + ".err").key
248  goodFlags = [schema.find(name).key for name in Version0FlagMapper(self.config.goodFlags)]
249  badFlags = [schema.find(name).key for name in Version0FlagMapper(self.config.badFlags)]
250  else:
251  flux = schema.find(self.config.fluxField).key
252  fluxErr = schema.find(self.config.fluxField + "Sigma").key
253  goodFlags = [schema.find(name).key for name in self.config.goodFlags]
254  badFlags = [schema.find(name).key for name in self.config.badFlags]
255  return pipeBase.Struct(flux=flux, fluxErr=fluxErr, goodFlags=goodFlags, badFlags=badFlags)
256 
257  @pipeBase.timeMethod
258  def selectMatches(self, matches, keys, frame=None):
259  """!Select reference/source matches according the criteria specified in the config.
260 
261  \param[in] matches ReferenceMatchVector (not modified)
262  \param[in] keys Struct of source catalog keys, as returned by getKeys()
263  \param[in] frame ds9 frame number to use for debugging display
264  if frame is non-None, display information about trimmed objects on that ds9 frame:
265  - Bad: red x
266  - Unsuitable objects: blue + (and a cyan o if a galaxy)
267  - Failed flux cut: magenta *
268 
269  \return a \link lsst.afw.table.ReferenceMatchVector\endlink that contains only the selected matches.
270  If a schema was passed during task construction, a flag field will be set on sources
271  in the selected matches.
272 
273  \throws ValueError There are no valid matches.
274  """
275 
276  self.log.logdebug("Number of input matches: %d" % (len(matches)))
277  if len(matches) == 0:
278  raise ValueError("No input matches")
279 
280  # Only use stars for which the flags indicate the photometry is good.
281  afterFlagCutInd = [i for i, m in enumerate(matches) if checkSourceFlags(m.second, keys)]
282  afterFlagCut = [matches[i] for i in afterFlagCutInd]
283  self.log.logdebug("Number of matches after source flag cuts: %d" % (len(afterFlagCut)))
284 
285  if len(afterFlagCut) != len(matches):
286  if frame is not None:
287  with ds9.Buffering():
288  for i, m in enumerate(matches):
289  if i not in afterFlagCutInd:
290  x, y = m.second.getCentroid()
291  ds9.dot("x", x, y, size=4, frame=frame, ctype=ds9.RED)
292 
293  matches = afterFlagCut
294 
295  if len(matches) == 0:
296  raise ValueError("All matches eliminated by source flags")
297 
298  refSchema = matches[0].first.schema
299  try:
300  refKey = refSchema.find("photometric").key
301  try:
302  stargalKey = refSchema.find("stargal").key
303  except:
304  stargalKey = None
305 
306  try:
307  varKey = refSchema.find("var").key
308  except:
309  varKey = None
310  except:
311  self.log.warn("No 'photometric' flag key found in reference schema.")
312  refKey = None
313 
314  if refKey is not None:
315  afterRefCutInd = [i for i, m in enumerate(matches) if m.first.get(refKey)]
316  afterRefCut = [matches[i] for i in afterRefCutInd]
317 
318  if len(afterRefCut) != len(matches):
319  if frame is not None:
320  with ds9.Buffering():
321  for i, m in enumerate(matches):
322  if i not in afterRefCutInd:
323  x, y = m.second.getCentroid()
324  ds9.dot("+", x, y, size=4, frame=frame, ctype=ds9.BLUE)
325 
326  if stargalKey and not m.first.get(stargalKey):
327  ds9.dot("o", x, y, size=6, frame=frame, ctype=ds9.CYAN)
328  if varKey and m.first.get(varKey):
329  ds9.dot("o", x, y, size=6, frame=frame, ctype=ds9.MAGENTA)
330 
331  matches = afterRefCut
332 
333  self.log.logdebug("Number of matches after reference catalog cuts: %d" % (len(matches)))
334  if len(matches) == 0:
335  raise RuntimeError("No sources remain in match list after reference catalog cuts.")
336  fluxKey = refSchema.find("flux").key
337  if self.config.magLimit is not None:
338  fluxLimit = 10.0**(-self.config.magLimit/2.5)
339 
340  afterMagCutInd = [i for i, m in enumerate(matches) if (m.first.get(fluxKey) > fluxLimit
341  and m.second.getPsfFlux() > 0.0)]
342  else:
343  afterMagCutInd = [i for i, m in enumerate(matches) if m.second.getPsfFlux() > 0.0]
344 
345  afterMagCut = [matches[i] for i in afterMagCutInd]
346 
347  if len(afterMagCut) != len(matches):
348  if frame is not None:
349  with ds9.Buffering():
350  for i, m in enumerate(matches):
351  if i not in afterMagCutInd:
352  x, y = m.second.getCentroid()
353  ds9.dot("*", x, y, size=4, frame=frame, ctype=ds9.MAGENTA)
354 
355  matches = afterMagCut
356 
357  self.log.logdebug("Number of matches after magnitude limit cuts: %d" % (len(matches)))
358 
359  if len(matches) == 0:
360  raise RuntimeError("No sources remaining in match list after magnitude limit cuts.")
361 
362  if frame is not None:
363  with ds9.Buffering():
364  for m in matches:
365  x, y = m.second.getCentroid()
366  ds9.dot("o", x, y, size=4, frame=frame, ctype=ds9.GREEN)
367 
369  for m in matches:
370  if self.outputField is not None:
371  m.second.set(self.outputField, True)
372  result.append(m)
373  return result
374 
375  @pipeBase.timeMethod
376  def extractMagArrays(self, matches, filterName, keys):
377  """!Extract magnitude and magnitude error arrays from the given matches.
378 
379  \param[in] matches \link lsst::afw::table::ReferenceMatchVector\endlink object containing reference/source matches
380  \param[in] filterName Name of filter being calibrated
381  \param[in] keys Struct of source catalog keys, as returned by getKeys()
382 
383  \return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays
384  where magErr is an error in the magnitude; the error in srcMag - refMag.
385  \note These are the \em inputs to the photometric calibration, some may have been
386  discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813)
387  """
388  srcFlux = np.array([m.second.get(keys.flux) for m in matches])
389  srcFluxErr = np.array([m.second.get(keys.fluxErr) for m in matches])
390  if not np.all(np.isfinite(srcFluxErr)):
391  self.log.warn("Source catalog does not have flux uncertainties; using sqrt(flux).")
392  srcFluxErr = np.sqrt(srcFlux)
393 
394  if not matches:
395  raise RuntimeError("No reference stars are available")
396 
397  refSchema = matches[0].first.schema
398  if self.config.applyColorTerms:
399  ct = Colorterm.getColorterm(filterName)
400  else:
401  ct = None
402 
403  if ct: # we have a colour term to worry about
404  fluxNames = [ct.primary, ct.secondary]
405  missingFluxes = []
406  for flux in fluxNames:
407  try:
408  refSchema.find(flux).key
409  except KeyError:
410  missingFluxes.append(flux)
411 
412  if missingFluxes:
413  self.log.warn("Source catalog does not have fluxes for %s; ignoring color terms" %
414  " ".join(missingFluxes))
415  ct = None
416 
417  if not ct:
418  fluxNames = ["flux"]
419 
420  refFluxes = []
421  refFluxErrors = []
422  for flux in fluxNames:
423  fluxKey = refSchema.find(flux).key
424  refFlux = np.array([m.first.get(fluxKey) for m in matches])
425  try:
426  fluxErrKey = refSchema.find(flux + ".err").key
427  refFluxErr = np.array([m.first.get(fluxErrKey) for m in matches])
428  except KeyError:
429  # Catalogue may not have flux uncertainties; HACK
430  self.log.warn("Reference catalog does not have flux uncertainties for %s; using sqrt(flux)."
431  % flux)
432  refFluxErr = np.sqrt(refFlux)
433 
434  refFluxes.append(refFlux)
435  refFluxErrors.append(refFluxErr)
436 
437  srcMag = -2.5*np.log10(srcFlux)
438  if ct: # we have a colour term to worry about
439  refMag = -2.5*np.log10(refFluxes[0]) # primary
440  refMag2 = -2.5*np.log10(refFluxes[1]) # secondary
441 
442  refMag = ct.transformMags(filterName, refMag, refMag2)
443  refFluxErr = ct.propagateFluxErrors(filterName, refFluxErrors[0], refFluxErrors[1])
444  else:
445  refMag = -2.5*np.log10(refFluxes[0])
446 
447  # Fitting with error bars in both axes is hard, so transfer all
448  # the error to src, then convert to magnitude
449  fluxErr = np.hypot(srcFluxErr, refFluxErr)
450  magErr = fluxErr / (srcFlux * np.log(10))
451 
452  srcMagErr = srcFluxErr / (srcFlux * np.log(10))
453  refMagErr = refFluxErr / (refFlux * np.log(10))
454 
455  return pipeBase.Struct(
456  srcMag = srcMag,
457  refMag = refMag,
458  magErr = magErr,
459  srcMagErr = srcMagErr,
460  refMagErr = refMagErr
461  )
462 
463  @pipeBase.timeMethod
464  def run(self, exposure, matches):
465  """!Do photometric calibration - select matches to use and (possibly iteratively) compute
466  the zero point.
467 
468  \param[in] exposure Exposure upon which the sources in the matches were detected.
469  \param[in] matches Input lsst.afw.table.ReferenceMatchVector
470  (\em i.e. a list of lsst.afw.table.Match with
471  \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord ---
472  the reference object and matched object respectively).
473  (will not be modified except to set the outputField if requested.).
474 
475  \return Struct of:
476  - calib ------- \link lsst::afw::image::Calib\endlink object containing the zero point
477  - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays
478  - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.
479 
480 The exposure is only used to provide the name of the filter being calibrated (it may also be
481 used to generate debugging plots).
482 
483 The reference objects:
484  - Must include a field \c photometric; True for objects which should be considered as photometric standards
485  - Must include a field \c flux; the flux used to impose a magnitude limit and also to calibrate the data to (unless a colour term is specified, in which case ColorTerm.primary is used; See https://jira.lsstcorp.org/browse/DM-933)
486  - May include a field \c stargal; if present, True means that the object is a star
487  - May include a field \c var; if present, True means that the object is variable
488 
489 The measured sources:
490 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration
491 
492 \throws RuntimeError with the following strings:
493 
494 <DL>
495 <DT> `sources' schema does not contain the calibration object flag "XXX"`
496 <DD> The constructor added fields to the schema that aren't in the Sources
497 <DT> No input matches
498 <DD> The input match vector is empty
499 <DT> All matches eliminated by source flags
500 <DD> The flags specified by \c badFlags in the config eliminated all candidate objects
501 <DT> No sources remain in match list after reference catalog cuts
502 <DD> The reference catalogue has a column "photometric", but no matched objects have it set
503 <DT> No sources remaining in match list after magnitude limit cuts
504 <DD> All surviving matches are either too faint in the catalogue or have negative or \c NaN flux
505 <DT> No reference stars are available
506 <DD> No matches survive all the checks
507 </DL>
508 
509  """
510  global scatterPlot, fig
511  import lsstDebug
512 
513  display = lsstDebug.Info(__name__).display
514  displaySources = display and lsstDebug.Info(__name__).displaySources
515  scatterPlot = display and lsstDebug.Info(__name__).scatterPlot
516 
517  if scatterPlot:
518  from matplotlib import pyplot
519  try:
520  fig.clf()
521  except:
522  fig = pyplot.figure()
523 
524  if displaySources:
525  frame = 1
526  ds9.mtv(exposure, frame=frame, title="photocal")
527  else:
528  frame = None
529 
530  keys = self.getKeys(matches[0].second.schema)
531  matches = self.selectMatches(matches, keys, frame=frame)
532  arrays = self.extractMagArrays(matches, exposure.getFilter().getName(), keys)
533 
534  if matches and self.outputField:
535  try:
536  # matches[].second is a measured source, wherein we wish to set outputField.
537  # Check that the field is present in the Sources schema.
538  matches[0].second.getSchema().find(self.outputField)
539  except:
540  raise RuntimeError("sources' schema does not contain the used-in-calibration flag \"%s\"" %
541  self.config.outputField)
542 
543  # Fit for zeropoint. We can run the code more than once, so as to
544  # give good stars that got clipped by a bad first guess a second
545  # chance.
546  # FIXME: these should be config values
547 
548  calib = afwImage.Calib()
549  zp = None # initial guess
550  r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr, zp0=zp)
551  zp = r.zp
552  self.log.info("Magnitude zero point: %f +/- %f from %d stars" % (r.zp, r.sigma, r.ngood))
553 
554  flux0 = 10**(0.4*r.zp) # Flux of mag=0 star
555  flux0err = 0.4*math.log(10)*flux0*r.sigma # Error in flux0
556 
557  calib.setFluxMag0(flux0, flux0err)
558 
559  return pipeBase.Struct(
560  calib = calib,
561  arrays = arrays,
562  matches = matches,
563  zp = r.zp,
564  sigma = r.sigma,
565  ngood = r.ngood,
566  )
567 
568  def getZeroPoint(self, src, ref, srcErr=None, zp0=None):
569  """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
570 
571  We perform nIter iterations of a simple sigma-clipping algorithm with a a couple of twists:
572  1. We use the median/interquartile range to estimate the position to clip around, and the
573  "sigma" to use.
574  2. We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
575  large estimate will prevent the clipping from ever taking effect.
576  3. Rather than start with the median we start with a crude mode. This means that a set of magnitude
577  residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
578  this core to set our maximum sigma (see 2.)
579  """
580 
581  sigmaMax = self.config.sigmaMax
582 
583  dmag = ref - src
584 
585  i = np.argsort(dmag)
586  dmag = dmag[i]
587 
588  if srcErr is not None:
589  dmagErr = srcErr[i]
590  else:
591  dmagErr = np.ones(len(dmag))
592 
593  # need to remove nan elements to avoid errors in stats calculation with numpy
594  ind_noNan = np.array([ i for i in range(len(dmag)) if (not np.isnan(dmag[i]) and not np.isnan(dmagErr[i])) ])
595  dmag = dmag[ind_noNan]
596  dmagErr = dmagErr[ind_noNan]
597 
598  IQ_TO_STDEV = 0.741301109252802; # 1 sigma in units of interquartile (assume Gaussian)
599 
600  npt = len(dmag)
601  ngood = npt
602  for i in range(self.config.nIter):
603  if i > 0:
604  npt = sum(good)
605 
606  center = None
607  if i == 0:
608  #
609  # Start by finding the mode
610  #
611  nhist = 20
612  try:
613  hist, edges = np.histogram(dmag, nhist, new=True)
614  except TypeError:
615  hist, edges = np.histogram(dmag, nhist) # they removed new=True around numpy 1.5
616  imode = np.arange(nhist)[np.where(hist == hist.max())]
617 
618  if imode[-1] - imode[0] + 1 == len(imode): # Multiple modes, but all contiguous
619  if zp0:
620  center = zp0
621  else:
622  center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
623 
624  peak = sum(hist[imode])/len(imode) # peak height
625 
626  # Estimate FWHM of mode
627  j = imode[0]
628  while j >= 0 and hist[j] > 0.5*peak:
629  j -= 1
630  j = max(j, 0)
631  q1 = dmag[sum(hist[range(j)])]
632 
633  j = imode[-1]
634  while j < nhist and hist[j] > 0.5*peak:
635  j += 1
636  j = min(j, nhist - 1)
637  j = min(sum(hist[range(j)]), npt - 1)
638  q3 = dmag[j]
639 
640  if q1 == q3:
641  q1 = dmag[int(0.25*npt)]
642  q3 = dmag[int(0.75*npt)]
643 
644  sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
645 
646  if sigmaMax is None:
647  sigmaMax = 2*sig # upper bound on st. dev. for clipping. multiplier is a heuristic
648 
649  self.log.logdebug("Photo calibration histogram: center = %.2f, sig = %.2f"
650  % (center, sig))
651 
652  else:
653  if sigmaMax is None:
654  sigmaMax = dmag[-1] - dmag[0]
655 
656  center = np.median(dmag)
657  q1 = dmag[int(0.25*npt)]
658  q3 = dmag[int(0.75*npt)]
659  sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian)
660 
661  if center is None: # usually equivalent to (i > 0)
662  gdmag = dmag[good]
663  if self.config.useMedian:
664  center = np.median(gdmag)
665  else:
666  gdmagErr = dmagErr[good]
667  center = np.average(gdmag, weights=gdmagErr)
668 
669  q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
670  q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
671 
672  sig = IQ_TO_STDEV*(q3 - q1) # estimate of standard deviation
673 
674  good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax) # don't clip too softly
675 
676  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
677 
678  if scatterPlot:
679  from matplotlib import pyplot
680  try:
681  fig.clf()
682 
683  axes = fig.add_axes((0.1, 0.1, 0.85, 0.80));
684 
685  axes.plot(ref[good], dmag[good] - center, "b+")
686  axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
687  linestyle='', color='b')
688 
689  bad = np.logical_not(good)
690  if len(ref[bad]) > 0:
691  axes.plot(ref[bad], dmag[bad] - center, "r+")
692  axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
693  linestyle='', color='r')
694 
695  axes.plot((-100, 100), (0, 0), "g-")
696  for x in (-1, 1):
697  axes.plot((-100, 100), x*0.05*np.ones(2), "g--")
698 
699  axes.set_ylim(-1.1, 1.1)
700  axes.set_xlim(24, 13)
701  axes.set_xlabel("Reference")
702  axes.set_ylabel("Reference - Instrumental")
703 
704  fig.show()
705 
706  if scatterPlot > 1:
707  while i == 0 or reply != "c":
708  try:
709  reply = raw_input("Next iteration? [ynhpc] ")
710  except EOFError:
711  reply = "n"
712 
713  if reply == "h":
714  print >> sys.stderr, "Options: c[ontinue] h[elp] n[o] p[db] y[es]"
715  continue
716 
717  if reply in ("", "c", "n", "p", "y"):
718  break
719  else:
720  print >> sys.stderr, "Unrecognised response: %s" % reply
721 
722  if reply == "n":
723  break
724  elif reply == "p":
725  import pdb; pdb.set_trace()
726  except Exception, e:
727  print >> sys.stderr, "Error plotting in PhotoCal.getZeroPoint: %s" % e
728 
729  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
730 
731  old_ngood = ngood
732  ngood = sum(good)
733  if ngood == 0:
734  msg = "PhotoCal.getZeroPoint: no good stars remain"
735 
736  if i == 0: # failed the first time round -- probably all fell in one bin
737  center = np.average(dmag, weights=dmagErr)
738  msg += " on first iteration; using average of all calibration stars"
739 
740  self.log.log(self.log.WARN, msg)
741 
742  return pipeBase.Struct(
743  zp = center,
744  sigma = sig,
745  ngood = len(dmag)
746  )
747  elif ngood == old_ngood:
748  break
749 
750  if False:
751  ref = ref[good]
752  dmag = dmag[good]
753  dmagErr = dmagErr[good]
754 
755  dmag = dmag[good]
756  dmagErr = dmagErr[good]
757  return pipeBase.Struct(
758  zp = np.average(dmag, weights=dmagErr),
759  sigma = np.std(dmag, ddof=1),
760  ngood = len(dmag)
761  )
def __init__
Create the photometric calibration task.
Definition: PhotoCal.py:227
def init
Create the photometric calibration task.
Definition: PhotoCal.py:218
def run
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
Definition: PhotoCal.py:464
def getKeys
Return a struct containing the source catalog keys for fields used by PhotoCalTask.
Definition: PhotoCal.py:241
std::vector< ReferenceMatch > ReferenceMatchVector
Definition: fwd.h:97
double min
Definition: attributes.cc:216
def extractMagArrays
Extract magnitude and magnitude error arrays from the given matches.
Definition: PhotoCal.py:376
double max
Definition: attributes.cc:218
def checkSourceFlags
Return True if the given source has all good flags set and none of the bad flags set.
Definition: PhotoCal.py:36
def selectMatches
Select reference/source matches according the criteria specified in the config.
Definition: PhotoCal.py:258
double sum
Definition: NaiveFlux.cc:137
Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
Definition: PhotoCal.py:89
def Version0FlagMapper
Definition: base.py:75
def getZeroPoint
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...
Definition: PhotoCal.py:568