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