24 from __future__
import absolute_import, division, print_function
25 from builtins
import zip
26 from builtins
import input
27 from builtins
import range
35 import lsst.pex.config
as pexConf
36 import lsst.pipe.base
as pipeBase
37 import lsst.afw.table
as afwTable
38 from lsst.afw.image
import abMagFromFlux, abMagErrFromFluxErr, fluxFromABMag, Calib
39 from lsst.meas.astrom
import RefMatchTask, RefMatchConfig
40 import lsst.afw.display.ds9
as ds9
41 from lsst.meas.algorithms
import getRefFluxField
42 from .colorterms
import ColortermLibrary
44 __all__ = [
"PhotoCalTask",
"PhotoCalConfig"]
48 """!Return True if the given source has all good flags set and none of the bad flags set.
50 \param[in] source SourceRecord object to process.
51 \param[in] sourceKeys Struct of source catalog keys, as returned by PhotCalTask.getSourceKeys()
53 for k
in sourceKeys.goodFlags:
56 if source.getPsfFluxFlag():
58 for k
in sourceKeys.badFlags:
65 """Config for PhotoCal"""
66 magLimit = pexConf.Field(
69 doc=
"Don't use objects fainter than this magnitude",
71 reserveFraction = pexConf.Field(
73 doc=
"Fraction of candidates to reserve from fitting; none if <= 0",
76 reserveSeed = pexConf.Field(
78 doc =
"This number will be multiplied by the exposure ID "
79 "to set the random seed for reserving candidates",
82 fluxField = pexConf.Field(
84 default=
"slot_CalibFlux_flux",
85 doc=(
"Name of the source flux field to use. The associated flag field\n"
86 "('<name>_flags') will be implicitly included in badFlags."),
88 applyColorTerms = pexConf.Field(
91 doc=(
"Apply photometric color terms to reference stars? One of:\n"
92 "None: apply if colorterms and photoCatName are not None;\n"
93 " fail if color term data is not available for the specified ref catalog and filter.\n"
94 "True: always apply colorterms; fail if color term data is not available for the\n"
95 " specified reference catalog and filter.\n"
96 "False: do not apply."),
99 goodFlags = pexConf.ListField(
102 doc=
"List of source flag fields that must be set for a source to be used.",
104 badFlags = pexConf.ListField(
106 default=[
"base_PixelFlags_flag_edge",
"base_PixelFlags_flag_interpolated",
107 "base_PixelFlags_flag_saturated"],
108 doc=
"List of source flag fields that will cause a source to be rejected when they are set.",
110 sigmaMax = pexConf.Field(
113 doc=
"maximum sigma to use when clipping",
116 nSigma = pexConf.Field(
119 doc=
"clip at nSigma",
121 useMedian = pexConf.Field(
124 doc=
"use median instead of mean to compute zeropoint",
126 nIter = pexConf.Field(
129 doc=
"number of iterations",
131 colorterms = pexConf.ConfigField(
132 dtype=ColortermLibrary,
133 doc=
"Library of photometric reference catalog name: color term dict",
135 photoCatName = pexConf.Field(
138 doc=(
"Name of photometric reference catalog; used to select a color term dict in colorterms."
139 " see also applyColorTerms"),
141 magErrFloor = pexConf.RangeField(
144 doc=
"Additional magnitude uncertainty to be added in quadrature with measurement errors.",
147 doSelectUnresolved = pexConf.Field(
150 doc=(
"Use the extendedness parameter to select objects to use in photometric calibration?\n"
151 "This applies only to the sources detected on the exposure, not the reference catalog"),
155 pexConf.Config.validate(self)
157 raise RuntimeError(
"applyColorTerms=True requires photoCatName is non-None")
159 raise RuntimeError(
"applyColorTerms=True requires colorterms be provided")
171 \anchor PhotoCalTask_
173 \brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
175 \section pipe_tasks_photocal_Contents Contents
177 - \ref pipe_tasks_photocal_Purpose
178 - \ref pipe_tasks_photocal_Initialize
179 - \ref pipe_tasks_photocal_IO
180 - \ref pipe_tasks_photocal_Config
181 - \ref pipe_tasks_photocal_Debug
182 - \ref pipe_tasks_photocal_Example
184 \section pipe_tasks_photocal_Purpose Description
186 \copybrief PhotoCalTask
188 Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue.
189 The type of flux to use is specified by PhotoCalConfig.fluxField.
191 The algorithm clips outliers iteratively, with parameters set in the configuration.
193 \note This task can adds fields to the schema, so any code calling this task must ensure that
194 these columns are indeed present in the input match list; see \ref pipe_tasks_photocal_Example
196 \section pipe_tasks_photocal_Initialize Task initialisation
198 \copydoc \_\_init\_\_
200 \section pipe_tasks_photocal_IO Inputs/Outputs to the run method
204 \section pipe_tasks_photocal_Config Configuration parameters
206 See \ref PhotoCalConfig
208 \section pipe_tasks_photocal_Debug Debug variables
210 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
211 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
213 The available variables in PhotoCalTask are:
216 <DD> If True enable other debug outputs
217 <DT> \c displaySources
218 <DD> If True, display the exposure on ds9's frame 1 and overlay the source catalogue:
223 <DD> Matched objects deemed unsuitable for photometric calibration.
224 Additional information is:
225 - a cyan o for galaxies
226 - a magenta o for variables
228 <DD> Objects that failed the flux cut
230 <DD> Objects used in the photometric calibration
233 <DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude.
234 - good objects in blue
235 - rejected objects in red
236 (if \c scatterPlot is 2 or more, prompt to continue after each iteration)
239 \section pipe_tasks_photocal_Example A complete example of using PhotoCalTask
241 This code is in \link examples/photoCalTask.py\endlink, and can be run as \em e.g.
243 examples/photoCalTask.py
245 \dontinclude photoCalTask.py
247 Import the tasks (there are some other standard imports; read the file for details)
248 \skipline from lsst.pipe.tasks.astrometry
249 \skipline measPhotocal
251 We need to create both our tasks before processing any data as the task constructors
252 can add extra columns to the schema which we get from the input catalogue, \c scrCat:
256 \skip AstrometryTask.ConfigClass
258 (that \c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises,
259 so we tell it to use the \c r band)
265 If the schema has indeed changed we need to add the new columns to the source table
266 (yes; this should be easier!)
270 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same
275 We can then unpack and use the results:
280 To investigate the \ref pipe_tasks_photocal_Debug, put something like
284 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively
285 if name.endswith(".PhotoCal"):
290 lsstDebug.Info = DebugInfo
292 into your debug.py file and run photoCalTask.py with the \c --debug flag.
294 ConfigClass = PhotoCalConfig
295 _DefaultName =
"photoCal"
297 def __init__(self, refObjLoader, schema=None, **kwds):
298 """!Create the photometric calibration task. See PhotoCalTask.init for documentation
300 RefMatchTask.__init__(self, refObjLoader, schema=
None, **kwds)
303 if schema
is not None:
304 self.
usedKey = schema.addField(
"calib_photometryUsed", type=
"Flag",
305 doc=
"set if source was used in photometric calibration")
306 self.
candidateKey = schema.addField(
"calib_photometryCandidate", type=
"Flag",
307 doc=
"set if source was a candidate for use in calibration")
308 self.
reservedKey = schema.addField(
"calib_photometryReserved", type=
"Flag",
309 doc=
"set if source was reserved, so not used in calibration")
316 """!Return a struct containing the source catalog keys for fields used by PhotoCalTask.
318 Returned fields include:
321 - goodFlags: a list of keys for field names in self.config.goodFlags
322 - badFlags: a list of keys for field names in self.config.badFlags
323 - starGal: key for star/galaxy classification
325 goodFlags = [schema.find(name).key
for name
in self.config.goodFlags]
326 flux = schema.find(self.config.fluxField).key
327 fluxErr = schema.find(self.config.fluxField +
"Sigma").key
328 badFlags = [schema.find(name).key
for name
in self.config.badFlags]
330 starGal = schema.find(
"base_ClassificationExtendedness_value").key
333 return pipeBase.Struct(flux=flux, fluxErr=fluxErr, goodFlags=goodFlags, badFlags=badFlags,
337 """!Return whether the provided source is unresolved or not
339 This particular implementation is designed to work with the
340 base_ClassificationExtendedness_value=0.0 or 1.0 scheme. Because
341 of the diversity of star/galaxy classification outputs (binary
342 decision vs probabilities; signs), it's difficult to make this
343 configurable without using code. This method should therefore
344 be overridden to use the appropriate classification output.
346 \param[in] source Source to test
347 \param[in] starGalKey Struct of schema keys for source
348 \return boolean value for starGalKey (True indicates Unresolved)
350 return source.get(starGalKey) < 0.5
if starGalKey
is not None else True
354 """!Select reference/source matches according the criteria specified in the config.
356 \param[in] matches ReferenceMatchVector (not modified)
357 \param[in] sourceKeys Struct of source catalog keys, as returned by getSourceKeys()
358 \param[in] filterName name of camera filter; used to obtain the reference flux field
359 \param[in] frame ds9 frame number to use for debugging display
360 if frame is non-None, display information about trimmed objects on that ds9 frame:
362 - Unsuitable objects: blue + (and a cyan o if a galaxy)
363 - Failed flux cut: magenta *
365 \return a \link lsst.afw.table.ReferenceMatchVector\endlink that contains only the selected matches.
366 If a schema was passed during task construction, a flag field will be set on sources
367 in the selected matches.
369 \throws ValueError There are no valid matches.
371 self.log.debug(
"Number of input matches: %d", len(matches))
373 if self.config.doSelectUnresolved:
375 matches = [m
for m
in matches
if self.
isUnresolved(m.second, sourceKeys.starGal)]
376 self.log.debug(
"Number of matches after culling resolved sources: %d", len(matches))
378 if len(matches) == 0:
379 raise ValueError(
"No input matches")
386 afterFlagCutInd = [i
for i, m
in enumerate(matches)
if checkSourceFlags(m.second, sourceKeys)]
387 afterFlagCut = [matches[i]
for i
in afterFlagCutInd]
388 self.log.debug(
"Number of matches after source flag cuts: %d", len(afterFlagCut))
390 if len(afterFlagCut) != len(matches):
391 if frame
is not None:
392 with ds9.Buffering():
393 for i, m
in enumerate(matches):
394 if i
not in afterFlagCutInd:
395 x, y = m.second.getCentroid()
396 ds9.dot(
"x", x, y, size=4, frame=frame, ctype=ds9.RED)
398 matches = afterFlagCut
400 if len(matches) == 0:
401 raise ValueError(
"All matches eliminated by source flags")
403 refSchema = matches[0].first.schema
405 photometricKey = refSchema.find(
"photometric").key
407 resolvedKey = refSchema.find(
"resolved").key
412 variableKey = refSchema.find(
"variable").key
416 self.log.warn(
"No 'photometric' flag key found in reference schema.")
417 photometricKey =
None
419 if photometricKey
is not None:
420 afterRefCutInd = [i
for i, m
in enumerate(matches)
if m.first.get(photometricKey)]
421 afterRefCut = [matches[i]
for i
in afterRefCutInd]
423 if len(afterRefCut) != len(matches):
424 if frame
is not None:
425 with ds9.Buffering():
426 for i, m
in enumerate(matches):
427 if i
not in afterRefCutInd:
428 x, y = m.second.getCentroid()
429 ds9.dot(
"+", x, y, size=4, frame=frame, ctype=ds9.BLUE)
431 if resolvedKey
and m.first.get(resolvedKey):
432 ds9.dot(
"o", x, y, size=6, frame=frame, ctype=ds9.CYAN)
433 if variableKey
and m.first.get(variableKey):
434 ds9.dot(
"o", x, y, size=6, frame=frame, ctype=ds9.MAGENTA)
436 matches = afterRefCut
438 self.log.debug(
"Number of matches after reference catalog cuts: %d", len(matches))
439 if len(matches) == 0:
440 raise RuntimeError(
"No sources remain in match list after reference catalog cuts.")
441 fluxName = getRefFluxField(refSchema, filterName)
442 fluxKey = refSchema.find(fluxName).key
443 if self.config.magLimit
is not None:
444 fluxLimit = fluxFromABMag(self.config.magLimit)
446 afterMagCutInd = [i
for i, m
in enumerate(matches)
if (m.first.get(fluxKey) > fluxLimit
and
447 m.second.getPsfFlux() > 0.0)]
449 afterMagCutInd = [i
for i, m
in enumerate(matches)
if m.second.getPsfFlux() > 0.0]
451 afterMagCut = [matches[i]
for i
in afterMagCutInd]
453 if len(afterMagCut) != len(matches):
454 if frame
is not None:
455 with ds9.Buffering():
456 for i, m
in enumerate(matches):
457 if i
not in afterMagCutInd:
458 x, y = m.second.getCentroid()
459 ds9.dot(
"*", x, y, size=4, frame=frame, ctype=ds9.MAGENTA)
461 matches = afterMagCut
463 self.log.debug(
"Number of matches after magnitude limit cuts: %d", len(matches))
465 if len(matches) == 0:
466 raise RuntimeError(
"No sources remaining in match list after magnitude limit cuts.")
468 if frame
is not None:
469 with ds9.Buffering():
471 x, y = m.second.getCentroid()
472 ds9.dot(
"o", x, y, size=4, frame=frame, ctype=ds9.GREEN)
477 m.second.set(self.
usedKey,
True)
483 """!Extract magnitude and magnitude error arrays from the given matches.
485 \param[in] matches Reference/source matches, a \link lsst::afw::table::ReferenceMatchVector\endlink
486 \param[in] filterName Name of filter being calibrated
487 \param[in] sourceKeys Struct of source catalog keys, as returned by getSourceKeys()
489 \return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays
490 where magErr is an error in the magnitude; the error in srcMag - refMag
491 If nonzero, config.magErrFloor will be added to magErr *only* (not srcMagErr or refMagErr), as
492 magErr is what is later used to determine the zero point.
493 Struct also contains refFluxFieldList: a list of field names of the reference catalog used for fluxes
495 \note These magnitude arrays are the \em inputs to the photometric calibration, some may have been
496 discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813)
498 srcFluxArr = np.array([m.second.get(sourceKeys.flux)
for m
in matches])
499 srcFluxErrArr = np.array([m.second.get(sourceKeys.fluxErr)
for m
in matches])
500 if not np.all(np.isfinite(srcFluxErrArr)):
502 self.log.warn(
"Source catalog does not have flux uncertainties; using sqrt(flux).")
503 srcFluxErrArr = np.sqrt(srcFluxArr)
506 JanskysPerABFlux = 3631.0
507 srcFluxArr = srcFluxArr * JanskysPerABFlux
508 srcFluxErrArr = srcFluxErrArr * JanskysPerABFlux
511 raise RuntimeError(
"No reference stars are available")
512 refSchema = matches[0].first.schema
514 applyColorTerms = self.config.applyColorTerms
515 applyCTReason =
"config.applyColorTerms is %s" % (self.config.applyColorTerms,)
516 if self.config.applyColorTerms
is None:
518 ctDataAvail = len(self.config.colorterms.data) > 0
519 photoCatSpecified = self.config.photoCatName
is not None
520 applyCTReason +=
" and data %s available" % (
"is" if ctDataAvail
else "is not")
521 applyCTReason +=
" and photoRefCat %s None" % (
"is not" if photoCatSpecified
else "is")
522 applyColorTerms = ctDataAvail
and photoCatSpecified
525 self.log.info(
"Applying color terms for filterName=%r, config.photoCatName=%s because %s",
526 filterName, self.config.photoCatName, applyCTReason)
527 ct = self.config.colorterms.getColorterm(
528 filterName=filterName, photoCatName=self.config.photoCatName, doRaise=
True)
530 self.log.info(
"Not applying color terms because %s", applyCTReason)
534 fluxFieldList = [getRefFluxField(refSchema, filt)
for filt
in (ct.primary, ct.secondary)]
535 missingFluxFieldList = []
536 for fluxField
in fluxFieldList:
538 refSchema.find(fluxField).key
540 missingFluxFieldList.append(fluxField)
542 if missingFluxFieldList:
543 self.log.warn(
"Source catalog does not have fluxes for %s; ignoring color terms",
544 " ".join(missingFluxFieldList))
548 fluxFieldList = [getRefFluxField(refSchema, filterName)]
551 refFluxErrArrList = []
552 for fluxField
in fluxFieldList:
553 fluxKey = refSchema.find(fluxField).key
554 refFluxArr = np.array([m.first.get(fluxKey)
for m
in matches])
556 fluxErrKey = refSchema.find(fluxField +
"Sigma").key
557 refFluxErrArr = np.array([m.first.get(fluxErrKey)
for m
in matches])
560 self.log.warn(
"Reference catalog does not have flux uncertainties for %s; using sqrt(flux).",
562 refFluxErrArr = np.sqrt(refFluxArr)
564 refFluxArrList.append(refFluxArr)
565 refFluxErrArrList.append(refFluxErrArr)
568 refMagArr1 = np.array([abMagFromFlux(rf1)
for rf1
in refFluxArrList[0]])
569 refMagArr2 = np.array([abMagFromFlux(rf2)
for rf2
in refFluxArrList[1]])
571 refMagArr = ct.transformMags(refMagArr1, refMagArr2)
572 refFluxErrArr = ct.propagateFluxErrors(refFluxErrArrList[0], refFluxErrArrList[1])
574 refMagArr = np.array([abMagFromFlux(rf)
for rf
in refFluxArrList[0]])
576 srcMagArr = np.array([abMagFromFlux(sf)
for sf
in srcFluxArr])
580 magErrArr = np.array([abMagErrFromFluxErr(fe, sf)
for fe, sf
in zip(srcFluxErrArr, srcFluxArr)])
581 if self.config.magErrFloor != 0.0:
582 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
584 srcMagErrArr = np.array([abMagErrFromFluxErr(sfe, sf)
for sfe, sf
in zip(srcFluxErrArr, srcFluxArr)])
585 refMagErrArr = np.array([abMagErrFromFluxErr(rfe, rf)
for rfe, rf
in zip(refFluxErrArr, refFluxArr)])
587 return pipeBase.Struct(
591 srcMagErr=srcMagErrArr,
592 refMagErr=refMagErrArr,
593 refFluxFieldList=fluxFieldList,
597 def run(self, exposure, sourceCat, expId=0):
598 """!Do photometric calibration - select matches to use and (possibly iteratively) compute
601 \param[in] exposure Exposure upon which the sources in the matches were detected.
602 \param[in] sourceCat A catalog of sources to use in the calibration
603 (\em i.e. a list of lsst.afw.table.Match with
604 \c first being of type lsst.afw.table.SimpleRecord and \c second type lsst.afw.table.SourceRecord ---
605 the reference object and matched object respectively).
606 (will not be modified except to set the outputField if requested.).
609 - calib ------- \link lsst::afw::image::Calib\endlink object containing the zero point
610 - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays
611 - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches.
612 - zp ---------- Photometric zero point (mag)
613 - sigma ------- Standard deviation of fit of photometric zero point (mag)
614 - ngood ------- Number of sources used to fit photometric zero point
616 The exposure is only used to provide the name of the filter being calibrated (it may also be
617 used to generate debugging plots).
619 The reference objects:
620 - Must include a field \c photometric; True for objects which should be considered as
621 photometric standards
622 - Must include a field \c flux; the flux used to impose a magnitude limit and also to calibrate
623 the data to (unless a color term is specified, in which case ColorTerm.primary is used;
624 See https://jira.lsstcorp.org/browse/DM-933)
625 - May include a field \c stargal; if present, True means that the object is a star
626 - May include a field \c var; if present, True means that the object is variable
628 The measured sources:
629 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration
631 \throws RuntimeError with the following strings:
634 <DT> `sources' schema does not contain the calibration object flag "XXX"`
635 <DD> The constructor added fields to the schema that aren't in the Sources
636 <DT> No input matches
637 <DD> The input match vector is empty
638 <DT> All matches eliminated by source flags
639 <DD> The flags specified by \c badFlags in the config eliminated all candidate objects
640 <DT> No sources remain in match list after reference catalog cuts
641 <DD> The reference catalogue has a column "photometric", but no matched objects have it set
642 <DT> No sources remaining in match list after magnitude limit cuts
643 <DD> All surviving matches are either too faint in the catalogue or have negative or \c NaN flux
644 <DT> No reference stars are available
645 <DD> No matches survive all the checks
650 display = lsstDebug.Info(__name__).display
651 displaySources = display
and lsstDebug.Info(__name__).displaySources
652 self.
scatterPlot = display
and lsstDebug.Info(__name__).scatterPlot
655 from matplotlib
import pyplot
659 self.
fig = pyplot.figure()
663 ds9.mtv(exposure, frame=frame, title=
"photocal")
667 res = self.loadAndMatch(exposure, sourceCat)
671 if self.config.reserveFraction > 0:
672 random.seed(self.config.reserveSeed*expId)
673 reserveList = random.sample(res.matches,
674 int((self.config.reserveFraction)*len(res.matches)))
676 for candidate
in reserveList:
677 res.matches.remove(candidate)
680 for candidate
in reserveList:
683 matches = res.matches
688 filterName = exposure.getFilter().getName()
691 matches = self.
selectMatches(matches=matches, sourceKeys=sourceKeys, filterName=filterName,
693 arrays = self.
extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys)
699 matches[0].second.getSchema().find(self.
usedKey)
701 raise RuntimeError(
"sources' schema does not contain the calib_photometryUsed flag \"%s\"" %
710 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr, zp0=zp)
712 self.log.info(
"Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
714 flux0 = 10**(0.4*r.zp)
715 flux0err = 0.4*math.log(10)*flux0*r.sigma
717 calib.setFluxMag0(flux0, flux0err)
719 return pipeBase.Struct(
729 """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars)
731 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
732 1. We use the median/interquartile range to estimate the position to clip around, and the
734 2. We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
735 large estimate will prevent the clipping from ever taking effect.
736 3. Rather than start with the median we start with a crude mode. This means that a set of magnitude
737 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
738 this core to set our maximum sigma (see 2.)
741 - zp ---------- Photometric zero point (mag)
742 - sigma ------- Standard deviation of fit of zero point (mag)
743 - ngood ------- Number of sources used to fit zero point
745 sigmaMax = self.config.sigmaMax
749 indArr = np.argsort(dmag)
752 if srcErr
is not None:
753 dmagErr = srcErr[indArr]
755 dmagErr = np.ones(len(dmag))
758 ind_noNan = np.array([i
for i
in range(len(dmag))
759 if (
not np.isnan(dmag[i])
and not np.isnan(dmagErr[i]))])
760 dmag = dmag[ind_noNan]
761 dmagErr = dmagErr[ind_noNan]
763 IQ_TO_STDEV = 0.741301109252802
768 for i
in range(self.config.nIter):
779 hist, edges = np.histogram(dmag, nhist, new=
True)
781 hist, edges = np.histogram(dmag, nhist)
782 imode = np.arange(nhist)[np.where(hist == hist.max())]
784 if imode[-1] - imode[0] + 1 == len(imode):
788 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
790 peak = sum(hist[imode])/len(imode)
794 while j >= 0
and hist[j] > 0.5*peak:
797 q1 = dmag[sum(hist[range(j)])]
800 while j < nhist
and hist[j] > 0.5*peak:
802 j = min(j, nhist - 1)
803 j = min(sum(hist[range(j)]), npt - 1)
807 q1 = dmag[int(0.25*npt)]
808 q3 = dmag[int(0.75*npt)]
815 self.log.debug(
"Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
819 sigmaMax = dmag[-1] - dmag[0]
821 center = np.median(dmag)
822 q1 = dmag[int(0.25*npt)]
823 q3 = dmag[int(0.75*npt)]
828 if self.config.useMedian:
829 center = np.median(gdmag)
831 gdmagErr = dmagErr[good]
832 center = np.average(gdmag, weights=gdmagErr)
834 q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
835 q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
837 sig = IQ_TO_STDEV*(q3 - q1)
839 good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax)
846 axes = self.fig.add_axes((0.1, 0.1, 0.85, 0.80))
848 axes.plot(ref[good], dmag[good] - center,
"b+")
849 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
850 linestyle=
'', color=
'b')
852 bad = np.logical_not(good)
853 if len(ref[bad]) > 0:
854 axes.plot(ref[bad], dmag[bad] - center,
"r+")
855 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
856 linestyle=
'', color=
'r')
858 axes.plot((-100, 100), (0, 0), "g-")
860 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
862 axes.set_ylim(-1.1, 1.1)
863 axes.set_xlim(24, 13)
864 axes.set_xlabel(
"Reference")
865 axes.set_ylabel(
"Reference - Instrumental")
871 while i == 0
or reply !=
"c":
873 reply = input(
"Next iteration? [ynhpc] ")
878 print(
"Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
881 if reply
in (
"",
"c",
"n",
"p",
"y"):
884 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
891 except Exception
as e:
892 print(
"Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
899 msg =
"PhotoCal.getZeroPoint: no good stars remain"
902 center = np.average(dmag, weights=dmagErr)
903 msg +=
" on first iteration; using average of all calibration stars"
907 return pipeBase.Struct(
911 elif ngood == old_ngood:
917 dmagErr = dmagErr[good]
920 dmagErr = dmagErr[good]
921 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=
True)
922 sigma = np.sqrt(1.0/weightSum)
923 return pipeBase.Struct(
def checkSourceFlags
Return True if the given source has all good flags set and none of the bad flags set.
def isUnresolved
Return whether the provided source is unresolved or not.
def run
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
def extractMagArrays
Extract magnitude and magnitude error arrays from the given matches.
def __init__
Create the photometric calibration task.
def getZeroPoint
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...
def getSourceKeys
Return a struct containing the source catalog keys for fields used by PhotoCalTask.
def selectMatches
Select reference/source matches according the criteria specified in the config.