1 from __future__
import absolute_import, division
24 from itertools
import izip
30 from .colorterms
import Colorterm
34 from lsst.afw.image import abMagFromFlux, abMagErrFromFluxErr, fluxFromABMag, Calib
39 """!Return True if the given source has all good flags set and none of the bad flags set.
41 \param[in] source SourceRecord object to process.
42 \param[in] sourceKeys Struct of source catalog keys, as returned by PhotCalTask.getSourceKeys()
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
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",
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"
63 applyColorTerms = pexConf.Field(
64 dtype=bool, default=
True,
65 doc=
"Apply photometric colour terms (if available) to reference stars",
67 goodFlags = pexConf.ListField(
68 dtype=str, optional=
False,
70 doc=
"List of source flag fields that must be set for a source to be used."
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."
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")
96 \brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
98 \section meas_photocal_photocal_Contents Contents
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
107 \section meas_photocal_photocal_Purpose Description
109 \copybrief PhotoCalTask
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.
114 The algorithm clips outliers iteratively, with parameters set in the configuration.
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
119 \section meas_photocal_photocal_Initialize Task initialisation
121 \copydoc \_\_init\_\_
123 \section meas_photocal_photocal_IO Inputs/Outputs to the run method
127 \section meas_photocal_photocal_Config Configuration parameters
129 See \ref PhotoCalConfig
131 \section meas_photocal_photocal_Debug Debug variables
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.
136 The available variables in PhotoCalTask are:
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:
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
151 <DD> Objects that failed the flux cut
153 <DD> Objects used in the photometric calibration
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)
162 \section meas_photocal_photocal_Example A complete example of using PhotoCalTask
164 This code is in \link photoCalTask.py\endlink in the examples directory, and can be run as \em e.g.
166 examples/photoCalTask.py
168 \dontinclude photoCalTask.py
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
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:
179 \skip AstrometryTask.ConfigClass
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)
188 If the schema has indeed changed we need to add the new columns to the source table
189 (yes; this should be easier!)
193 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
198 We can then unpack and use the results:
203 To investigate the \ref meas_photocal_photocal_Debug, put something like
207 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
208 if name == "lsst.meas.photocal.PhotoCal":
213 lsstDebug.Info = DebugInfo
215 into your debug.py file and run photoCalTask.py with the \c --debug flag.
217 ConfigClass = PhotoCalConfig
218 _DefaultName =
"photoCal"
221 """!Create the photometric calibration task. See PhotoCalTask.init for documentation
223 pipeBase.Task.__init__(self, **kwds)
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")
231 self.
outputField = schema.addField(
"photocal_photometricStandard", type=
"Flag",
232 doc=
"set if source was used in photometric calibration")
237 """!Return a struct containing the source catalog keys for fields used by PhotoCalTask.
239 Returned fields include:
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
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]
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]
259 return pipeBase.Struct(flux=flux, fluxErr=fluxErr, goodFlags=goodFlags, badFlags=badFlags)
263 """!Select reference/source matches according the criteria specified in the config.
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:
271 - Unsuitable objects: blue + (and a cyan o if a galaxy)
272 - Failed flux cut: magenta *
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.
278 \throws ValueError There are no valid matches.
281 self.log.logdebug(
"Number of input matches: %d" % (len(matches)))
282 if len(matches) == 0:
283 raise ValueError(
"No input matches")
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)))
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)
298 matches = afterFlagCut
300 if len(matches) == 0:
301 raise ValueError(
"All matches eliminated by source flags")
303 refSchema = matches[0].first.schema
305 photometricKey = refSchema.find(
"photometric").key
307 resolvedKey = refSchema.find(
"resolved").key
312 variableKey = refSchema.find(
"variable").key
316 self.log.warn(
"No 'photometric' flag key found in reference schema.")
317 photometricKey =
None
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]
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)
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)
336 matches = afterRefCut
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.")
342 fluxKey = refSchema.find(fluxName).key
343 if self.config.magLimit
is not None:
346 afterMagCutInd = [i
for i, m
in enumerate(matches)
if (m.first.get(fluxKey) > fluxLimit
347 and m.second.getPsfFlux() > 0.0)]
349 afterMagCutInd = [i
for i, m
in enumerate(matches)
if m.second.getPsfFlux() > 0.0]
351 afterMagCut = [matches[i]
for i
in afterMagCutInd]
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)
361 matches = afterMagCut
363 self.log.logdebug(
"Number of matches after magnitude limit cuts: %d" % (len(matches)))
365 if len(matches) == 0:
366 raise RuntimeError(
"No sources remaining in match list after magnitude limit cuts.")
368 if frame
is not None:
369 with ds9.Buffering():
371 x, y = m.second.getCentroid()
372 ds9.dot(
"o", x, y, size=4, frame=frame, ctype=ds9.GREEN)
383 """!Extract magnitude and magnitude error arrays from the given matches.
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()
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
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)
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)):
400 self.log.warn(
"Source catalog does not have flux uncertainties; using sqrt(flux).")
401 srcFluxErrArr = np.sqrt(srcFluxArr)
404 JanskysPerABFlux = 3631.0
405 srcFluxArr = srcFluxArr * JanskysPerABFlux
406 srcFluxErrArr = srcFluxErrArr * JanskysPerABFlux
409 raise RuntimeError(
"No reference stars are available")
411 refSchema = matches[0].first.schema
412 if self.config.applyColorTerms:
413 ct = Colorterm.getColorterm(filterName)
418 fluxFieldList = [
getRefFluxField(refSchema, filt)
for filt
in (ct.primary, ct.secondary)]
419 missingFluxFieldList = []
420 for fluxField
in fluxFieldList:
422 refSchema.find(fluxField).key
424 missingFluxFieldList.append(fluxField)
426 if missingFluxFieldList:
427 self.log.warn(
"Source catalog does not have fluxes for %s; ignoring color terms" %
428 " ".join(missingFluxFieldList))
435 refFluxErrArrList = []
436 for fluxField
in fluxFieldList:
437 fluxKey = refSchema.find(fluxField).key
438 refFluxArr = np.array([m.first.get(fluxKey)
for m
in matches])
440 fluxErrKey = refSchema.find(fluxField +
"Sigma").key
441 refFluxErrArr = np.array([m.first.get(fluxErrKey)
for m
in matches])
444 self.log.warn(
"Reference catalog does not have flux uncertainties for %s; using sqrt(flux)."
446 refFluxErrArr = np.sqrt(refFluxArr)
448 refFluxArrList.append(refFluxArr)
449 refFluxErrArrList.append(refFluxErrArr)
452 refMagArr1 = np.array([
abMagFromFlux(rf1)
for rf1
in refFluxArrList[0]])
453 refMagArr2 = np.array([
abMagFromFlux(rf2)
for rf2
in refFluxArrList[1]])
455 refMagArr = ct.transformMags(filterName, refMagArr1, refMagArr2)
456 refFluxErrArr = ct.propagateFluxErrors(filterName, refFluxErrArrList[0], refFluxErrArrList[1])
458 refMagArr = np.array([
abMagFromFlux(rf)
for rf
in refFluxArrList[0]])
460 srcMagArr = np.array([
abMagFromFlux(sf)
for sf
in srcFluxArr])
464 magErrArr = np.array([
abMagErrFromFluxErr(fe, sf)
for fe, sf
in izip(srcFluxErrArr, srcFluxArr)])
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)])
469 return pipeBase.Struct(
473 srcMagErr = srcMagErrArr,
474 refMagErr = refMagErrArr,
475 refFluxFieldList = fluxFieldList,
479 def run(self, exposure, matches):
480 """!Do photometric calibration - select matches to use and (possibly iteratively) compute
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.).
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.
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).
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
507 The measured sources:
508 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration
510 \throws RuntimeError with the following strings:
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
530 displaySources = display
and lsstDebug.Info(__name__).displaySources
534 from matplotlib
import pyplot
538 self.
fig = pyplot.figure()
542 ds9.mtv(exposure, frame=frame, title=
"photocal")
546 filterName = exposure.getFilter().getName()
548 matches = self.
selectMatches(matches=matches, sourceKeys=sourceKeys, filterName=filterName,
550 arrays = self.
extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys)
556 matches[0].second.getSchema().find(self.
outputField)
558 raise RuntimeError(
"sources' schema does not contain the used-in-calibration flag \"%s\"" %
559 self.config.outputField)
568 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr, zp0=zp)
570 self.log.info(
"Magnitude zero point: %f +/- %f from %d stars" % (r.zp, r.sigma, r.ngood))
572 flux0 = 10**(0.4*r.zp)
573 flux0err = 0.4*math.log(10)*flux0*r.sigma
575 calib.setFluxMag0(flux0, flux0err)
577 return pipeBase.Struct(
587 """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
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
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.)
598 sigmaMax = self.config.sigmaMax
602 indArr = np.argsort(dmag)
605 if srcErr
is not None:
606 dmagErr = srcErr[indArr]
608 dmagErr = np.ones(len(dmag))
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]
615 IQ_TO_STDEV = 0.741301109252802;
620 for i
in range(self.config.nIter):
631 hist, edges = np.histogram(dmag, nhist, new=
True)
633 hist, edges = np.histogram(dmag, nhist)
634 imode = np.arange(nhist)[np.where(hist == hist.max())]
636 if imode[-1] - imode[0] + 1 == len(imode):
640 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
642 peak =
sum(hist[imode])/len(imode)
646 while j >= 0
and hist[j] > 0.5*peak:
649 q1 = dmag[
sum(hist[range(j)])]
652 while j < nhist
and hist[j] > 0.5*peak:
654 j =
min(j, nhist - 1)
655 j =
min(
sum(hist[range(j)]), npt - 1)
659 q1 = dmag[int(0.25*npt)]
660 q3 = dmag[int(0.75*npt)]
667 self.log.logdebug(
"Photo calibration histogram: center = %.2f, sig = %.2f"
672 sigmaMax = dmag[-1] - dmag[0]
674 center = np.median(dmag)
675 q1 = dmag[int(0.25*npt)]
676 q3 = dmag[int(0.75*npt)]
681 if self.config.useMedian:
682 center = np.median(gdmag)
684 gdmagErr = dmagErr[good]
685 center = np.average(gdmag, weights=gdmagErr)
687 q3 = gdmag[
min(int(0.75*npt + 0.5), npt - 1)]
688 q1 = gdmag[
min(int(0.25*npt + 0.5), npt - 1)]
690 sig = IQ_TO_STDEV*(q3 - q1)
692 good = abs(dmag - center) < self.config.nSigma*
min(sig, sigmaMax)
699 axes = self.fig.add_axes((0.1, 0.1, 0.85, 0.80));
701 axes.plot(ref[good], dmag[good] - center,
"b+")
702 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
703 linestyle=
'', color=
'b')
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')
711 axes.plot((-100, 100), (0, 0), "g-")
713 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
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")
724 while i == 0
or reply !=
"c":
726 reply = raw_input(
"Next iteration? [ynhpc] ")
731 print >> sys.stderr,
"Options: c[ontinue] h[elp] n[o] p[db] y[es]"
734 if reply
in (
"",
"c",
"n",
"p",
"y"):
737 print >> sys.stderr,
"Unrecognised response: %s" % reply
742 import pdb; pdb.set_trace()
744 print >> sys.stderr,
"Error plotting in PhotoCal.getZeroPoint: %s" % e
751 msg =
"PhotoCal.getZeroPoint: no good stars remain"
754 center = np.average(dmag, weights=dmagErr)
755 msg +=
" on first iteration; using average of all calibration stars"
757 self.log.log(self.log.WARN, msg)
759 return pipeBase.Struct(
764 elif ngood == old_ngood:
770 dmagErr = dmagErr[good]
773 dmagErr = dmagErr[good]
774 return pipeBase.Struct(
775 zp = np.average(dmag, weights=dmagErr),
776 sigma = np.std(dmag, ddof=1),
def __init__
Create the photometric calibration task.
def run
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
def getSourceKeys
Return a struct containing the source catalog keys for fields used by PhotoCalTask.
double abMagErrFromFluxErr(double fluxErr, double flux)
Compute AB magnitude error from flux and flux error in Janskys.
std::vector< ReferenceMatch > ReferenceMatchVector
double abMagFromFlux(double flux)
Compute AB magnitude from flux in Janskys.
boost::enable_if< typename ExpressionTraits< Scalar >::IsScalar, Scalar >::type sum(Scalar const &scalar)
def extractMagArrays
Extract magnitude and magnitude error arrays from the given matches.
double fluxFromABMag(double mag)
Compute flux in Janskys from AB magnitude.
def checkSourceFlags
Return True if the given source has all good flags set and none of the bad flags set.
def selectMatches
Select reference/source matches according the criteria specified in the config.
Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
def getRefFluxField
Get name of flux field in schema.
def getZeroPoint
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...