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 msg = (
"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))
293 self.log.warning(msg)
296 apCorrField = ChebyshevBoundedField.fit(bbox, x, y, z, ctrl)
297 fitValues = apCorrField.evaluate(x, y)
303 "Aperture correction for %s from %d stars: MAD %f, RMS %f",
305 median_abs_deviation(fitValues - z, scale=
"normal"),
306 np.mean((fitValues - z)**2.)**0.5,
311 plotApCorr(bbox, x, y, z, apCorrField,
"%s, final" % (name,), doPause)
314 used = np.zeros(len(catalog), dtype=bool)
315 used[np.searchsorted(catalog[
'id'], ids)] =
True
316 catalog[fluxNames.usedName] = used
322 apCorrMap[fluxNames.fluxName] = apCorrField
325 np.array([[apCorrErr]]),
333def plotApCorr(bbox, xx, yy, zzMeasure, field, title, doPause):
334 """Plot aperture correction fit residuals
336 There are two subplots: residuals against x and y.
338 Intended
for debugging.
343 Bounding box (
for bounds)
344 xx, yy : `numpy.ndarray`, (N)
347 Measured value of the aperture correction
349 Fit aperture correction field
353 Pause to inspect the residuals plot? If
354 False, there will be a 4 second delay to
355 allow
for inspection of the plot before
356 closing it
and moving on.
358 import matplotlib.pyplot
as plt
360 zzFit = field.evaluate(xx, yy)
361 residuals = zzMeasure - zzFit
363 fig, axes = plt.subplots(2, 1)
365 axes[0].scatter(xx, residuals, s=3, marker=
'o', lw=0, alpha=0.7)
366 axes[1].scatter(yy, residuals, s=3, marker=
'o', lw=0, alpha=0.7)
368 ax.set_ylabel(
"ApCorr Fit Residual")
369 ax.set_ylim(0.9*residuals.min(), 1.1*residuals.max())
370 axes[0].set_xlabel(
"x")
371 axes[0].set_xlim(bbox.getMinX(), bbox.getMaxX())
372 axes[1].set_xlabel(
"y")
373 axes[1].set_xlim(bbox.getMinY(), bbox.getMaxY())
381 print(
"%s: plt.pause() failed. Please close plots when done." % __name__)
384 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)