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 matchResults = self.
match.run(sourceCat, filterLabel.bandLabel)
408 matches = matchResults.matches
410 reserveResults = self.reserve.run([mm.second
for mm
in matches], expId=expId)
413 if reserveResults.reserved.sum() > 0:
414 matches = [mm
for mm, use
in zip(matches, reserveResults.use)
if use]
415 if len(matches) == 0:
416 raise MatcherFailure(
"No matches to use for photocal.")
419 mm.second.set(self.
usedKey,
True)
426 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
427 self.log.info(
"Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
430 flux0 = 10**(0.4*r.zp)
431 flux0err = 0.4*math.log(10)*flux0*r.sigma
432 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)
433 self.log.info(
"Photometric calibration factor (nJy/ADU): %f +/- %f",
434 photoCalib.getCalibrationMean(),
435 photoCalib.getCalibrationErr())
437 return pipeBase.Struct(
438 photoCalib=photoCalib,
441 matchMeta=matchResults.matchMeta,
448 """Display sources we'll use for photocal.
450 Sources that will be actually used will be green.
451 Sources reserved from the fit will be red.
455 exposure : `lsst.afw.image.ExposureF`
457 matches : `list` of `lsst.afw.table.RefMatch`
458 Matches used for photocal.
459 reserved : `numpy.ndarray` of type `bool`
460 Boolean array indicating sources that are reserved.
461 frame : `int`, optional
462 Frame number for display.
464 disp = afwDisplay.getDisplay(frame=frame)
465 disp.mtv(exposure, title=
"photocal")
466 with disp.Buffering():
467 for mm, rr
in zip(matches, reserved):
468 x, y = mm.second.getCentroid()
469 ctype = afwDisplay.RED
if rr
else afwDisplay.GREEN
470 disp.dot(
"o", x, y, size=4, ctype=ctype)
473 """Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars).
477 result : `lsst.pipe.base.Struct`
478 Results as a struct with attributes:
481 Photometric zero point (mag, `float`).
483 Standard deviation of fit of photometric zero point (mag, `float`).
485 Number of sources used to fit photometric zero point (`int`).
489 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
490 - We use the median/interquartile range to estimate the position to clip around, and the
492 - We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
493 large estimate will prevent the clipping from ever taking effect.
494 - Rather than start with the median we start with a crude mode. This means that a set of magnitude
495 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
496 this core to set our maximum sigma (see second bullet).
498 sigmaMax = self.config.sigmaMax
502 indArr = np.argsort(dmag)
505 if srcErr
is not None:
506 dmagErr = srcErr[indArr]
508 dmagErr = np.ones(len(dmag))
511 ind_noNan = np.array([i
for i
in range(len(dmag))
512 if (
not np.isnan(dmag[i])
and not np.isnan(dmagErr[i]))])
513 dmag = dmag[ind_noNan]
514 dmagErr = dmagErr[ind_noNan]
516 IQ_TO_STDEV = 0.741301109252802
521 for i
in range(self.config.nIter):
532 hist, edges = np.histogram(dmag, nhist, new=
True)
534 hist, edges = np.histogram(dmag, nhist)
535 imode = np.arange(nhist)[np.where(hist == hist.max())]
537 if imode[-1] - imode[0] + 1 == len(imode):
541 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
543 peak = sum(hist[imode])/len(imode)
547 while j >= 0
and hist[j] > 0.5*peak:
550 q1 = dmag[sum(hist[range(j)])]
553 while j < nhist
and hist[j] > 0.5*peak:
555 j = min(j, nhist - 1)
556 j = min(sum(hist[range(j)]), npt - 1)
560 q1 = dmag[int(0.25*npt)]
561 q3 = dmag[int(0.75*npt)]
568 self.log.debug(
"Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
572 sigmaMax = dmag[-1] - dmag[0]
574 center = np.median(dmag)
575 q1 = dmag[int(0.25*npt)]
576 q3 = dmag[int(0.75*npt)]
581 if self.config.useMedian:
582 center = np.median(gdmag)
584 gdmagErr = dmagErr[good]
585 center = np.average(gdmag, weights=gdmagErr)
587 q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)]
588 q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)]
590 sig = IQ_TO_STDEV*(q3 - q1)
592 good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax)
599 axes = self.
fig.add_axes((0.1, 0.1, 0.85, 0.80))
601 axes.plot(ref[good], dmag[good] - center,
"b+")
602 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
603 linestyle=
'', color=
'b')
605 bad = np.logical_not(good)
606 if len(ref[bad]) > 0:
607 axes.plot(ref[bad], dmag[bad] - center,
"r+")
608 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
609 linestyle=
'', color=
'r')
611 axes.plot((-100, 100), (0, 0),
"g-")
613 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
615 axes.set_ylim(-1.1, 1.1)
616 axes.set_xlim(24, 13)
617 axes.set_xlabel(
"Reference")
618 axes.set_ylabel(
"Reference - Instrumental")
624 while i == 0
or reply !=
"c":
626 reply = input(
"Next iteration? [ynhpc] ")
631 print(
"Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
634 if reply
in (
"",
"c",
"n",
"p",
"y"):
637 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
644 except Exception
as e:
645 print(
"Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
652 msg =
"PhotoCal.getZeroPoint: no good stars remain"
655 center = np.average(dmag, weights=dmagErr)
656 msg +=
" on first iteration; using average of all calibration stars"
658 self.log.warning(msg)
660 return pipeBase.Struct(
664 elif ngood == old_ngood:
670 dmagErr = dmagErr[good]
673 dmagErr = dmagErr[good]
674 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=
True)
675 sigma = np.sqrt(1.0/weightSum)
676 return pipeBase.Struct(