24__all__ = (
"MeasureApCorrConfig",
"MeasureApCorrTask",
"MeasureApCorrError")
27from scipy.stats
import median_abs_deviation
31from lsst.afw.math import ChebyshevBoundedField, ChebyshevBoundedFieldConfig
35from .sourceSelector
import sourceSelectorRegistry
43 """A collection of flux-related names for a given flux measurement algorithm.
48 Name of flux measurement algorithm, e.g. ``base_PsfFlux``.
50 Catalog schema containing the flux field. The ``{name}_instFlux``,
51 ``{name}_instFluxErr``, ``{name}_flag`` fields are checked for
52 existence,
and the ``apcorr_{name}_used`` field
is added.
56 KeyError
if any of instFlux, instFluxErr,
or flag fields
is missing.
61 raise KeyError(
"Could not find " + self.
fluxName)
64 raise KeyError(
"Could not find " + self.
errName)
67 raise KeyError(
"Cound not find " + self.
flagName)
69 schema.addField(self.
usedName, type=
"Flag",
70 doc=
"Set if source was used in measuring aperture correction.")
74 """Configuration for MeasureApCorrTask.
76 refFluxName = lsst.pex.config.Field(
77 doc="Field name prefix for the flux other measurements should be aperture corrected to match",
79 default=
"slot_CalibFlux",
81 sourceSelector = sourceSelectorRegistry.makeField(
82 doc=
"Selector that sets the stars that aperture corrections will be measured from.",
85 minDegreesOfFreedom = lsst.pex.config.RangeField(
86 doc=
"Minimum number of degrees of freedom (# of valid data points - # of parameters);"
87 " if this is exceeded, the order of the fit is decreased (in both dimensions), and"
88 " if we can't decrease it enough, we'll raise ValueError.",
93 fitConfig = lsst.pex.config.ConfigField(
94 doc=
"Configuration used in fitting the aperture correction fields.",
95 dtype=ChebyshevBoundedFieldConfig,
97 numIter = lsst.pex.config.Field(
98 doc=
"Number of iterations for robust MAD sigma clipping.",
102 numSigmaClip = lsst.pex.config.Field(
103 doc=
"Number of robust MAD sigma to do clipping.",
107 allowFailure = lsst.pex.config.ListField(
108 doc=
"Allow these measurement algorithms to fail without an exception.",
116 selector.doFluxLimit =
False
117 selector.doFlags =
True
118 selector.doUnresolved =
True
119 selector.doSignalToNoise =
True
120 selector.doIsolated =
False
121 selector.flags.good = []
122 selector.flags.bad = [
123 "base_PixelFlags_flag_edge",
124 "base_PixelFlags_flag_interpolatedCenter",
125 "base_PixelFlags_flag_saturatedCenter",
126 "base_PixelFlags_flag_crCenter",
127 "base_PixelFlags_flag_bad",
128 "base_PixelFlags_flag_interpolated",
129 "base_PixelFlags_flag_saturated",
131 selector.signalToNoise.minimum = 200.0
132 selector.signalToNoise.maximum =
None
133 selector.signalToNoise.fluxField =
"base_PsfFlux_instFlux"
134 selector.signalToNoise.errField =
"base_PsfFlux_instFluxErr"
137 lsst.pex.config.Config.validate(self)
139 raise lsst.pex.config.FieldValidationError(
140 MeasureApCorrConfig.sourceSelector,
142 "Star selectors that require matches are not permitted.")
146 """Task to measure aperture correction.
148 ConfigClass = MeasureApCorrConfig
149 _DefaultName = "measureApCorr"
152 """Construct a MeasureApCorrTask
154 For every name in lsst.meas.base.getApCorrNameSet():
155 - If the corresponding flux fields exist
in the schema:
156 - Add a new field apcorr_{name}_used
157 - Add an entry to the self.
toCorrect dict
158 - Otherwise silently skip the name
160 Task.__init__(self, **kwargs)
163 for name
in sorted(getApCorrNameSet()):
169 self.makeSubtask(
"sourceSelector")
171 def run(self, exposure, catalog):
172 """Measure aperture correction
177 Exposure aperture corrections are being measured on. The
178 bounding box is retrieved
from it,
and it
is passed to the
179 sourceSelector. The output aperture correction map
is *
not*
180 added to the exposure; this
is left to the caller.
182 SourceCatalog containing measurements to be used to
183 compute aperture corrections.
187 Struct : `lsst.pipe.base.Struct`
188 Contains the following:
192 that contains two entries
for each flux field:
193 - flux field (e.g. base_PsfFlux_instFlux): 2d model
194 - flux sigma field (e.g. base_PsfFlux_instFluxErr): 2d model of error
196 bbox = exposure.getBBox()
201 self.log.info(
"Measuring aperture corrections for %d flux fields", len(self.
toCorrect))
205 selected = self.sourceSelector.
run(catalog, exposure=exposure)
209 & (np.isfinite(selected.sourceCat[self.
refFluxNames.fluxName]))
211 goodRefCat = selected.sourceCat[use].copy()
216 for name, fluxNames
in self.
toCorrect.items():
219 fluxes = goodRefCat[fluxNames.fluxName]
220 with np.errstate(invalid=
"ignore"):
222 (~goodRefCat[fluxNames.flagName])
223 & (np.isfinite(fluxes))
229 if (isGood.sum() - 1) < self.config.minDegreesOfFreedom:
230 if name
in self.config.allowFailure:
231 self.log.warning(
"Unable to measure aperture correction for '%s': "
232 "only %d sources, but require at least %d.",
233 name, isGood.sum(), self.config.minDegreesOfFreedom + 1)
236 msg = (
"Unable to measure aperture correction for required algorithm '%s': "
237 "only %d sources, but require at least %d." %
238 (name, isGood.sum(), self.config.minDegreesOfFreedom + 1))
239 self.log.warning(msg)
242 goodCat = goodRefCat[isGood].copy()
244 x = goodCat[
'slot_Centroid_x']
245 y = goodCat[
'slot_Centroid_y']
246 z = goodCat[self.
refFluxNames.fluxName]/goodCat[fluxNames.fluxName]
251 fitValues = np.median(z)
253 ctrl = self.config.fitConfig.makeControl()
256 for iteration
in range(self.config.numIter):
257 resid = z - fitValues
261 apCorrErr = median_abs_deviation(resid, scale=
"normal") + 1e-7
262 keep = np.abs(resid) <= self.config.numSigmaClip * apCorrErr
264 self.log.debug(
"Removing %d sources as outliers.", len(resid) - keep.sum())
271 while (len(x) - ctrl.computeSize()) < self.config.minDegreesOfFreedom:
284 if name
in self.config.allowFailure:
285 self.log.warning(
"Unable to measure aperture correction for '%s': "
286 "only %d sources remain, but require at least %d." %
287 (name, keep.sum(), self.config.minDegreesOfFreedom + 1))
290 raise RuntimeError(
"Unable to measure aperture correction for required algorithm "
291 "'%s': only %d sources remain, but require at least %d." %
292 (name, keep.sum(), self.config.minDegreesOfFreedom + 1))
294 apCorrField = ChebyshevBoundedField.fit(bbox, x, y, z, ctrl)
295 fitValues = apCorrField.evaluate(x, y)
301 "Aperture correction for %s from %d stars: MAD %f, RMS %f",
303 median_abs_deviation(fitValues - z, scale=
"normal"),
304 np.mean((fitValues - z)**2.)**0.5,
309 plotApCorr(bbox, x, y, z, apCorrField,
"%s, final" % (name,), doPause)
312 used = np.zeros(len(catalog), dtype=bool)
313 used[np.searchsorted(catalog[
'id'], ids)] =
True
314 catalog[fluxNames.usedName] = used
320 apCorrMap[fluxNames.fluxName] = apCorrField
323 np.array([[apCorrErr]]),
331def plotApCorr(bbox, xx, yy, zzMeasure, field, title, doPause):
332 """Plot aperture correction fit residuals
334 There are two subplots: residuals against x and y.
336 Intended
for debugging.
341 Bounding box (
for bounds)
342 xx, yy : `numpy.ndarray`, (N)
345 Measured value of the aperture correction
347 Fit aperture correction field
351 Pause to inspect the residuals plot? If
352 False, there will be a 4 second delay to
353 allow
for inspection of the plot before
354 closing it
and moving on.
356 import matplotlib.pyplot
as plt
358 zzFit = field.evaluate(xx, yy)
359 residuals = zzMeasure - zzFit
361 fig, axes = plt.subplots(2, 1)
363 axes[0].scatter(xx, residuals, s=3, marker=
'o', lw=0, alpha=0.7)
364 axes[1].scatter(yy, residuals, s=3, marker=
'o', lw=0, alpha=0.7)
366 ax.set_ylabel(
"ApCorr Fit Residual")
367 ax.set_ylim(0.9*residuals.min(), 1.1*residuals.max())
368 axes[0].set_xlabel(
"x")
369 axes[0].set_xlim(bbox.getMinX(), bbox.getMaxX())
370 axes[1].set_xlabel(
"y")
371 axes[1].set_xlim(bbox.getMinY(), bbox.getMaxY())
379 print(
"%s: plt.pause() failed. Please close plots when done." % __name__)
382 print(
"%s: Please close plots when done." % __name__)
def __init__(self, name, schema)
def __init__(self, schema, **kwargs)
def run(self, exposure, catalog)
def plotApCorr(bbox, xx, yy, zzMeasure, field, title, doPause)