207 """Extract magnitude and magnitude error arrays from the given matches.
211 matches : `lsst.afw.table.ReferenceMatchVector`
212 Reference/source matches.
214 Label of filter being calibrated.
215 sourceKeys : `lsst.pipe.base.Struct`
216 Struct of source catalog keys, as returned by getSourceKeys().
220 result : `lsst.pipe.base.Struct`
221 Results as a struct with attributes:
224 Source magnitude (`np.array`).
226 Reference magnitude (`np.array`).
228 Source magnitude error (`np.array`).
230 Reference magnitude error (`np.array`).
232 An error in the magnitude; the error in ``srcMag`` - ``refMag``.
233 If nonzero, ``config.magErrFloor`` will be added to ``magErr`` only
234 (not ``srcMagErr`` or ``refMagErr``), as
235 ``magErr`` is what is later used to determine the zero point (`np.array`).
237 A list of field names of the reference catalog used for fluxes (1 or 2 strings) (`list`).
239 srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux)
for m
in matches])
240 srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr)
for m
in matches])
241 if not np.all(np.isfinite(srcInstFluxErrArr)):
243 self.log.warning(
"Source catalog does not have flux uncertainties; using sqrt(flux).")
244 srcInstFluxErrArr = np.sqrt(srcInstFluxArr)
247 referenceFlux = (0*u.ABmag).to_value(u.nJy)
248 srcInstFluxArr = srcInstFluxArr * referenceFlux
249 srcInstFluxErrArr = srcInstFluxErrArr * referenceFlux
252 raise RuntimeError(
"No reference stars are available")
253 refSchema = matches[0].first.schema
255 if self.config.applyColorTerms:
256 self.log.info(
"Applying color terms for filter=%r, config.photoCatName=%s",
257 filterLabel.physicalLabel, self.config.photoCatName)
258 colorterm = self.config.colorterms.getColorterm(filterLabel.physicalLabel,
259 self.config.photoCatName,
264 refCat.reserve(len(matches))
266 record = refCat.addNew()
267 record.assign(x.first)
269 refMagArr, refMagErrArr = colorterm.getCorrectedMagnitudes(refCat)
270 fluxFieldList = [getRefFluxField(refSchema, filt)
for filt
in (colorterm.primary,
271 colorterm.secondary)]
273 self.log.info(
"Not applying color terms.")
276 fluxFieldList = [getRefFluxField(refSchema, filterLabel.bandLabel)]
277 fluxField = getRefFluxField(refSchema, filterLabel.bandLabel)
278 fluxKey = refSchema.find(fluxField).key
279 refFluxArr = np.array([m.first.get(fluxKey)
for m
in matches])
282 fluxErrKey = refSchema.find(fluxField +
"Err").key
283 refFluxErrArr = np.array([m.first.get(fluxErrKey)
for m
in matches])
286 self.log.warning(
"Reference catalog does not have flux uncertainties for %s;"
287 " using sqrt(flux).", fluxField)
288 refFluxErrArr = np.sqrt(refFluxArr)
290 refMagArr = u.Quantity(refFluxArr, u.nJy).to_value(u.ABmag)
292 refMagErrArr = abMagErrFromFluxErr(refFluxErrArr*1e-9, refFluxArr*1e-9)
295 srcMagArr = u.Quantity(srcInstFluxArr, u.nJy).to_value(u.ABmag)
299 magErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
300 if self.config.magErrFloor != 0.0:
301 magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5
303 srcMagErrArr = abMagErrFromFluxErr(srcInstFluxErrArr*1e-9, srcInstFluxArr*1e-9)
305 good = np.isfinite(srcMagArr) & np.isfinite(refMagArr)
307 return pipeBase.Struct(
308 srcMag=srcMagArr[good],
309 refMag=refMagArr[good],
310 magErr=magErrArr[good],
311 srcMagErr=srcMagErrArr[good],
312 refMagErr=refMagErrArr[good],
313 refFluxFieldList=fluxFieldList,
317 def run(self, exposure, sourceCat, expId=0):
318 """Do photometric calibration - select matches to use and (possibly iteratively) compute
323 exposure : `lsst.afw.image.Exposure`
324 Exposure upon which the sources in the matches were detected.
325 sourceCat : `lsst.afw.image.SourceCatalog`
326 A catalog of sources to use in the calibration
327 (i.e. a `list` of `lsst.afw.table.Match` with
328 first being of type `lsst.afw.table.SimpleRecord` and second type `lsst.afw.table.SourceRecord`
329 the reference object and matched object respectively).
330 Will not be modified except to set the outputField if requested.
331 expId : `int`, optional
336 result : `lsst.pipe.base.Struct`
337 Results as a struct with attributes:
340 Object containing the zero point (`lsst.afw.image.Calib`).
342 Magnitude arrays returned be `PhotoCalTask.extractMagArrays`.
344 ReferenceMatchVector, as returned by the matcher
345 ``matchMeta`` : metadata needed to unpersist matches, as returned
346 by the matcher (`lsst.daf.base.PropertyList`)
348 Photometric zero point (mag, `float`).
350 Standard deviation of fit of photometric zero point (mag, `float`).
352 Number of sources used to fit photometric zero point (`int`).
357 Raised if any of the following occur:
358 - No matches to use for photocal.
359 - No matches are available (perhaps no sources/references were selected by the matcher).
360 - No reference stars are available.
361 - No matches are available from which to extract magnitudes.
365 The exposure is only used to provide the name of the filter being calibrated (it may also be
366 used to generate debugging plots).
368 The reference objects:
369 - Must include a field ``photometric``; True for objects which should be considered as
370 photometric standards.
371 - Must include a field ``flux``; the flux used to impose a magnitude limit and also to calibrate
372 the data to (unless a color term is specified, in which case ColorTerm.primary is used;
373 See https://jira.lsstcorp.org/browse/DM-933).
374 - May include a field ``stargal``; if present, True means that the object is a star.
375 - May include a field ``var``; if present, True means that the object is variable.
377 The measured sources:
378 - Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration.
383 displaySources = display
and lsstDebug.Info(__name__).displaySources
387 from matplotlib
import pyplot
391 self.
fig = pyplot.figure()
393 filterLabel = exposure.getFilter()
396 matchResults = self.
match.run(sourceCat, filterLabel.bandLabel)
397 matches = matchResults.matches
399 reserveResults = self.reserve.run([mm.second
for mm
in matches], expId=expId)
402 if reserveResults.reserved.sum() > 0:
403 matches = [mm
for mm, use
in zip(matches, reserveResults.use)
if use]
404 if len(matches) == 0:
405 raise RuntimeError(
"No matches to use for photocal")
408 mm.second.set(self.
usedKey,
True)
415 r = self.
getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr)
416 self.log.info(
"Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood)
419 flux0 = 10**(0.4*r.zp)
420 flux0err = 0.4*math.log(10)*flux0*r.sigma
421 photoCalib = makePhotoCalibFromCalibZeroPoint(flux0, flux0err)
422 self.log.info(
"Photometric calibration factor (nJy/ADU): %f +/- %f",
423 photoCalib.getCalibrationMean(),
424 photoCalib.getCalibrationErr())
426 return pipeBase.Struct(
427 photoCalib=photoCalib,
430 matchMeta=matchResults.matchMeta,
437 """Display sources we'll use for photocal.
439 Sources that will be actually used will be green.
440 Sources reserved from the fit will be red.
444 exposure : `lsst.afw.image.ExposureF`
446 matches : `list` of `lsst.afw.table.RefMatch`
447 Matches used for photocal.
448 reserved : `numpy.ndarray` of type `bool`
449 Boolean array indicating sources that are reserved.
450 frame : `int`, optional
451 Frame number for display.
453 disp = afwDisplay.getDisplay(frame=frame)
454 disp.mtv(exposure, title=
"photocal")
455 with disp.Buffering():
456 for mm, rr
in zip(matches, reserved):
457 x, y = mm.second.getCentroid()
458 ctype = afwDisplay.RED
if rr
else afwDisplay.GREEN
459 disp.dot(
"o", x, y, size=4, ctype=ctype)
462 """Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars).
466 result : `lsst.pipe.base.Struct`
467 Results as a struct with attributes:
470 Photometric zero point (mag, `float`).
472 Standard deviation of fit of photometric zero point (mag, `float`).
474 Number of sources used to fit photometric zero point (`int`).
478 We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists:
479 - We use the median/interquartile range to estimate the position to clip around, and the
481 - We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently
482 large estimate will prevent the clipping from ever taking effect.
483 - Rather than start with the median we start with a crude mode. This means that a set of magnitude
484 residuals with a tight core and asymmetrical outliers will start in the core. We use the width of
485 this core to set our maximum sigma (see second bullet).
487 sigmaMax = self.config.sigmaMax
491 indArr = np.argsort(dmag)
494 if srcErr
is not None:
495 dmagErr = srcErr[indArr]
497 dmagErr = np.ones(len(dmag))
500 ind_noNan = np.array([i
for i
in range(len(dmag))
501 if (
not np.isnan(dmag[i])
and not np.isnan(dmagErr[i]))])
502 dmag = dmag[ind_noNan]
503 dmagErr = dmagErr[ind_noNan]
505 IQ_TO_STDEV = 0.741301109252802
510 for i
in range(self.config.nIter):
521 hist, edges = np.histogram(dmag, nhist, new=
True)
523 hist, edges = np.histogram(dmag, nhist)
524 imode = np.arange(nhist)[np.where(hist == hist.max())]
526 if imode[-1] - imode[0] + 1 == len(imode):
530 center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1])
532 peak = sum(hist[imode])/len(imode)
536 while j >= 0
and hist[j] > 0.5*peak:
539 q1 = dmag[sum(hist[range(j)])]
542 while j < nhist
and hist[j] > 0.5*peak:
544 j =
min(j, nhist - 1)
545 j =
min(sum(hist[range(j)]), npt - 1)
549 q1 = dmag[int(0.25*npt)]
550 q3 = dmag[int(0.75*npt)]
557 self.log.debug(
"Photo calibration histogram: center = %.2f, sig = %.2f", center, sig)
561 sigmaMax = dmag[-1] - dmag[0]
563 center = np.median(dmag)
564 q1 = dmag[int(0.25*npt)]
565 q3 = dmag[int(0.75*npt)]
570 if self.config.useMedian:
571 center = np.median(gdmag)
573 gdmagErr = dmagErr[good]
574 center = np.average(gdmag, weights=gdmagErr)
576 q3 = gdmag[
min(int(0.75*npt + 0.5), npt - 1)]
577 q1 = gdmag[
min(int(0.25*npt + 0.5), npt - 1)]
579 sig = IQ_TO_STDEV*(q3 - q1)
581 good = abs(dmag - center) < self.config.nSigma*
min(sig, sigmaMax)
588 axes = self.
fig.add_axes((0.1, 0.1, 0.85, 0.80))
590 axes.plot(ref[good], dmag[good] - center,
"b+")
591 axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good],
592 linestyle=
'', color=
'b')
594 bad = np.logical_not(good)
595 if len(ref[bad]) > 0:
596 axes.plot(ref[bad], dmag[bad] - center,
"r+")
597 axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad],
598 linestyle=
'', color=
'r')
600 axes.plot((-100, 100), (0, 0),
"g-")
602 axes.plot((-100, 100), x*0.05*np.ones(2),
"g--")
604 axes.set_ylim(-1.1, 1.1)
605 axes.set_xlim(24, 13)
606 axes.set_xlabel(
"Reference")
607 axes.set_ylabel(
"Reference - Instrumental")
613 while i == 0
or reply !=
"c":
615 reply = input(
"Next iteration? [ynhpc] ")
620 print(
"Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr)
623 if reply
in (
"",
"c",
"n",
"p",
"y"):
626 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
633 except Exception
as e:
634 print(
"Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr)
641 msg =
"PhotoCal.getZeroPoint: no good stars remain"
644 center = np.average(dmag, weights=dmagErr)
645 msg +=
" on first iteration; using average of all calibration stars"
647 self.log.warning(msg)
649 return pipeBase.Struct(
653 elif ngood == old_ngood:
659 dmagErr = dmagErr[good]
662 dmagErr = dmagErr[good]
663 zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=
True)
664 sigma = np.sqrt(1.0/weightSum)
665 return pipeBase.Struct(