24__all__ = (
"MeasureApCorrConfig",
"MeasureApCorrTask")
27from scipy.stats
import median_abs_deviation
31from lsst.afw.math import ChebyshevBoundedField, ChebyshevBoundedFieldConfig
35from .sourceSelector
import sourceSelectorRegistry
39 """A collection of flux-related names for a given flux measurement algorithm.
44 Name of flux measurement algorithm, e.g. ``base_PsfFlux``.
46 Catalog schema containing the flux field. The ``{name}_instFlux``,
47 ``{name}_instFluxErr``, ``{name}_flag`` fields are checked for
48 existence,
and the ``apcorr_{name}_used`` field
is added.
52 KeyError
if any of instFlux, instFluxErr,
or flag fields
is missing.
57 raise KeyError(
"Could not find " + self.
fluxName)
60 raise KeyError(
"Could not find " + self.
errName)
63 raise KeyError(
"Cound not find " + self.
flagName)
65 schema.addField(self.
usedName, type=
"Flag",
66 doc=
"Set if source was used in measuring aperture correction.")
70 """Configuration for MeasureApCorrTask.
72 refFluxName = lsst.pex.config.Field(
73 doc="Field name prefix for the flux other measurements should be aperture corrected to match",
75 default=
"slot_CalibFlux",
77 sourceSelector = sourceSelectorRegistry.makeField(
78 doc=
"Selector that sets the stars that aperture corrections will be measured from.",
81 minDegreesOfFreedom = lsst.pex.config.RangeField(
82 doc=
"Minimum number of degrees of freedom (# of valid data points - # of parameters);"
83 " if this is exceeded, the order of the fit is decreased (in both dimensions), and"
84 " if we can't decrease it enough, we'll raise ValueError.",
89 fitConfig = lsst.pex.config.ConfigField(
90 doc=
"Configuration used in fitting the aperture correction fields.",
91 dtype=ChebyshevBoundedFieldConfig,
93 numIter = lsst.pex.config.Field(
94 doc=
"Number of iterations for robust MAD sigma clipping.",
98 numSigmaClip = lsst.pex.config.Field(
99 doc=
"Number of robust MAD sigma to do clipping.",
103 allowFailure = lsst.pex.config.ListField(
104 doc=
"Allow these measurement algorithms to fail without an exception.",
112 selector.doFluxLimit =
False
113 selector.doFlags =
True
114 selector.doUnresolved =
True
115 selector.doSignalToNoise =
True
116 selector.doIsolated =
False
117 selector.flags.good = []
118 selector.flags.bad = [
119 "base_PixelFlags_flag_edge",
120 "base_PixelFlags_flag_interpolatedCenter",
121 "base_PixelFlags_flag_saturatedCenter",
122 "base_PixelFlags_flag_crCenter",
123 "base_PixelFlags_flag_bad",
124 "base_PixelFlags_flag_interpolated",
125 "base_PixelFlags_flag_saturated",
127 selector.signalToNoise.minimum = 200.0
128 selector.signalToNoise.maximum =
None
129 selector.signalToNoise.fluxField =
"base_PsfFlux_instFlux"
130 selector.signalToNoise.errField =
"base_PsfFlux_instFluxErr"
133 lsst.pex.config.Config.validate(self)
135 raise lsst.pex.config.FieldValidationError(
136 MeasureApCorrConfig.sourceSelector,
138 "Star selectors that require matches are not permitted.")
142 """Task to measure aperture correction.
144 ConfigClass = MeasureApCorrConfig
145 _DefaultName = "measureApCorr"
148 """Construct a MeasureApCorrTask
150 For every name in lsst.meas.base.getApCorrNameSet():
151 - If the corresponding flux fields exist
in the schema:
152 - Add a new field apcorr_{name}_used
153 - Add an entry to the self.
toCorrect dict
154 - Otherwise silently skip the name
156 Task.__init__(self, **kwargs)
159 for name
in sorted(getApCorrNameSet()):
165 self.makeSubtask(
"sourceSelector")
167 def run(self, exposure, catalog):
168 """Measure aperture correction
173 Exposure aperture corrections are being measured on. The
174 bounding box is retrieved
from it,
and it
is passed to the
175 sourceSelector. The output aperture correction map
is *
not*
176 added to the exposure; this
is left to the caller.
178 SourceCatalog containing measurements to be used to
179 compute aperture corrections.
183 Struct : `lsst.pipe.base.Struct`
184 Contains the following:
188 that contains two entries
for each flux field:
189 - flux field (e.g. base_PsfFlux_instFlux): 2d model
190 - flux sigma field (e.g. base_PsfFlux_instFluxErr): 2d model of error
192 bbox = exposure.getBBox()
197 self.log.info(
"Measuring aperture corrections for %d flux fields", len(self.
toCorrect))
201 selected = self.sourceSelector.
run(catalog, exposure=exposure)
205 & (np.isfinite(selected.sourceCat[self.
refFluxNames.fluxName]))
207 goodRefCat = selected.sourceCat[use].copy()
212 for name, fluxNames
in self.
toCorrect.items():
215 fluxes = goodRefCat[fluxNames.fluxName]
216 with np.errstate(invalid=
"ignore"):
218 (~goodRefCat[fluxNames.flagName])
219 & (np.isfinite(fluxes))
225 if (isGood.sum() - 1) < self.config.minDegreesOfFreedom:
226 if name
in self.config.allowFailure:
227 self.log.warning(
"Unable to measure aperture correction for '%s': "
228 "only %d sources, but require at least %d." %
229 (name, isGood.sum(), self.config.minDegreesOfFreedom + 1))
232 raise RuntimeError(
"Unable to measure aperture correction for required algorithm '%s': "
233 "only %d sources, but require at least %d." %
234 (name, isGood.sum(), self.config.minDegreesOfFreedom + 1))
236 goodCat = goodRefCat[isGood].copy()
238 x = goodCat[
'slot_Centroid_x']
239 y = goodCat[
'slot_Centroid_y']
240 z = goodCat[self.
refFluxNames.fluxName]/goodCat[fluxNames.fluxName]
245 fitValues = np.median(z)
247 ctrl = self.config.fitConfig.makeControl()
250 for iteration
in range(self.config.numIter):
251 resid = z - fitValues
255 apCorrErr = median_abs_deviation(resid, scale=
"normal") + 1e-7
256 keep = np.abs(resid) <= self.config.numSigmaClip * apCorrErr
258 self.log.debug(
"Removing %d sources as outliers.", len(resid) - keep.sum())
265 while (len(x) - ctrl.computeSize()) < self.config.minDegreesOfFreedom:
278 if name
in self.config.allowFailure:
279 self.log.warning(
"Unable to measure aperture correction for '%s': "
280 "only %d sources remain, but require at least %d." %
281 (name, keep.sum(), self.config.minDegreesOfFreedom + 1))
284 raise RuntimeError(
"Unable to measure aperture correction for required algorithm "
285 "'%s': only %d sources remain, but require at least %d." %
286 (name, keep.sum(), self.config.minDegreesOfFreedom + 1))
288 apCorrField = ChebyshevBoundedField.fit(bbox, x, y, z, ctrl)
289 fitValues = apCorrField.evaluate(x, y)
295 "Aperture correction for %s from %d stars: MAD %f, RMS %f",
297 median_abs_deviation(fitValues - z, scale=
"normal"),
298 np.mean((fitValues - z)**2.)**0.5,
303 plotApCorr(bbox, x, y, z, apCorrField,
"%s, final" % (name,), doPause)
306 used = np.zeros(len(catalog), dtype=bool)
307 used[np.searchsorted(catalog[
'id'], ids)] =
True
308 catalog[fluxNames.usedName] = used
314 apCorrMap[fluxNames.fluxName] = apCorrField
317 np.array([[apCorrErr]]),
325def plotApCorr(bbox, xx, yy, zzMeasure, field, title, doPause):
326 """Plot aperture correction fit residuals
328 There are two subplots: residuals against x and y.
330 Intended
for debugging.
335 Bounding box (
for bounds)
336 xx, yy : `numpy.ndarray`, (N)
339 Measured value of the aperture correction
341 Fit aperture correction field
345 Pause to inspect the residuals plot? If
346 False, there will be a 4 second delay to
347 allow
for inspection of the plot before
348 closing it
and moving on.
350 import matplotlib.pyplot
as plt
352 zzFit = field.evaluate(xx, yy)
353 residuals = zzMeasure - zzFit
355 fig, axes = plt.subplots(2, 1)
357 axes[0].scatter(xx, residuals, s=3, marker=
'o', lw=0, alpha=0.7)
358 axes[1].scatter(yy, residuals, s=3, marker=
'o', lw=0, alpha=0.7)
360 ax.set_ylabel(
"ApCorr Fit Residual")
361 ax.set_ylim(0.9*residuals.min(), 1.1*residuals.max())
362 axes[0].set_xlabel(
"x")
363 axes[0].set_xlim(bbox.getMinX(), bbox.getMaxX())
364 axes[1].set_xlabel(
"y")
365 axes[1].set_xlim(bbox.getMinY(), bbox.getMaxY())
373 print(
"%s: plt.pause() failed. Please close plots when done." % __name__)
376 print(
"%s: Please close plots when done." % __name__)
def __init__(self, name, schema)
sourceSelectorRegistry sourceSelector
def __init__(self, schema, **kwargs)
def run(self, exposure, catalog)
def plotApCorr(bbox, xx, yy, zzMeasure, field, title, doPause)