218 """Extract magnitude and magnitude error arrays from the given matches.
222 matches : `lsst.afw.table.ReferenceMatchVector`
223 Reference/source matches.
225 Label of filter being calibrated.
226 sourceKeys : `lsst.pipe.base.Struct`
227 Struct of source catalog keys, as returned by getSourceKeys().
231 result : `lsst.pipe.base.Struct`
232 Results as a struct with attributes:
235 Source magnitude (`np.array`).
237 Reference magnitude (`np.array`).
239 Source magnitude error (`np.array`).
241 Reference magnitude error (`np.array`).
243 An error in the magnitude; the error in ``srcMag`` - ``refMag``.
244 If nonzero, ``config.magErrFloor`` will be added to ``magErr`` only
245 (not ``srcMagErr`` or ``refMagErr``), as
246 ``magErr`` is what is later used to determine the zero point (`np.array`).
248 A list of field names of the reference catalog used for fluxes (1 or 2 strings) (`list`).
250 srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux)
for m
in matches])
251 srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr)
for m
in matches])
252 if not np.all(np.isfinite(srcInstFluxErrArr)):
254 self.log.warning(
"Source catalog does not have flux uncertainties; using sqrt(flux).")
255 srcInstFluxErrArr = np.sqrt(srcInstFluxArr)
258 referenceFlux = (0*u.ABmag).to_value(u.nJy)
259 srcInstFluxArr = srcInstFluxArr * referenceFlux
260 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux
263 raise MatcherFailure(
"No reference stars are available")
264 refSchema = matches[0].first.schema
266 if self.config.applyColorTerms:
267 self.log.info(
"Applying color terms for filter=%r, config.photoCatName=%s",
268 filterLabel.physicalLabel, self.config.photoCatName)
269 colorterm = self.config.colorterms.getColorterm(filterLabel.physicalLabel,
270 self.config.photoCatName,
272 refCat = afwTable.SimpleCatalog(matches[0].first.schema)
275 refCat.reserve(len(matches))
277 record = refCat.addNew()
278 record.assign(x.first)
280 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat)
281 fluxFieldList = [getRefFluxField(refSchema, filt)
for filt
in (colorterm.primary,
282 colorterm.secondary)]
284 self.log.info(
"Not applying color terms.")
287 fluxFieldList = [getRefFluxField(refSchema, filterLabel.bandLabel)]
288 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel)
289 fluxKey = refSchema.find(fluxField).key
290 refFluxArr = np.array([m.first.get(fluxKey)
for m
in matches])
293 fluxErrKey = refSchema.find(fluxField +
"Err").key
294 refFluxErrArr = np.array([m.first.get(fluxErrKey)
for m
in matches])
297 self.log.warning(
"Reference catalog does not have flux uncertainties for %s;"
298 " using sqrt(flux).", fluxField)
299 refFluxErrArr = np.sqrt(refFluxArr)
301 refMagArr = u.Quantity(refFluxArr, u.nJy).to_value(u.ABmag)
303 refMagErrArr = abMagErrFromFluxErr(refFluxErrArr*1e-9, refFluxArr*1e-9)
306 srcMagArr = u.Quantity(srcInstFluxArr, u.nJy).to_value(u.ABmag)
310 magErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
311 if self.config.magErrFloor != 0.0:
312 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
314 srcMagErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
316 good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
318 raise MatcherFailure(
"No matched reference stars with valid fluxes",
319 goodSrc=int(np.sum(np.isfinite(srcMagArr))),
320 goodRef=int(np.sum(np.isfinite(refMagArr))))
322 return pipeBase.Struct(
323 srcMag=srcMagArr[good],
324 refMag=refMagArr[good],
325 magErr=magErrArr[good],
326 srcMagErr=srcMagErrArr[good],
327 refMagErr=refMagErrArr[good],
328 refFluxFieldList=fluxFieldList,
332 def run(self, exposure, sourceCat, expId=0):
333 """Do photometric calibration - select matches to use and (possibly iteratively) compute
338 exposure : `lsst.afw.image.Exposure`
339 Exposure upon which the sources in the matches were detected.
340 sourceCat : `lsst.afw.table.SourceCatalog`
341 Good stars selected for use in calibration.
342 expId : `int`, optional
347 result : `lsst.pipe.base.Struct`
348 Results as a struct with attributes:
351 Object containing the zero point (`lsst.afw.image.Calib`).
353 Magnitude arrays returned be `PhotoCalTask.extractMagArrays`.
355 ReferenceMatchVector, as returned by the matcher
356 ``matchMeta`` : metadata needed to unpersist matches, as returned
357 by the matcher (`lsst.daf.base.PropertyList`)
359 Photometric zero point (mag, `float`).
361 Standard deviation of fit of photometric zero point (mag, `float`).
363 Number of sources used to fit photometric zero point (`int`).
368 Raised if any of the following occur:
369 - No matches to use for photocal.
370 - No matches are available (perhaps no sources/references were selected by the matcher).
371 - No reference stars are available.
372 - No matches are available from which to extract magnitudes.
376 The exposure is only used to provide the name of the filter being calibrated (it may also be
377 used to generate debugging plots).
379 The reference objects:
380 - Must include a field ``photometric``; True for objects which should be considered as
381 photometric standards.
382 - Must include a field ``flux``; the flux used to impose a magnitude limit and also to calibrate
383 the data to (unless a color term is specified, in which case ColorTerm.primary is used;
384 See https://jira.lsstcorp.org/browse/DM-933).
385 - May include a field ``stargal``; if present, True means that the object is a star.
386 - May include a field ``var``; if present, True means that the object is variable.
388 The measured sources:
389 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration.
394 displaySources = display
and lsstDebug.Info(__name__).displaySources
398 from matplotlib
import pyplot
402 self.
fig = pyplot.figure()
404 filterLabel = exposure.getFilter()
407 if exposure.visitInfo
is not None:
408 epoch = exposure.visitInfo.date.toAstropy()
411 self.log.warning(
"visitInfo is None for exposure %d. Setting epoch to None.", expId)
413 matchResults = self.
match.run(sourceCat, filterLabel.bandLabel, epoch=epoch)
414 matches = matchResults.matches
416 reserveResults = self.reserve.run([mm.second
for mm
in matches], expId=expId)
419 if reserveResults.reserved.sum() > 0:
420 matches = [mm
for mm, use
in zip(matches, reserveResults.use)
if use]
421 if len(matches) == 0:
422 raise MatcherFailure(
"No matches to use for photocal.")
425 mm.second.set(self.
usedKey,
True)
432 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
433 self.log.info(
"Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
436 flux0 = 10**(0.4*r.zp)
437 flux0err = 0.4*math.log(10)*flux0*r.sigma
438 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)
439 self.log.info(
"Photometric calibration factor (nJy/ADU): %f +/- %f",
440 photoCalib.getCalibrationMean(),
441 photoCalib.getCalibrationErr())
443 return pipeBase.Struct(
444 photoCalib=photoCalib,
447 matchMeta=matchResults.matchMeta,
454 """Display sources we'll use for photocal.
456 Sources that will be actually used will be green.
457 Sources reserved from the fit will be red.
461 exposure : `lsst.afw.image.ExposureF`
463 matches : `list` of `lsst.afw.table.RefMatch`
464 Matches used for photocal.
465 reserved : `numpy.ndarray` of type `bool`
466 Boolean array indicating sources that are reserved.
467 frame : `int`, optional
468 Frame number for display.
470 disp = afwDisplay.getDisplay(frame=frame)
471 disp.mtv(exposure, title=
"photocal")
472 with disp.Buffering():
473 for mm, rr
in zip(matches, reserved):
474 x, y = mm.second.getCentroid()
475 ctype = afwDisplay.RED
if rr
else afwDisplay.GREEN
476 disp.dot(
"o", x, y, size=4, ctype=ctype)
479 """Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars).
483 result : `lsst.pipe.base.Struct`
484 Results as a struct with attributes:
487 Photometric zero point (mag, `float`).
489 Standard deviation of fit of photometric zero point (mag, `float`).
491 Number of sources used to fit photometric zero point (`int`).
495 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
496 - We use the median/interquartile range to estimate the position to clip around, and the
498 - We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
499 large estimate will prevent the clipping from ever taking effect.
500 - Rather than start with the median we start with a crude mode. This means that a set of magnitude
501 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
502 this core to set our maximum sigma (see second bullet).
504 sigmaMax = self.config.sigmaMax
508 indArr = np.argsort(dmag)
511 if srcErr
is not None:
512 dmagErr = srcErr[indArr]
514 dmagErr = np.ones(len(dmag))
517 ind_noNan = np.array([i
for i
in range(len(dmag))
518 if (
not np.isnan(dmag[i])
and not np.isnan(dmagErr[i]))])
519 dmag = dmag[ind_noNan]
520 dmagErr = dmagErr[ind_noNan]
522 IQ_TO_STDEV = 0.741301109252802
527 for i
in range(self.config.nIter):
538 hist, edges = np.histogram(dmag, nhist, new=
True)
540 hist, edges = np.histogram(dmag, nhist)
541 imode = np.arange(nhist)[np.where(hist == hist.max())]
543 if imode[-1] - imode[0] + 1 == len(imode):
547 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
549 peak = sum(hist[imode])/len(imode)
553 while j >= 0
and hist[j] > 0.5*peak:
556 q1 = dmag[sum(hist[range(j)])]
559 while j < nhist
and hist[j] > 0.5*peak:
561 j = min(j, nhist - 1)
562 j = min(sum(hist[range(j)]), npt - 1)
566 q1 = dmag[int(0.25*npt)]
567 q3 = dmag[int(0.75*npt)]
574 self.log.debug(
"Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
578 sigmaMax = dmag[-1] - dmag[0]
580 center = np.median(dmag)
581 q1 = dmag[int(0.25*npt)]
582 q3 = dmag[int(0.75*npt)]
587 if self.config.useMedian:
588 center = np.median(gdmag)
590 gdmagErr = dmagErr[good]
591 center = np.average(gdmag, weights=gdmagErr)
593 q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
594 q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
596 sig = IQ_TO_STDEV*(q3 - q1)
598 good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax)
605 axes = self.
fig.add_axes((0.1, 0.1, 0.85, 0.80))
607 axes.plot(ref[good], dmag[good] - center,
"b+")
608 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
609 linestyle=
'', color=
'b')
611 bad = np.logical_not(good)
612 if len(ref[bad]) > 0:
613 axes.plot(ref[bad], dmag[bad] - center,
"r+")
614 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
615 linestyle=
'', color=
'r')
617 axes.plot((-100, 100), (0, 0),
"g-")
619 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
621 axes.set_ylim(-1.1, 1.1)
622 axes.set_xlim(24, 13)
623 axes.set_xlabel(
"Reference")
624 axes.set_ylabel(
"Reference - Instrumental")
630 while i == 0
or reply !=
"c":
632 reply = input(
"Next iteration? [ynhpc] ")
637 print(
"Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
640 if reply
in (
"",
"c",
"n",
"p",
"y"):
643 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
650 except Exception
as e:
651 print(
"Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
658 msg =
"PhotoCal.getZeroPoint: no good stars remain"
661 center = np.average(dmag, weights=dmagErr)
662 msg +=
" on first iteration; using average of all calibration stars"
664 self.log.warning(msg)
666 return pipeBase.Struct(
670 elif ngood == old_ngood:
676 dmagErr = dmagErr[good]
679 dmagErr = dmagErr[good]
680 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=
True)
681 sigma = np.sqrt(1.0/weightSum)
682 return pipeBase.Struct(