37 """!Return True if the given source has all good flags set and none of the bad flags set.
39 \param[in] source SourceRecord object to process.
40 \param[in] keys Struct of source catalog keys, as returned by PhotCalTask.getKeys()
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
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",
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"
61 applyColorTerms = pexConf.Field(
62 dtype=bool, default=
True,
63 doc=
"Apply photometric colour terms (if available) to reference stars",
65 goodFlags = pexConf.ListField(
66 dtype=str, optional=
False,
68 doc=
"List of source flag fields that must be set for a source to be used."
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."
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")
93 \brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
95 \section meas_photocal_photocal_Contents Contents
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
104 \section meas_photocal_photocal_Purpose Description
106 \copybrief PhotoCalTask
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.
111 The algorithm clips outliers iteratively, with parameters set in the configuration.
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
116 \section meas_photocal_photocal_Initialize Task initialisation
120 \section meas_photocal_photocal_IO Inputs/Outputs to the run method
124 \section meas_photocal_photocal_Config Configuration parameters
126 See \ref PhotoCalConfig
128 \section meas_photocal_photocal_Debug Debug variables
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.
133 The available variables in PhotoCalTask are:
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:
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
148 <DD> Objects that failed the flux cut
150 <DD> Objects used in the photometric calibration
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)
159 \section meas_photocal_photocal_Example A complete example of using PhotoCalTask
161 This code is in \link photoCalTask.py\endlink in the examples directory, and can be run as \em e.g.
163 examples/photoCalTask.py
165 \dontinclude photoCalTask.py
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
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:
176 \skip AstrometryTask.ConfigClass
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)
185 If the schema has indeed changed we need to add the new columns to the source table
186 (yes; this should be easier!)
190 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
195 We can then unpack and use the results:
200 To investigate the \ref meas_photocal_photocal_Debug, put something like
204 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
205 if name == "lsst.meas.photocal.PhotoCal":
210 lsstDebug.Info = DebugInfo
212 into your debug.py file and run photoCalTask.py with the \c --debug flag.
214 ConfigClass = PhotoCalConfig
215 _DefaultName =
"photoCal"
218 def init(self, schema, **kwds):
219 """!Create the photometric calibration task. Most arguments are simply passed onto pipe.base.Task.
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
228 """!Create the photometric calibration task. See PhotoCalTask.init for documentation
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")
236 self.
outputField = schema.addField(
"photocal_photometricStandard", type=
"Flag",
237 doc=
"set if source was used in photometric calibration")
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)]
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)
259 """!Select reference/source matches according the criteria specified in the config.
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:
266 - Unsuitable objects: blue + (and a cyan o if a galaxy)
267 - Failed flux cut: magenta *
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.
273 \throws ValueError There are no valid matches.
276 self.log.logdebug(
"Number of input matches: %d" % (len(matches)))
277 if len(matches) == 0:
278 raise ValueError(
"No input matches")
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)))
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)
293 matches = afterFlagCut
295 if len(matches) == 0:
296 raise ValueError(
"All matches eliminated by source flags")
298 refSchema = matches[0].first.schema
300 refKey = refSchema.find(
"photometric").key
302 stargalKey = refSchema.find(
"stargal").key
307 varKey = refSchema.find(
"var").key
311 self.log.warn(
"No 'photometric' flag key found in reference schema.")
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]
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)
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)
331 matches = afterRefCut
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)
340 afterMagCutInd = [i
for i, m
in enumerate(matches)
if (m.first.get(fluxKey) > fluxLimit
341 and m.second.getPsfFlux() > 0.0)]
343 afterMagCutInd = [i
for i, m
in enumerate(matches)
if m.second.getPsfFlux() > 0.0]
345 afterMagCut = [matches[i]
for i
in afterMagCutInd]
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)
355 matches = afterMagCut
357 self.log.logdebug(
"Number of matches after magnitude limit cuts: %d" % (len(matches)))
359 if len(matches) == 0:
360 raise RuntimeError(
"No sources remaining in match list after magnitude limit cuts.")
362 if frame
is not None:
363 with ds9.Buffering():
365 x, y = m.second.getCentroid()
366 ds9.dot(
"o", x, y, size=4, frame=frame, ctype=ds9.GREEN)
377 """!Extract magnitude and magnitude error arrays from the given matches.
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()
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)
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)
395 raise RuntimeError(
"No reference stars are available")
397 refSchema = matches[0].first.schema
398 if self.config.applyColorTerms:
399 ct = Colorterm.getColorterm(filterName)
404 fluxNames = [ct.primary, ct.secondary]
406 for flux
in fluxNames:
408 refSchema.find(flux).key
410 missingFluxes.append(flux)
413 self.log.warn(
"Source catalog does not have fluxes for %s; ignoring color terms" %
414 " ".join(missingFluxes))
422 for flux
in fluxNames:
423 fluxKey = refSchema.find(flux).key
424 refFlux = np.array([m.first.get(fluxKey)
for m
in matches])
426 fluxErrKey = refSchema.find(flux +
".err").key
427 refFluxErr = np.array([m.first.get(fluxErrKey)
for m
in matches])
430 self.log.warn(
"Reference catalog does not have flux uncertainties for %s; using sqrt(flux)."
432 refFluxErr = np.sqrt(refFlux)
434 refFluxes.append(refFlux)
435 refFluxErrors.append(refFluxErr)
437 srcMag = -2.5*np.log10(srcFlux)
439 refMag = -2.5*np.log10(refFluxes[0])
440 refMag2 = -2.5*np.log10(refFluxes[1])
442 refMag = ct.transformMags(filterName, refMag, refMag2)
443 refFluxErr = ct.propagateFluxErrors(filterName, refFluxErrors[0], refFluxErrors[1])
445 refMag = -2.5*np.log10(refFluxes[0])
449 fluxErr = np.hypot(srcFluxErr, refFluxErr)
450 magErr = fluxErr / (srcFlux * np.log(10))
452 srcMagErr = srcFluxErr / (srcFlux * np.log(10))
453 refMagErr = refFluxErr / (refFlux * np.log(10))
455 return pipeBase.Struct(
459 srcMagErr = srcMagErr,
460 refMagErr = refMagErr
464 def run(self, exposure, matches):
465 """!Do photometric calibration - select matches to use and (possibly iteratively) compute
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.).
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.
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).
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
489 The measured sources:
490 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration
492 \throws RuntimeError with the following strings:
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
510 global scatterPlot, fig
514 displaySources = display
and lsstDebug.Info(__name__).displaySources
518 from matplotlib
import pyplot
522 fig = pyplot.figure()
526 ds9.mtv(exposure, frame=frame, title=
"photocal")
530 keys = self.
getKeys(matches[0].second.schema)
532 arrays = self.
extractMagArrays(matches, exposure.getFilter().getName(), keys)
538 matches[0].second.getSchema().find(self.
outputField)
540 raise RuntimeError(
"sources' schema does not contain the used-in-calibration flag \"%s\"" %
541 self.config.outputField)
550 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr, zp0=zp)
552 self.log.info(
"Magnitude zero point: %f +/- %f from %d stars" % (r.zp, r.sigma, r.ngood))
554 flux0 = 10**(0.4*r.zp)
555 flux0err = 0.4*math.log(10)*flux0*r.sigma
557 calib.setFluxMag0(flux0, flux0err)
559 return pipeBase.Struct(
569 """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
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
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.)
581 sigmaMax = self.config.sigmaMax
588 if srcErr
is not None:
591 dmagErr = np.ones(len(dmag))
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]
598 IQ_TO_STDEV = 0.741301109252802;
602 for i
in range(self.config.nIter):
613 hist, edges = np.histogram(dmag, nhist, new=
True)
615 hist, edges = np.histogram(dmag, nhist)
616 imode = np.arange(nhist)[np.where(hist == hist.max())]
618 if imode[-1] - imode[0] + 1 == len(imode):
622 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
624 peak =
sum(hist[imode])/len(imode)
628 while j >= 0
and hist[j] > 0.5*peak:
631 q1 = dmag[
sum(hist[range(j)])]
634 while j < nhist
and hist[j] > 0.5*peak:
636 j =
min(j, nhist - 1)
637 j =
min(
sum(hist[range(j)]), npt - 1)
641 q1 = dmag[int(0.25*npt)]
642 q3 = dmag[int(0.75*npt)]
649 self.log.logdebug(
"Photo calibration histogram: center = %.2f, sig = %.2f"
654 sigmaMax = dmag[-1] - dmag[0]
656 center = np.median(dmag)
657 q1 = dmag[int(0.25*npt)]
658 q3 = dmag[int(0.75*npt)]
663 if self.config.useMedian:
664 center = np.median(gdmag)
666 gdmagErr = dmagErr[good]
667 center = np.average(gdmag, weights=gdmagErr)
669 q3 = gdmag[
min(int(0.75*npt + 0.5), npt - 1)]
670 q1 = gdmag[
min(int(0.25*npt + 0.5), npt - 1)]
672 sig = IQ_TO_STDEV*(q3 - q1)
674 good = abs(dmag - center) < self.config.nSigma*
min(sig, sigmaMax)
679 from matplotlib
import pyplot
683 axes = fig.add_axes((0.1, 0.1, 0.85, 0.80));
685 axes.plot(ref[good], dmag[good] - center,
"b+")
686 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
687 linestyle=
'', color=
'b')
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')
695 axes.plot((-100, 100), (0, 0), "g-")
697 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
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")
707 while i == 0
or reply !=
"c":
709 reply = raw_input(
"Next iteration? [ynhpc] ")
714 print >> sys.stderr,
"Options: c[ontinue] h[elp] n[o] p[db] y[es]"
717 if reply
in (
"",
"c",
"n",
"p",
"y"):
720 print >> sys.stderr,
"Unrecognised response: %s" % reply
725 import pdb; pdb.set_trace()
727 print >> sys.stderr,
"Error plotting in PhotoCal.getZeroPoint: %s" % e
734 msg =
"PhotoCal.getZeroPoint: no good stars remain"
737 center = np.average(dmag, weights=dmagErr)
738 msg +=
" on first iteration; using average of all calibration stars"
740 self.log.log(self.log.WARN, msg)
742 return pipeBase.Struct(
747 elif ngood == old_ngood:
753 dmagErr = dmagErr[good]
756 dmagErr = dmagErr[good]
757 return pipeBase.Struct(
758 zp = np.average(dmag, weights=dmagErr),
759 sigma = np.std(dmag, ddof=1),
def __init__
Create the photometric calibration task.
def init
Create the photometric calibration task.
def run
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
def getKeys
Return a struct containing the source catalog keys for fields used by PhotoCalTask.
std::vector< ReferenceMatch > ReferenceMatchVector
def extractMagArrays
Extract magnitude and magnitude error arrays from the given matches.
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 getZeroPoint
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...