Coverage for python / lsst / meas / base / plugins.py: 44%
368 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:53 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:53 +0000
1# This file is part of meas_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""Definition of measurement plugins.
24This module defines and registers a series of pure-Python measurement plugins
25which have trivial implementations. It also wraps measurement algorithms
26defined in C++ to expose them to the measurement framework.
27"""
29import logging
30import numpy as np
32import lsst.pex.exceptions
33import lsst.geom
34import lsst.afw.detection
35import lsst.afw.geom
37from deprecated.sphinx import deprecated
38from ._measBaseLib import (ApertureFluxControl, ApertureFluxTransform,
39 BaseTransform, BlendednessAlgorithm,
40 BlendednessControl, CircularApertureFluxAlgorithm,
41 GaussianFluxAlgorithm, GaussianFluxControl,
42 GaussianFluxTransform, LocalBackgroundAlgorithm,
43 LocalBackgroundControl, LocalBackgroundTransform,
44 MeasurementError,
45 PeakLikelihoodFluxAlgorithm,
46 PeakLikelihoodFluxControl,
47 PeakLikelihoodFluxTransform, PixelFlagsAlgorithm,
48 PixelFlagsControl, PsfFluxAlgorithm, PsfFluxControl,
49 PsfFluxTransform, ScaledApertureFluxAlgorithm,
50 ScaledApertureFluxControl,
51 ScaledApertureFluxTransform, SdssCentroidAlgorithm,
52 SdssCentroidControl, SdssCentroidTransform,
53 SdssShapeAlgorithm, SdssShapeControl,
54 SdssShapeTransform)
56from .baseMeasurement import BaseMeasurementPluginConfig
57from .forcedMeasurement import ForcedPlugin, ForcedPluginConfig
58from .pluginRegistry import register
59from .pluginsBase import BasePlugin
60from .sfm import SingleFramePlugin, SingleFramePluginConfig
61from .transforms import SimpleCentroidTransform
62from .wrappers import GenericPlugin, wrapSimpleAlgorithm, wrapTransform
64__all__ = (
65 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin",
66 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin",
67 "VarianceConfig", "SingleFrameVariancePlugin", "ForcedVariancePlugin",
68 "InputCountConfig", "SingleFrameInputCountPlugin", "ForcedInputCountPlugin",
69 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin",
70 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin",
71 "SingleFrameMomentsClassifierConfig", "SingleFrameMomentsClassifierPlugin", # TODO: Remove in DM-47494.
72 "SingleFrameClassificationSizeExtendednessConfig",
73 "SingleFrameClassificationSizeExtendednessPlugin",
74 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin",
75 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin",
76 "ForcedTransformedCentroidFromCoordConfig",
77 "ForcedTransformedCentroidFromCoordPlugin",
78 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin",
79 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig",
80 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig",
81)
84wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl,
85 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER,
86 shouldApCorr=True, hasLogName=True)
87wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl,
88 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
89wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl,
90 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER,
91 shouldApCorr=True)
92wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl,
93 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER)
94wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl,
95 executionOrder=BasePlugin.FLUX_ORDER)
96wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl,
97 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER)
98wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl,
99 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
101wrapSimpleAlgorithm(CircularApertureFluxAlgorithm, needsMetadata=True, Control=ApertureFluxControl,
102 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
103wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl,
104 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER)
106wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl,
107 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER)
109wrapTransform(PsfFluxTransform)
110wrapTransform(PeakLikelihoodFluxTransform)
111wrapTransform(GaussianFluxTransform)
112wrapTransform(SdssCentroidTransform)
113wrapTransform(SdssShapeTransform)
114wrapTransform(ScaledApertureFluxTransform)
115wrapTransform(ApertureFluxTransform)
116wrapTransform(LocalBackgroundTransform)
118log = logging.getLogger(__name__)
121class SingleFrameFPPositionConfig(SingleFramePluginConfig):
122 """Configuration for the focal plane position measurment algorithm.
123 """
126@register("base_FPPosition")
127class SingleFrameFPPositionPlugin(SingleFramePlugin):
128 """Algorithm to calculate the position of a centroid on the focal plane.
130 Parameters
131 ----------
132 config : `SingleFrameFPPositionConfig`
133 Plugin configuraion.
134 name : `str`
135 Plugin name.
136 schema : `lsst.afw.table.Schema`
137 The schema for the measurement output catalog. New fields will be
138 added to hold measurements produced by this plugin.
139 metadata : `lsst.daf.base.PropertySet`
140 Plugin metadata that will be attached to the output catalog
141 """
143 ConfigClass = SingleFrameFPPositionConfig
145 @classmethod
146 def getExecutionOrder(cls):
147 return cls.SHAPE_ORDER
149 def __init__(self, config, name, schema, metadata):
150 SingleFramePlugin.__init__(self, config, name, schema, metadata)
151 self.focalValue = lsst.afw.table.Point2DKey.addFields(schema, name, "Position on the focal plane",
152 "mm")
153 self.focalFlag = schema.addField(name + "_flag", type="Flag", doc="Set to True for any fatal failure")
154 self.detectorFlag = schema.addField(name + "_missingDetector_flag", type="Flag",
155 doc="Set to True if detector object is missing")
157 def measure(self, measRecord, exposure):
158 det = exposure.getDetector()
159 if not det:
160 measRecord.set(self.detectorFlag, True)
161 fp = lsst.geom.Point2D(np.nan, np.nan)
162 else:
163 center = measRecord.getCentroid()
164 fp = det.transform(center, lsst.afw.cameraGeom.PIXELS, lsst.afw.cameraGeom.FOCAL_PLANE)
165 measRecord.set(self.focalValue, fp)
167 def fail(self, measRecord, error=None):
168 measRecord.set(self.focalFlag, True)
171class SingleFrameJacobianConfig(SingleFramePluginConfig):
172 """Configuration for the Jacobian calculation plugin.
173 """
175 pixelScale = lsst.pex.config.Field(dtype=float, default=0.5, doc="Nominal pixel size (arcsec)")
178@register("base_Jacobian")
179class SingleFrameJacobianPlugin(SingleFramePlugin):
180 """Compute the Jacobian and its ratio with a nominal pixel area.
182 This enables one to compare relative, rather than absolute, pixel areas.
184 Parameters
185 ----------
186 config : `SingleFrameJacobianConfig`
187 Plugin configuraion.
188 name : `str`
189 Plugin name.
190 schema : `lsst.afw.table.Schema`
191 The schema for the measurement output catalog. New fields will be
192 added to hold measurements produced by this plugin.
193 metadata : `lsst.daf.base.PropertySet`
194 Plugin metadata that will be attached to the output catalog
195 """
197 ConfigClass = SingleFrameJacobianConfig
199 @classmethod
200 def getExecutionOrder(cls):
201 return cls.SHAPE_ORDER
203 def __init__(self, config, name, schema, metadata):
204 SingleFramePlugin.__init__(self, config, name, schema, metadata)
205 self.jacValue = schema.addField(name + '_value', type="D", doc="Jacobian correction")
206 self.jacFlag = schema.addField(name + '_flag', type="Flag", doc="Set to 1 for any fatal failure")
207 # Calculate one over the area of a nominal reference pixel, where area
208 # is in arcsec^2.
209 self.scale = pow(self.config.pixelScale, -2)
211 def measure(self, measRecord, exposure):
212 center = measRecord.getCentroid()
213 # Compute the area of a pixel at a source record's centroid, and take
214 # the ratio of that with the defined reference pixel area.
215 result = np.abs(self.scale*exposure.getWcs().linearizePixelToSky(
216 center,
217 lsst.geom.arcseconds).getLinear().computeDeterminant())
218 measRecord.set(self.jacValue, result)
220 def fail(self, measRecord, error=None):
221 measRecord.set(self.jacFlag, True)
224class VarianceConfig(BaseMeasurementPluginConfig):
225 """Configuration for the variance calculation plugin.
226 """
227 scale = lsst.pex.config.Field(dtype=float, default=5.0, optional=True,
228 doc="Scale factor to apply to shape for aperture")
229 mask = lsst.pex.config.ListField(doc="Mask planes to ignore", dtype=str,
230 default=["DETECTED", "DETECTED_NEGATIVE", "BAD", "SAT"])
233class VariancePlugin(GenericPlugin):
234 """Compute the median variance corresponding to a footprint.
236 The aim here is to measure the background variance, rather than that of
237 the object itself. In order to achieve this, the variance is calculated
238 over an area scaled up from the shape of the input footprint.
240 Parameters
241 ----------
242 config : `VarianceConfig`
243 Plugin configuraion.
244 name : `str`
245 Plugin name.
246 schema : `lsst.afw.table.Schema`
247 The schema for the measurement output catalog. New fields will be
248 added to hold measurements produced by this plugin.
249 metadata : `lsst.daf.base.PropertySet`
250 Plugin metadata that will be attached to the output catalog
251 """
253 ConfigClass = VarianceConfig
255 FAILURE_BAD_CENTROID = 1
256 """Denotes failures due to bad centroiding (`int`).
257 """
259 FAILURE_EMPTY_FOOTPRINT = 2
260 """Denotes failures due to a lack of usable pixels (`int`).
261 """
263 @classmethod
264 def getExecutionOrder(cls):
265 return BasePlugin.FLUX_ORDER
267 def __init__(self, config, name, schema, metadata):
268 GenericPlugin.__init__(self, config, name, schema, metadata)
269 self.varValue = schema.addField(name + '_value', type="D", doc="Variance at object position")
270 self.emptyFootprintFlag = schema.addField(name + '_flag_emptyFootprint', type="Flag",
271 doc="Set to True when the footprint has no usable pixels")
273 # Alias the badCentroid flag to that which is defined for the target
274 # of the centroid slot. We do not simply rely on the alias because
275 # that could be changed post-measurement.
276 schema.getAliasMap().set(name + '_flag_badCentroid', schema.getAliasMap().apply("slot_Centroid_flag"))
278 def measure(self, measRecord, exposure, center):
279 # Create an aperture and grow it by scale value defined in config to
280 # ensure there are enough pixels around the object to get decent
281 # statistics
282 if not np.all(np.isfinite(measRecord.getCentroid())):
283 raise MeasurementError("Bad centroid and/or shape", self.FAILURE_BAD_CENTROID)
284 aperture = lsst.afw.geom.Ellipse(measRecord.getShape(), measRecord.getCentroid())
285 aperture.scale(self.config.scale)
286 ellipse = lsst.afw.geom.SpanSet.fromShape(aperture)
287 foot = lsst.afw.detection.Footprint(ellipse)
288 foot.clipTo(exposure.getBBox(lsst.afw.image.PARENT))
289 # Filter out any pixels which have mask bits set corresponding to the
290 # planes to be excluded (defined in config.mask)
291 maskedImage = exposure.getMaskedImage()
292 pixels = lsst.afw.detection.makeHeavyFootprint(foot, maskedImage)
293 maskBits = maskedImage.getMask().getPlaneBitMask(self.config.mask)
294 logicalMask = np.logical_not(pixels.getMaskArray() & maskBits)
295 # Compute the median variance value for each pixel not excluded by the
296 # mask and write the record. Numpy median is used here instead of
297 # afw.math makeStatistics because of an issue with data types being
298 # passed into the C++ layer (DM-2379).
299 if np.any(logicalMask):
300 medVar = np.median(pixels.getVarianceArray()[logicalMask])
301 measRecord.set(self.varValue, medVar)
302 else:
303 raise MeasurementError("Footprint empty, or all pixels are masked, can't compute median",
304 self.FAILURE_EMPTY_FOOTPRINT)
306 def fail(self, measRecord, error=None):
307 # Check that we have an error object and that it is of type
308 # MeasurementError
309 if isinstance(error, MeasurementError):
310 assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_EMPTY_FOOTPRINT)
311 # FAILURE_BAD_CENTROID handled by alias to centroid record.
312 if error.getFlagBit() == self.FAILURE_EMPTY_FOOTPRINT:
313 measRecord.set(self.emptyFootprintFlag, True)
314 measRecord.set(self.varValue, np.nan)
315 GenericPlugin.fail(self, measRecord, error)
318SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance")
319"""Single-frame version of `VariancePlugin`.
320"""
322ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance")
323"""Forced version of `VariancePlugin`.
324"""
327class InputCountConfig(BaseMeasurementPluginConfig):
328 """Configuration for the input image counting plugin.
329 """
332class InputCountPlugin(GenericPlugin):
333 """Count the number of input images which contributed to a source.
335 Parameters
336 ----------
337 config : `InputCountConfig`
338 Plugin configuration.
339 name : `str`
340 Plugin name.
341 schema : `lsst.afw.table.Schema`
342 The schema for the measurement output catalog. New fields will be
343 added to hold measurements produced by this plugin.
344 metadata : `lsst.daf.base.PropertySet`
345 Plugin metadata that will be attached to the output catalog
347 Notes
348 -----
349 Information is derived from the image's `~lsst.afw.image.CoaddInputs`.
350 Note these limitation:
352 - This records the number of images which contributed to the pixel in the
353 center of the source footprint, rather than to any or all pixels in the
354 source.
355 - Clipping in the coadd is not taken into account.
356 """
358 ConfigClass = InputCountConfig
360 FAILURE_BAD_CENTROID = 1
361 """Denotes failures due to bad centroiding (`int`).
362 """
364 FAILURE_NO_INPUTS = 2
365 """Denotes failures due to the image not having coadd inputs. (`int`)
366 """
368 @classmethod
369 def getExecutionOrder(cls):
370 return BasePlugin.SHAPE_ORDER
372 def __init__(self, config, name, schema, metadata):
373 GenericPlugin.__init__(self, config, name, schema, metadata)
374 self.numberKey = schema.addField(name + '_value', type="I",
375 doc="Number of images contributing at center, not including any"
376 "clipping")
377 self.noInputsFlag = schema.addField(name + '_flag_noInputs', type="Flag",
378 doc="No coadd inputs available")
379 # Alias the badCentroid flag to that which is defined for the target of
380 # the centroid slot. We do not simply rely on the alias because that
381 # could be changed post-measurement.
382 schema.getAliasMap().set(name + '_flag_badCentroid', schema.getAliasMap().apply("slot_Centroid_flag"))
384 def measure(self, measRecord, exposure, center):
385 if not (coaddInputs := exposure.getInfo().getCoaddInputs()):
386 raise MeasurementError("No coadd inputs defined.", self.FAILURE_NO_INPUTS)
387 if not np.all(np.isfinite(center)):
388 raise MeasurementError("Source has a bad centroid.", self.FAILURE_BAD_CENTROID)
390 count = len(coaddInputs.subset_containing_ccds(center, exposure.wcs))
391 measRecord.set(self.numberKey, count)
393 def fail(self, measRecord, error=None):
394 if error is not None:
395 assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_NO_INPUTS)
396 # FAILURE_BAD_CENTROID handled by alias to centroid record.
397 if error.getFlagBit() == self.FAILURE_NO_INPUTS:
398 measRecord.set(self.noInputsFlag, True)
399 GenericPlugin.fail(self, measRecord, error)
402SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount")
403"""Single-frame version of `InputCoutPlugin`.
404"""
406ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount")
407"""Forced version of `InputCoutPlugin`.
408"""
411class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig):
412 """Configuration for the variance calculation plugin.
413 """
416class EvaluateLocalPhotoCalibPlugin(GenericPlugin):
417 """Evaluate the local value of the Photometric Calibration in the exposure.
419 The aim is to store the local calib value within the catalog for later
420 use in the Science Data Model functors.
421 """
422 ConfigClass = EvaluateLocalPhotoCalibPluginConfig
424 @classmethod
425 def getExecutionOrder(cls):
426 return BasePlugin.FLUX_ORDER
428 def __init__(self, config, name, schema, metadata):
429 GenericPlugin.__init__(self, config, name, schema, metadata)
430 self.photoKey = schema.addField(
431 name,
432 type="D",
433 doc="Local approximation of the PhotoCalib calibration factor at "
434 "the location of the src.")
435 self.photoErrKey = schema.addField(
436 "%sErr" % name,
437 type="D",
438 doc="Error on the local approximation of the PhotoCalib "
439 "calibration factor at the location of the src.")
441 def measure(self, measRecord, exposure, center):
442 photoCalib = exposure.getPhotoCalib()
443 if photoCalib is None:
444 log.debug(
445 "%s: photoCalib is None. Setting localPhotoCalib to NaN for record %d",
446 self.name,
447 measRecord.getId(),
448 )
449 calib = np.nan
450 calibErr = np.nan
451 measRecord.set(self._failKey, True)
452 else:
453 calib = photoCalib.getLocalCalibration(center)
454 calibErr = photoCalib.getCalibrationErr()
455 measRecord.set(self.photoKey, calib)
456 measRecord.set(self.photoErrKey, calibErr)
459SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin(
460 "base_LocalPhotoCalib")
461"""Single-frame version of `EvaluatePhotoCalibPlugin`.
462"""
464ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin(
465 "base_LocalPhotoCalib")
466"""Forced version of `EvaluatePhotoCalibPlugin`.
467"""
470class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig):
471 """Configuration for the variance calculation plugin.
472 """
475class EvaluateLocalWcsPlugin(GenericPlugin):
476 """Evaluate the local, linear approximation of the Wcs.
478 The aim is to store the local calib value within the catalog for later
479 use in the Science Data Model functors.
480 """
481 ConfigClass = EvaluateLocalWcsPluginConfig
482 _scale = (1.0 * lsst.geom.arcseconds).asDegrees()
484 @classmethod
485 def getExecutionOrder(cls):
486 return BasePlugin.FLUX_ORDER
488 def __init__(self, config, name, schema, metadata):
489 GenericPlugin.__init__(self, config, name, schema, metadata)
490 self.cdMatrix11Key = schema.addField(
491 f"{name}_CDMatrix_1_1",
492 type="D",
493 doc="(1, 1) element of the CDMatrix for the linear approximation "
494 "of the WCS at the src location. Gives units in radians.")
495 self.cdMatrix12Key = schema.addField(
496 f"{name}_CDMatrix_1_2",
497 type="D",
498 doc="(1, 2) element of the CDMatrix for the linear approximation "
499 "of the WCS at the src location. Gives units in radians.")
500 self.cdMatrix21Key = schema.addField(
501 f"{name}_CDMatrix_2_1",
502 type="D",
503 doc="(2, 1) element of the CDMatrix for the linear approximation "
504 "of the WCS at the src location. Gives units in radians.")
505 self.cdMatrix22Key = schema.addField(
506 f"{name}_CDMatrix_2_2",
507 type="D",
508 doc="(2, 2) element of the CDMatrix for the linear approximation "
509 "of the WCS at the src location. Gives units in radians.")
511 def measure(self, measRecord, exposure, center):
512 wcs = exposure.getWcs()
513 if wcs is None:
514 log.debug(
515 "%s: WCS is None. Setting localWcs matrix values to NaN for record %d",
516 self.name,
517 measRecord.getId(),
518 )
519 localMatrix = np.array([[np.nan, np.nan], [np.nan, np.nan]])
520 measRecord.set(self._failKey, True)
521 else:
522 localMatrix = self.makeLocalTransformMatrix(wcs, center)
523 measRecord.set(self.cdMatrix11Key, localMatrix[0, 0])
524 measRecord.set(self.cdMatrix12Key, localMatrix[0, 1])
525 measRecord.set(self.cdMatrix21Key, localMatrix[1, 0])
526 measRecord.set(self.cdMatrix22Key, localMatrix[1, 1])
528 def makeLocalTransformMatrix(self, wcs, center):
529 """Create a local, linear approximation of the wcs transformation
530 matrix.
532 The approximation is created as if the center is at RA=0, DEC=0. All
533 comparing x,y coordinate are relative to the position of center. Matrix
534 is initially calculated with units arcseconds and then converted to
535 radians. This yields higher precision results due to quirks in AST.
537 Parameters
538 ----------
539 wcs : `lsst.afw.geom.SkyWcs`
540 Wcs to approximate
541 center : `lsst.geom.Point2D`
542 Point at which to evaluate the LocalWcs.
544 Returns
545 -------
546 localMatrix : `numpy.ndarray`
547 Matrix representation the local wcs approximation with units
548 radians.
549 """
550 skyCenter = wcs.pixelToSky(center)
551 localGnomonicWcs = lsst.afw.geom.makeSkyWcs(
552 center, skyCenter, np.diag((self._scale, self._scale)))
553 measurementToLocalGnomonic = wcs.getTransform().then(
554 localGnomonicWcs.getTransform().inverted()
555 )
556 localMatrix = measurementToLocalGnomonic.getJacobian(center)
557 return np.radians(localMatrix / 3600)
560SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs")
561"""Single-frame version of `EvaluateLocalWcsPlugin`.
562"""
564ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs")
565"""Forced version of `EvaluateLocalWcsPlugin`.
566"""
569class SingleFramePeakCentroidConfig(SingleFramePluginConfig):
570 """Configuration for the single frame peak centroiding algorithm.
571 """
574@register("base_PeakCentroid")
575class SingleFramePeakCentroidPlugin(SingleFramePlugin):
576 """Record the highest peak in a source footprint as its centroid.
578 This is of course a relatively poor measure of the true centroid of the
579 object; this algorithm is provided mostly for testing and debugging.
581 Parameters
582 ----------
583 config : `SingleFramePeakCentroidConfig`
584 Plugin configuraion.
585 name : `str`
586 Plugin name.
587 schema : `lsst.afw.table.Schema`
588 The schema for the measurement output catalog. New fields will be
589 added to hold measurements produced by this plugin.
590 metadata : `lsst.daf.base.PropertySet`
591 Plugin metadata that will be attached to the output catalog
592 """
594 ConfigClass = SingleFramePeakCentroidConfig
596 @classmethod
597 def getExecutionOrder(cls):
598 return cls.CENTROID_ORDER
600 def __init__(self, config, name, schema, metadata):
601 SingleFramePlugin.__init__(self, config, name, schema, metadata)
602 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
603 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
604 self.flag = schema.addField(name + "_flag", type="Flag", doc="Centroiding failed")
606 def measure(self, measRecord, exposure):
607 peak = measRecord.getFootprint().getPeaks()[0]
608 measRecord.set(self.keyX, peak.getFx())
609 measRecord.set(self.keyY, peak.getFy())
611 def fail(self, measRecord, error=None):
612 measRecord.set(self.flag, True)
614 @staticmethod
615 def getTransformClass():
616 return SimpleCentroidTransform
619class SingleFrameSkyCoordConfig(SingleFramePluginConfig):
620 """Configuration for the sky coordinates algorithm.
621 """
624@register("base_SkyCoord")
625class SingleFrameSkyCoordPlugin(SingleFramePlugin):
626 """Record the sky position and uncertainties of an object based on its
627 centroid slot and WCS.
629 The position is recorded in the ``coord`` field, which is part of the
630 `~lsst.afw.table.SourceCatalog` minimal schema.
632 Parameters
633 ----------
634 config : `SingleFrameSkyCoordConfig`
635 Plugin configuraion.
636 name : `str`
637 Plugin name.
638 schema : `lsst.afw.table.Schema`
639 The schema for the measurement output catalog. New fields will be
640 added to hold measurements produced by this plugin.
641 metadata : `lsst.daf.base.PropertySet`
642 Plugin metadata that will be attached to the output catalog
643 """
645 ConfigClass = SingleFrameSkyCoordConfig
647 @classmethod
648 def getExecutionOrder(cls):
649 return cls.SHAPE_ORDER
651 def __init__(self, config, name, schema, metadata):
652 SingleFramePlugin.__init__(self, config, name, schema, metadata)
653 if "coord_raErr" not in schema:
654 lsst.afw.table.CoordKey.addErrorFields(schema)
656 def measure(self, measRecord, exposure):
657 # There should be a base class method for handling this exception. Put
658 # this on a later ticket. Also, there should be a python Exception of
659 # the appropriate type for this error
660 if not exposure.hasWcs():
661 raise RuntimeError("Wcs not attached to exposure. Required for " + self.name + " algorithm")
662 measRecord.updateCoord(exposure.getWcs())
664 def fail(self, measRecord, error=None):
665 # Override fail() to do nothing in the case of an exception: this is
666 # not ideal, but we don't have a place to put failures because we
667 # don't allocate any fields. Should consider fixing as part of
668 # DM-1011
669 pass
672class SingleFrameClassificationSizeExtendednessConfig(SingleFramePluginConfig):
673 """Configuration for moments-based star-galaxy classifier."""
675 exponent = lsst.pex.config.Field[float](
676 doc="Exponent to raise the PSF size squared (Ixx + Iyy) to, "
677 "in the likelihood normalization",
678 default=0.5,
679 )
682@register("base_ClassificationSizeExtendedness")
683class SingleFrameClassificationSizeExtendednessPlugin(SingleFramePlugin):
684 """Classify objects by comparing their moments-based trace radius to PSF's.
686 The plugin computes chi^2 as ((T_obj - T_psf)/T_psf^exponent)^2, where
687 T_obj is the sum of Ixx and Iyy moments of the object, and T_psf is the
688 sum of Ixx and Iyy moments of the PSF. The exponent is configurable.
689 The measure of being a galaxy is then 1 - exp(-0.5*chi^2).
691 Parameters
692 ----------
693 config : `SingleFrameClassificationSizeExtendednessConfig`
694 Plugin configuration.
695 name : `str`
696 Plugin name.
697 schema : `~lsst.afw.table.Schema`
698 The schema for the measurement output catalog. New fields will be
699 added to hold measurements produced by this plugin.
700 metadata : `~lsst.daf.base.PropertySet`
701 Plugin metadata that will be attached to the output catalog.
703 Notes
704 -----
705 The ``measure`` method of the plugin requires a value for the ``exposure``
706 argument to maintain consistent API, but it is not used in the measurement.
707 """
709 ConfigClass = SingleFrameClassificationSizeExtendednessConfig
711 FAILURE_BAD_SHAPE = 1
712 """Denotes failures due to bad shape (`int`).
713 """
715 @classmethod
716 def getExecutionOrder(cls):
717 return cls.FLUX_ORDER
719 def __init__(self, config, name, schema, metadata):
720 SingleFramePlugin.__init__(self, config, name, schema, metadata)
721 self.key = schema.addField(name + "_value",
722 type="D",
723 doc="Measure of being a galaxy based on trace of second order moments",
724 )
725 self.flag = schema.addField(name + "_flag", type="Flag", doc="Moments-based classification failed")
727 def measure(self, measRecord, exposure) -> None:
728 # Docstring inherited.
730 if measRecord.getShapeFlag():
731 raise MeasurementError(
732 "Shape flag is set. Required for " + self.name + " algorithm",
733 self.FAILURE_BAD_SHAPE,
734 )
736 shape = measRecord.getShape()
737 psf_shape = measRecord.getPsfShape()
739 ixx = shape.getIxx()
740 iyy = shape.getIyy()
741 ixx_psf = psf_shape.getIxx()
742 iyy_psf = psf_shape.getIyy()
744 object_t = ixx + iyy
745 psf_t = ixx_psf + iyy_psf
747 chi_sq = ((object_t - psf_t)/(psf_t**self.config.exponent))**2.
748 likelihood = 1. - np.exp(-0.5*chi_sq)
749 measRecord.set(self.key, likelihood)
751 def fail(self, measRecord, error=None) -> None:
752 # Docstring inherited.
753 measRecord.set(self.key, np.nan)
754 measRecord.set(self.flag, True)
757@deprecated(reason="Use SingleFrameClassificationSizeExtendednessConfig instead", version="v29.0.0",
758 category=FutureWarning)
759class SingleFrameMomentsClassifierConfig(SingleFrameClassificationSizeExtendednessConfig):
760 pass
763@deprecated(reason="Use SingleFrameClassificationSizeExtendednessPlugin instead", version="v29.0.0",
764 category=FutureWarning)
765class SingleFrameMomentsClassifierPlugin(SingleFrameClassificationSizeExtendednessPlugin):
766 ConfigClass = SingleFrameMomentsClassifierConfig
769class ForcedPeakCentroidConfig(ForcedPluginConfig):
770 """Configuration for the forced peak centroid algorithm.
771 """
774@register("base_PeakCentroid")
775class ForcedPeakCentroidPlugin(ForcedPlugin):
776 """Record the highest peak in a source footprint as its centroid.
778 This is of course a relatively poor measure of the true centroid of the
779 object; this algorithm is provided mostly for testing and debugging.
781 This is similar to `SingleFramePeakCentroidPlugin`, except that transforms
782 the peak coordinate from the original (reference) coordinate system to the
783 coordinate system of the exposure being measured.
785 Parameters
786 ----------
787 config : `ForcedPeakCentroidConfig`
788 Plugin configuraion.
789 name : `str`
790 Plugin name.
791 schemaMapper : `lsst.afw.table.SchemaMapper`
792 A mapping from reference catalog fields to output
793 catalog fields. Output fields are added to the output schema.
794 metadata : `lsst.daf.base.PropertySet`
795 Plugin metadata that will be attached to the output catalog.
796 """
798 ConfigClass = ForcedPeakCentroidConfig
800 @classmethod
801 def getExecutionOrder(cls):
802 return cls.CENTROID_ORDER
804 def __init__(self, config, name, schemaMapper, metadata):
805 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
806 schema = schemaMapper.editOutputSchema()
807 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
808 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
810 def measure(self, measRecord, exposure, refRecord, refWcs):
811 targetWcs = exposure.getWcs()
812 peak = refRecord.getFootprint().getPeaks()[0]
813 result = lsst.geom.Point2D(peak.getFx(), peak.getFy())
814 result = targetWcs.skyToPixel(refWcs.pixelToSky(result))
815 measRecord.set(self.keyX, result.getX())
816 measRecord.set(self.keyY, result.getY())
818 @staticmethod
819 def getTransformClass():
820 return SimpleCentroidTransform
823class ForcedTransformedCentroidConfig(ForcedPluginConfig):
824 """Configuration for the forced transformed centroid algorithm.
825 """
828@register("base_TransformedCentroid")
829class ForcedTransformedCentroidPlugin(ForcedPlugin):
830 """Record the transformation of the reference catalog centroid.
832 The centroid recorded in the reference catalog is tranformed to the
833 measurement coordinate system and stored.
835 Parameters
836 ----------
837 config : `ForcedTransformedCentroidConfig`
838 Plugin configuration
839 name : `str`
840 Plugin name
841 schemaMapper : `lsst.afw.table.SchemaMapper`
842 A mapping from reference catalog fields to output
843 catalog fields. Output fields are added to the output schema.
844 metadata : `lsst.daf.base.PropertySet`
845 Plugin metadata that will be attached to the output catalog.
847 Notes
848 -----
849 This is used as the slot centroid by default in forced measurement,
850 allowing subsequent measurements to simply refer to the slot value just as
851 they would in single-frame measurement.
852 """
854 ConfigClass = ForcedTransformedCentroidConfig
856 @classmethod
857 def getExecutionOrder(cls):
858 return cls.CENTROID_ORDER
860 def __init__(self, config, name, schemaMapper, metadata):
861 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
862 schema = schemaMapper.editOutputSchema()
863 # Allocate x and y fields, join these into a single FunctorKey for
864 # ease-of-use.
865 xKey = schema.addField(name + "_x", type="D", doc="transformed reference centroid column",
866 units="pixel")
867 yKey = schema.addField(name + "_y", type="D", doc="transformed reference centroid row",
868 units="pixel")
869 self.centroidKey = lsst.afw.table.Point2DKey(xKey, yKey)
870 # Because we're taking the reference position as given, we don't bother
871 # transforming its uncertainty and reporting that here, so there are no
872 # sigma or cov fields. We do propagate the flag field, if it exists.
873 if "slot_Centroid_flag" in schemaMapper.getInputSchema():
874 self.flagKey = schema.addField(name + "_flag", type="Flag",
875 doc="whether the reference centroid is marked as bad")
876 else:
877 self.flagKey = None
879 def measure(self, measRecord, exposure, refRecord, refWcs):
880 targetWcs = exposure.getWcs()
881 if not refWcs == targetWcs:
882 targetPos = targetWcs.skyToPixel(refWcs.pixelToSky(refRecord.getCentroid()))
883 measRecord.set(self.centroidKey, targetPos)
884 else:
885 measRecord.set(self.centroidKey, refRecord.getCentroid())
886 if self.flagKey is not None:
887 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
890class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig):
891 """Configuration for the forced transformed coord algorithm.
892 """
895@register("base_TransformedCentroidFromCoord")
896class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin):
897 """Record the transformation of the reference catalog coord.
899 The coord recorded in the reference catalog is tranformed to the
900 measurement coordinate system and stored.
902 Parameters
903 ----------
904 config : `ForcedTransformedCentroidFromCoordConfig`
905 Plugin configuration
906 name : `str`
907 Plugin name
908 schemaMapper : `lsst.afw.table.SchemaMapper`
909 A mapping from reference catalog fields to output
910 catalog fields. Output fields are added to the output schema.
911 metadata : `lsst.daf.base.PropertySet`
912 Plugin metadata that will be attached to the output catalog.
914 Notes
915 -----
916 This can be used as the slot centroid in forced measurement when only a
917 reference coord exist, allowing subsequent measurements to simply refer to
918 the slot value just as they would in single-frame measurement.
919 """
921 ConfigClass = ForcedTransformedCentroidFromCoordConfig
923 def measure(self, measRecord, exposure, refRecord, refWcs):
924 targetWcs = exposure.getWcs()
926 targetPos = targetWcs.skyToPixel(refRecord.getCoord())
927 measRecord.set(self.centroidKey, targetPos)
929 if self.flagKey is not None:
930 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
933class ForcedTransformedShapeConfig(ForcedPluginConfig):
934 """Configuration for the forced transformed shape algorithm.
935 """
938@register("base_TransformedShape")
939class ForcedTransformedShapePlugin(ForcedPlugin):
940 """Record the transformation of the reference catalog shape.
942 The shape recorded in the reference catalog is tranformed to the
943 measurement coordinate system and stored.
945 Parameters
946 ----------
947 config : `ForcedTransformedShapeConfig`
948 Plugin configuration
949 name : `str`
950 Plugin name
951 schemaMapper : `lsst.afw.table.SchemaMapper`
952 A mapping from reference catalog fields to output
953 catalog fields. Output fields are added to the output schema.
954 metadata : `lsst.daf.base.PropertySet`
955 Plugin metadata that will be attached to the output catalog.
957 Notes
958 -----
959 This is used as the slot shape by default in forced measurement, allowing
960 subsequent measurements to simply refer to the slot value just as they
961 would in single-frame measurement.
962 """
964 ConfigClass = ForcedTransformedShapeConfig
966 @classmethod
967 def getExecutionOrder(cls):
968 return cls.SHAPE_ORDER
970 def __init__(self, config, name, schemaMapper, metadata):
971 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
972 schema = schemaMapper.editOutputSchema()
973 # Allocate xx, yy, xy fields, join these into a single FunctorKey for
974 # ease-of-use.
975 xxKey = schema.addField(name + "_xx", type="D", doc="transformed reference shape x^2 moment",
976 units="pixel^2")
977 yyKey = schema.addField(name + "_yy", type="D", doc="transformed reference shape y^2 moment",
978 units="pixel^2")
979 xyKey = schema.addField(name + "_xy", type="D", doc="transformed reference shape xy moment",
980 units="pixel^2")
981 self.shapeKey = lsst.afw.table.QuadrupoleKey(xxKey, yyKey, xyKey)
982 # Because we're taking the reference position as given, we don't bother
983 # transforming its uncertainty and reporting that here, so there are no
984 # sigma or cov fields. We do propagate the flag field, if it exists.
985 if "slot_Shape_flag" in schemaMapper.getInputSchema():
986 self.flagKey = schema.addField(name + "_flag", type="Flag",
987 doc="whether the reference shape is marked as bad")
988 else:
989 self.flagKey = None
991 def measure(self, measRecord, exposure, refRecord, refWcs):
992 targetWcs = exposure.getWcs()
993 if not refWcs == targetWcs:
994 fullTransform = lsst.afw.geom.makeWcsPairTransform(refWcs, targetWcs)
995 localTransform = lsst.afw.geom.linearizeTransform(fullTransform, refRecord.getCentroid())
996 measRecord.set(self.shapeKey, refRecord.getShape().transform(localTransform.getLinear()))
997 else:
998 measRecord.set(self.shapeKey, refRecord.getShape())
999 if self.flagKey is not None:
1000 measRecord.set(self.flagKey, refRecord.getShapeFlag())