27 import astropy.units
as u
31 from lsst.afw.image import abMagFromFlux, abMagErrFromFluxErr, Calib
36 from .colorterms
import ColortermLibrary
38 __all__ = [
"PhotoCalTask",
"PhotoCalConfig"]
42 """Config for PhotoCal""" 43 match = pexConf.ConfigField(
"Match to reference catalog",
44 DirectMatchConfigWithoutLoader)
45 reserve = pexConf.ConfigurableField(target=ReserveSourcesTask, doc=
"Reserve sources from fitting")
46 fluxField = pexConf.Field(
48 default=
"slot_CalibFlux_instFlux",
49 doc=(
"Name of the source instFlux field to use. The associated flag field\n" 50 "('<name>_flags') will be implicitly included in badFlags."),
52 applyColorTerms = pexConf.Field(
55 doc=(
"Apply photometric color terms to reference stars? One of:\n" 56 "None: apply if colorterms and photoCatName are not None;\n" 57 " fail if color term data is not available for the specified ref catalog and filter.\n" 58 "True: always apply colorterms; fail if color term data is not available for the\n" 59 " specified reference catalog and filter.\n" 60 "False: do not apply."),
63 sigmaMax = pexConf.Field(
66 doc=
"maximum sigma to use when clipping",
69 nSigma = pexConf.Field(
74 useMedian = pexConf.Field(
77 doc=
"use median instead of mean to compute zeropoint",
79 nIter = pexConf.Field(
82 doc=
"number of iterations",
84 colorterms = pexConf.ConfigField(
85 dtype=ColortermLibrary,
86 doc=
"Library of photometric reference catalog name: color term dict",
88 photoCatName = pexConf.Field(
91 doc=(
"Name of photometric reference catalog; used to select a color term dict in colorterms." 92 " see also applyColorTerms"),
94 magErrFloor = pexConf.RangeField(
97 doc=
"Additional magnitude uncertainty to be added in quadrature with measurement errors.",
102 pexConf.Config.validate(self)
104 raise RuntimeError(
"applyColorTerms=True requires photoCatName is non-None")
106 raise RuntimeError(
"applyColorTerms=True requires colorterms be provided")
109 pexConf.Config.setDefaults(self)
110 self.
match.sourceSelection.doFlags =
True 111 self.
match.sourceSelection.flags.bad = [
112 "base_PixelFlags_flag_edge",
113 "base_PixelFlags_flag_interpolated",
114 "base_PixelFlags_flag_saturated",
116 self.
match.sourceSelection.doUnresolved =
True 128 @anchor PhotoCalTask_ 130 @brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector. 132 @section pipe_tasks_photocal_Contents Contents 134 - @ref pipe_tasks_photocal_Purpose 135 - @ref pipe_tasks_photocal_Initialize 136 - @ref pipe_tasks_photocal_IO 137 - @ref pipe_tasks_photocal_Config 138 - @ref pipe_tasks_photocal_Debug 139 - @ref pipe_tasks_photocal_Example 141 @section pipe_tasks_photocal_Purpose Description 143 @copybrief PhotoCalTask 145 Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue. 146 The type of flux to use is specified by PhotoCalConfig.fluxField. 148 The algorithm clips outliers iteratively, with parameters set in the configuration. 150 @note This task can adds fields to the schema, so any code calling this task must ensure that 151 these columns are indeed present in the input match list; see @ref pipe_tasks_photocal_Example 153 @section pipe_tasks_photocal_Initialize Task initialisation 155 @copydoc \_\_init\_\_ 157 @section pipe_tasks_photocal_IO Inputs/Outputs to the run method 161 @section pipe_tasks_photocal_Config Configuration parameters 163 See @ref PhotoCalConfig 165 @section pipe_tasks_photocal_Debug Debug variables 167 The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a 168 flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files. 170 The available variables in PhotoCalTask are: 173 <DD> If True enable other debug outputs 174 <DT> @c displaySources 175 <DD> If True, display the exposure on ds9's frame 1 and overlay the source catalogue. 178 <DD> Reserved objects 180 <DD> Objects used in the photometric calibration 183 <DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude. 184 - good objects in blue 185 - rejected objects in red 186 (if @c scatterPlot is 2 or more, prompt to continue after each iteration) 189 @section pipe_tasks_photocal_Example A complete example of using PhotoCalTask 191 This code is in @link examples/photoCalTask.py@endlink, and can be run as @em e.g. 193 examples/photoCalTask.py 195 @dontinclude photoCalTask.py 197 Import the tasks (there are some other standard imports; read the file for details) 198 @skipline from lsst.pipe.tasks.astrometry 199 @skipline measPhotocal 201 We need to create both our tasks before processing any data as the task constructors 202 can add extra columns to the schema which we get from the input catalogue, @c scrCat: 206 @skip AstrometryTask.ConfigClass 208 (that @c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises, 209 so we tell it to use the @c r band) 215 If the schema has indeed changed we need to add the new columns to the source table 216 (yes; this should be easier!) 220 We're now ready to process the data (we could loop over multiple exposures/catalogues using the same 225 We can then unpack and use the results: 230 To investigate the @ref pipe_tasks_photocal_Debug, put something like 234 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 235 if name.endswith(".PhotoCal"): 240 lsstDebug.Info = DebugInfo 242 into your debug.py file and run photoCalTask.py with the @c --debug flag. 244 ConfigClass = PhotoCalConfig
245 _DefaultName =
"photoCal" 247 def __init__(self, refObjLoader, schema=None, **kwds):
248 """!Create the photometric calibration task. See PhotoCalTask.init for documentation 250 pipeBase.Task.__init__(self, **kwds)
253 if schema
is not None:
254 self.
usedKey = schema.addField(
"calib_photometry_used", type=
"Flag",
255 doc=
"set if source was used in photometric calibration")
259 name=
"match", parentTask=self)
260 self.makeSubtask(
"reserve", columnName=
"calib_photometry", schema=schema,
261 doc=
"set if source was reserved from photometric calibration")
264 """Return a struct containing the source catalog keys for fields used 270 schema : `lsst.afw.table.schema` 271 Schema of the catalog to get keys from. 275 result : `lsst.pipe.base.Struct` 276 Result struct with components: 278 - ``instFlux``: Instrument flux key. 279 - ``instFluxErr``: Instrument flux error key. 281 instFlux = schema.find(self.config.fluxField).key
282 instFluxErr = schema.find(self.config.fluxField +
"Err").key
283 return pipeBase.Struct(instFlux=instFlux, instFluxErr=instFluxErr)
287 """!Extract magnitude and magnitude error arrays from the given matches. 289 @param[in] matches Reference/source matches, a @link lsst::afw::table::ReferenceMatchVector@endlink 290 @param[in] filterName Name of filter being calibrated 291 @param[in] sourceKeys Struct of source catalog keys, as returned by getSourceKeys() 293 @return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays 294 where magErr is an error in the magnitude; the error in srcMag - refMag 295 If nonzero, config.magErrFloor will be added to magErr *only* (not srcMagErr or refMagErr), as 296 magErr is what is later used to determine the zero point. 297 Struct also contains refFluxFieldList: a list of field names of the reference catalog used for fluxes 299 @note These magnitude arrays are the @em inputs to the photometric calibration, some may have been 300 discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813) 302 srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux)
for m
in matches])
303 srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr)
for m
in matches])
304 if not np.all(np.isfinite(srcInstFluxErrArr)):
306 self.log.
warn(
"Source catalog does not have flux uncertainties; using sqrt(flux).")
307 srcInstFluxErrArr = np.sqrt(srcInstFluxArr)
310 JanskysPerABFlux = 3631.0
311 srcInstFluxArr = srcInstFluxArr * JanskysPerABFlux
312 srcInstFluxErrArr = srcInstFluxErrArr * JanskysPerABFlux
315 raise RuntimeError(
"No reference stars are available")
316 refSchema = matches[0].first.schema
318 applyColorTerms = self.config.applyColorTerms
319 applyCTReason =
"config.applyColorTerms is %s" % (self.config.applyColorTerms,)
320 if self.config.applyColorTerms
is None:
322 ctDataAvail = len(self.config.colorterms.data) > 0
323 photoCatSpecified = self.config.photoCatName
is not None 324 applyCTReason +=
" and data %s available" % (
"is" if ctDataAvail
else "is not")
325 applyCTReason +=
" and photoRefCat %s provided" % (
"is" if photoCatSpecified
else "is not")
326 applyColorTerms = ctDataAvail
and photoCatSpecified
329 self.log.
info(
"Applying color terms for filterName=%r, config.photoCatName=%s because %s",
330 filterName, self.config.photoCatName, applyCTReason)
331 colorterm = self.config.colorterms.getColorterm(
332 filterName=filterName, photoCatName=self.config.photoCatName, doRaise=
True)
336 refCat.reserve(len(matches))
338 record = refCat.addNew()
339 record.assign(x.first)
341 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat, filterName)
342 fluxFieldList = [
getRefFluxField(refSchema, filt)
for filt
in (colorterm.primary,
343 colorterm.secondary)]
346 self.log.
info(
"Not applying color terms because %s", applyCTReason)
351 fluxKey = refSchema.find(fluxField).key
352 refFluxArr = np.array([m.first.get(fluxKey)
for m
in matches])
355 fluxErrKey = refSchema.find(fluxField +
"Err").key
356 refFluxErrArr = np.array([m.first.get(fluxErrKey)
for m
in matches])
359 self.log.
warn(
"Reference catalog does not have flux uncertainties for %s; using sqrt(flux).",
361 refFluxErrArr = np.sqrt(refFluxArr)
363 refMagArr = u.Quantity(refFluxArr, u.Jy).to_value(u.ABmag)
367 srcMagArr = np.array([
abMagFromFlux(sf)
for sf
in srcInstFluxArr])
371 if self.config.magErrFloor != 0.0:
372 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
376 good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
378 return pipeBase.Struct(
379 srcMag=srcMagArr[good],
380 refMag=refMagArr[good],
381 magErr=magErrArr[good],
382 srcMagErr=srcMagErrArr[good],
383 refMagErr=refMagErrArr[good],
384 refFluxFieldList=fluxFieldList,
388 def run(self, exposure, sourceCat, expId=0):
389 """!Do photometric calibration - select matches to use and (possibly iteratively) compute 392 @param[in] exposure Exposure upon which the sources in the matches were detected. 393 @param[in] sourceCat A catalog of sources to use in the calibration 394 (@em i.e. a list of lsst.afw.table.Match with 395 @c first being of type lsst.afw.table.SimpleRecord and @c second type lsst.afw.table.SourceRecord --- 396 the reference object and matched object respectively). 397 (will not be modified except to set the outputField if requested.). 400 - calib ------- @link lsst::afw::image::Calib@endlink object containing the zero point 401 - arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays 402 - matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches. 403 - zp ---------- Photometric zero point (mag) 404 - sigma ------- Standard deviation of fit of photometric zero point (mag) 405 - ngood ------- Number of sources used to fit photometric zero point 407 The exposure is only used to provide the name of the filter being calibrated (it may also be 408 used to generate debugging plots). 410 The reference objects: 411 - Must include a field @c photometric; True for objects which should be considered as 412 photometric standards 413 - Must include a field @c flux; the flux used to impose a magnitude limit and also to calibrate 414 the data to (unless a color term is specified, in which case ColorTerm.primary is used; 415 See https://jira.lsstcorp.org/browse/DM-933) 416 - May include a field @c stargal; if present, True means that the object is a star 417 - May include a field @c var; if present, True means that the object is variable 419 The measured sources: 420 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration 422 @throws RuntimeError with the following strings: 425 <DT> No matches to use for photocal 426 <DD> No matches are available (perhaps no sources/references were selected by the matcher). 427 <DT> No reference stars are available 428 <DD> No matches are available from which to extract magnitudes. 434 displaySources = display
and lsstDebug.Info(__name__).displaySources
438 from matplotlib
import pyplot
442 self.
fig = pyplot.figure()
444 filterName = exposure.getFilter().getName()
447 matchResults = self.
match.
run(sourceCat, filterName)
448 matches = matchResults.matches
449 reserveResults = self.reserve.
run([mm.second
for mm
in matches], expId=expId)
452 if reserveResults.reserved.sum() > 0:
453 matches = [mm
for mm, use
in zip(matches, reserveResults.use)
if use]
454 if len(matches) == 0:
455 raise RuntimeError(
"No matches to use for photocal")
458 mm.second.set(self.
usedKey,
True)
462 arrays = self.
extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys)
465 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
466 self.log.
info(
"Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
469 flux0 = 10**(0.4*r.zp)
470 flux0err = 0.4*math.log(10)*flux0*r.sigma
472 calib.setFluxMag0(flux0, flux0err)
474 return pipeBase.Struct(
484 """Display sources we'll use for photocal 486 Sources that will be actually used will be green. 487 Sources reserved from the fit will be red. 491 exposure : `lsst.afw.image.ExposureF` 493 matches : `list` of `lsst.afw.table.RefMatch` 494 Matches used for photocal. 495 reserved : `numpy.ndarray` of type `bool` 496 Boolean array indicating sources that are reserved. 498 Frame number for display. 500 ds9.mtv(exposure, frame=frame, title=
"photocal")
501 with ds9.Buffering():
502 for mm, rr
in zip(matches, reserved):
503 x, y = mm.second.getCentroid()
504 ctype = ds9.RED
if rr
else ds9.GREEN
505 ds9.dot(
"o", x, y, size=4, frame=frame, ctype=ctype)
508 """!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) 510 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists: 511 1. We use the median/interquartile range to estimate the position to clip around, and the 513 2. We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently 514 large estimate will prevent the clipping from ever taking effect. 515 3. Rather than start with the median we start with a crude mode. This means that a set of magnitude 516 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of 517 this core to set our maximum sigma (see 2.) 520 - zp ---------- Photometric zero point (mag) 521 - sigma ------- Standard deviation of fit of zero point (mag) 522 - ngood ------- Number of sources used to fit zero point 524 sigmaMax = self.config.sigmaMax
528 indArr = np.argsort(dmag)
531 if srcErr
is not None:
532 dmagErr = srcErr[indArr]
534 dmagErr = np.ones(len(dmag))
537 ind_noNan = np.array([i
for i
in range(len(dmag))
538 if (
not np.isnan(dmag[i])
and not np.isnan(dmagErr[i]))])
539 dmag = dmag[ind_noNan]
540 dmagErr = dmagErr[ind_noNan]
542 IQ_TO_STDEV = 0.741301109252802
547 for i
in range(self.config.nIter):
558 hist, edges = np.histogram(dmag, nhist, new=
True)
560 hist, edges = np.histogram(dmag, nhist)
561 imode = np.arange(nhist)[np.where(hist == hist.max())]
563 if imode[-1] - imode[0] + 1 == len(imode):
567 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
569 peak = sum(hist[imode])/len(imode)
573 while j >= 0
and hist[j] > 0.5*peak:
576 q1 = dmag[sum(hist[range(j)])]
579 while j < nhist
and hist[j] > 0.5*peak:
581 j =
min(j, nhist - 1)
582 j =
min(sum(hist[range(j)]), npt - 1)
586 q1 = dmag[
int(0.25*npt)]
587 q3 = dmag[
int(0.75*npt)]
594 self.log.
debug(
"Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
598 sigmaMax = dmag[-1] - dmag[0]
600 center = np.median(dmag)
601 q1 = dmag[
int(0.25*npt)]
602 q3 = dmag[
int(0.75*npt)]
607 if self.config.useMedian:
608 center = np.median(gdmag)
610 gdmagErr = dmagErr[good]
611 center = np.average(gdmag, weights=gdmagErr)
613 q3 = gdmag[
min(
int(0.75*npt + 0.5), npt - 1)]
614 q1 = gdmag[
min(
int(0.25*npt + 0.5), npt - 1)]
616 sig = IQ_TO_STDEV*(q3 - q1)
618 good =
abs(dmag - center) < self.config.nSigma*
min(sig, sigmaMax)
625 axes = self.
fig.add_axes((0.1, 0.1, 0.85, 0.80))
627 axes.plot(ref[good], dmag[good] - center,
"b+")
628 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
629 linestyle=
'', color=
'b')
631 bad = np.logical_not(good)
632 if len(ref[bad]) > 0:
633 axes.plot(ref[bad], dmag[bad] - center,
"r+")
634 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
635 linestyle=
'', color=
'r') 637 axes.plot((-100, 100), (0, 0), "g-")
639 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
641 axes.set_ylim(-1.1, 1.1)
642 axes.set_xlim(24, 13)
643 axes.set_xlabel(
"Reference")
644 axes.set_ylabel(
"Reference - Instrumental")
650 while i == 0
or reply !=
"c":
652 reply = input(
"Next iteration? [ynhpc] ")
657 print(
"Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
660 if reply
in (
"",
"c",
"n",
"p",
"y"):
663 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
670 except Exception
as e:
671 print(
"Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
678 msg =
"PhotoCal.getZeroPoint: no good stars remain" 681 center = np.average(dmag, weights=dmagErr)
682 msg +=
" on first iteration; using average of all calibration stars" 686 return pipeBase.Struct(
690 elif ngood == old_ngood:
696 dmagErr = dmagErr[good]
699 dmagErr = dmagErr[good]
700 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=
True)
701 sigma = np.sqrt(1.0/weightSum)
702 return pipeBase.Struct(
def __init__(self, refObjLoader, schema=None, kwds)
Create the photometric calibration task.
Angle abs(Angle const &a)
def run(self, exposure, sourceCat, expId=0)
Do photometric calibration - select matches to use and (possibly iteratively) compute the zero point...
def getSourceKeys(self, schema)
double abMagErrFromFluxErr(double fluxErr, double flux)
Compute AB magnitude error from flux and flux error in Janskys.
def extractMagArrays(self, matches, filterName, sourceKeys)
Extract magnitude and magnitude error arrays from the given matches.
Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector.
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
double abMagFromFlux(double flux)
Compute AB magnitude from flux in Janskys.
Describe an exposure's calibration.
def getRefFluxField(schema, filterName=None)
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
def getZeroPoint(self, src, ref, srcErr=None, zp0=None)
Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) ...
def displaySources(self, exposure, matches, reserved, frame=1)