Coverage for python/lsst/meas/base/plugins.py : 50%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 numpy as np
31import lsst.pex.exceptions
32import lsst.geom
33import lsst.afw.detection
34import lsst.afw.geom
36from .pluginRegistry import register
37from .pluginsBase import BasePlugin
38from .baseMeasurement import BaseMeasurementPluginConfig
39from .sfm import SingleFramePluginConfig, SingleFramePlugin
40from .forcedMeasurement import ForcedPluginConfig, ForcedPlugin
41from .wrappers import wrapSimpleAlgorithm, wrapTransform, GenericPlugin
42from .transforms import SimpleCentroidTransform
44from .apertureFlux import ApertureFluxControl, ApertureFluxTransform
45from .transform import BaseTransform
46from .blendedness import BlendednessAlgorithm, BlendednessControl
47from .circularApertureFlux import CircularApertureFluxAlgorithm
48from .gaussianFlux import GaussianFluxAlgorithm, GaussianFluxControl, GaussianFluxTransform
49from .exceptions import MeasurementError
50from .localBackground import LocalBackgroundControl, LocalBackgroundAlgorithm, LocalBackgroundTransform
51from .naiveCentroid import NaiveCentroidAlgorithm, NaiveCentroidControl, NaiveCentroidTransform
52from .peakLikelihoodFlux import PeakLikelihoodFluxAlgorithm, PeakLikelihoodFluxControl, \
53 PeakLikelihoodFluxTransform
54from .pixelFlags import PixelFlagsAlgorithm, PixelFlagsControl
55from .psfFlux import PsfFluxAlgorithm, PsfFluxControl, PsfFluxTransform
56from .scaledApertureFlux import ScaledApertureFluxAlgorithm, ScaledApertureFluxControl, \
57 ScaledApertureFluxTransform
58from .sdssCentroid import SdssCentroidAlgorithm, SdssCentroidControl, SdssCentroidTransform
59from .sdssShape import SdssShapeAlgorithm, SdssShapeControl, SdssShapeTransform
61__all__ = (
62 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin",
63 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin",
64 "VarianceConfig", "SingleFrameVariancePlugin", "ForcedVariancePlugin",
65 "InputCountConfig", "SingleFrameInputCountPlugin", "ForcedInputCountPlugin",
66 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin",
67 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin",
68 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin",
69 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin",
70 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin",
71 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig",
72 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig",
73)
76wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl,
77 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER,
78 shouldApCorr=True, hasLogName=True)
79wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl,
80 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
81wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl,
82 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER,
83 shouldApCorr=True)
84wrapSimpleAlgorithm(NaiveCentroidAlgorithm, Control=NaiveCentroidControl,
85 TransformClass=NaiveCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER)
86wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl,
87 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER)
88wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl,
89 executionOrder=BasePlugin.FLUX_ORDER)
90wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl,
91 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER)
92wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl,
93 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
95wrapSimpleAlgorithm(CircularApertureFluxAlgorithm, needsMetadata=True, Control=ApertureFluxControl,
96 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
97wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl,
98 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER)
100wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl,
101 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER)
103wrapTransform(PsfFluxTransform)
104wrapTransform(PeakLikelihoodFluxTransform)
105wrapTransform(GaussianFluxTransform)
106wrapTransform(NaiveCentroidTransform)
107wrapTransform(SdssCentroidTransform)
108wrapTransform(SdssShapeTransform)
109wrapTransform(ScaledApertureFluxTransform)
110wrapTransform(ApertureFluxTransform)
111wrapTransform(LocalBackgroundTransform)
114class SingleFrameFPPositionConfig(SingleFramePluginConfig):
115 """Configuration for the focal plane position measurment algorithm.
116 """
118 pass
121@register("base_FPPosition")
122class SingleFrameFPPositionPlugin(SingleFramePlugin):
123 """Algorithm to calculate the position of a centroid on the focal plane.
125 Parameters
126 ----------
127 config : `SingleFrameFPPositionConfig`
128 Plugin configuraion.
129 name : `str`
130 Plugin name.
131 schema : `lsst.afw.table.Schema`
132 The schema for the measurement output catalog. New fields will be
133 added to hold measurements produced by this plugin.
134 metadata : `lsst.daf.base.PropertySet`
135 Plugin metadata that will be attached to the output catalog
136 """
138 ConfigClass = SingleFrameFPPositionConfig
140 @classmethod
141 def getExecutionOrder(cls):
142 return cls.SHAPE_ORDER
144 def __init__(self, config, name, schema, metadata):
145 SingleFramePlugin.__init__(self, config, name, schema, metadata)
146 self.focalValue = lsst.afw.table.Point2DKey.addFields(schema, name, "Position on the focal plane",
147 "mm")
148 self.focalFlag = schema.addField(name + "_flag", type="Flag", doc="Set to True for any fatal failure")
149 self.detectorFlag = schema.addField(name + "_missingDetector_flag", type="Flag",
150 doc="Set to True if detector object is missing")
152 def measure(self, measRecord, exposure):
153 det = exposure.getDetector()
154 if not det:
155 measRecord.set(self.detectorFlag, True)
156 fp = lsst.geom.Point2D(np.nan, np.nan)
157 else:
158 center = measRecord.getCentroid()
159 fp = det.transform(center, lsst.afw.cameraGeom.PIXELS, lsst.afw.cameraGeom.FOCAL_PLANE)
160 measRecord.set(self.focalValue, fp)
162 def fail(self, measRecord, error=None):
163 measRecord.set(self.focalFlag, True)
166class SingleFrameJacobianConfig(SingleFramePluginConfig):
167 """Configuration for the Jacobian calculation plugin.
168 """
170 pixelScale = lsst.pex.config.Field(dtype=float, default=0.5, doc="Nominal pixel size (arcsec)")
173@register("base_Jacobian")
174class SingleFrameJacobianPlugin(SingleFramePlugin):
175 """Compute the Jacobian and its ratio with a nominal pixel area.
177 This enables one to compare relative, rather than absolute, pixel areas.
179 Parameters
180 ----------
181 config : `SingleFrameJacobianConfig`
182 Plugin configuraion.
183 name : `str`
184 Plugin name.
185 schema : `lsst.afw.table.Schema`
186 The schema for the measurement output catalog. New fields will be
187 added to hold measurements produced by this plugin.
188 metadata : `lsst.daf.base.PropertySet`
189 Plugin metadata that will be attached to the output catalog
190 """
192 ConfigClass = SingleFrameJacobianConfig
194 @classmethod
195 def getExecutionOrder(cls):
196 return cls.SHAPE_ORDER
198 def __init__(self, config, name, schema, metadata):
199 SingleFramePlugin.__init__(self, config, name, schema, metadata)
200 self.jacValue = schema.addField(name + '_value', type="D", doc="Jacobian correction")
201 self.jacFlag = schema.addField(name + '_flag', type="Flag", doc="Set to 1 for any fatal failure")
202 # Calculate one over the area of a nominal reference pixel, where area is in arcsec^2
203 self.scale = pow(self.config.pixelScale, -2)
205 def measure(self, measRecord, exposure):
206 center = measRecord.getCentroid()
207 # Compute the area of a pixel at a source record's centroid, and take
208 # the ratio of that with the defined reference pixel area.
209 result = np.abs(self.scale*exposure.getWcs().linearizePixelToSky(
210 center,
211 lsst.geom.arcseconds).getLinear().computeDeterminant())
212 measRecord.set(self.jacValue, result)
214 def fail(self, measRecord, error=None):
215 measRecord.set(self.jacFlag, True)
218class VarianceConfig(BaseMeasurementPluginConfig):
219 """Configuration for the variance calculation plugin.
220 """
221 scale = lsst.pex.config.Field(dtype=float, default=5.0, optional=True,
222 doc="Scale factor to apply to shape for aperture")
223 mask = lsst.pex.config.ListField(doc="Mask planes to ignore", dtype=str,
224 default=["DETECTED", "DETECTED_NEGATIVE", "BAD", "SAT"])
227class VariancePlugin(GenericPlugin):
228 """Compute the median variance corresponding to a footprint.
230 The aim here is to measure the background variance, rather than that of
231 the object itself. In order to achieve this, the variance is calculated
232 over an area scaled up from the shape of the input footprint.
234 Parameters
235 ----------
236 config : `VarianceConfig`
237 Plugin configuraion.
238 name : `str`
239 Plugin name.
240 schema : `lsst.afw.table.Schema`
241 The schema for the measurement output catalog. New fields will be
242 added to hold measurements produced by this plugin.
243 metadata : `lsst.daf.base.PropertySet`
244 Plugin metadata that will be attached to the output catalog
245 """
247 ConfigClass = VarianceConfig
249 FAILURE_BAD_CENTROID = 1
250 """Denotes failures due to bad centroiding (`int`).
251 """
253 FAILURE_EMPTY_FOOTPRINT = 2
254 """Denotes failures due to a lack of usable pixels (`int`).
255 """
257 @classmethod
258 def getExecutionOrder(cls):
259 return BasePlugin.FLUX_ORDER
261 def __init__(self, config, name, schema, metadata):
262 GenericPlugin.__init__(self, config, name, schema, metadata)
263 self.varValue = schema.addField(name + '_value', type="D", doc="Variance at object position")
264 self.emptyFootprintFlag = schema.addField(name + '_flag_emptyFootprint', type="Flag",
265 doc="Set to True when the footprint has no usable pixels")
267 # Alias the badCentroid flag to that which is defined for the target
268 # of the centroid slot. We do not simply rely on the alias because
269 # that could be changed post-measurement.
270 schema.getAliasMap().set(name + '_flag_badCentroid', schema.getAliasMap().apply("slot_Centroid_flag"))
272 def measure(self, measRecord, exposure, center):
273 # Create an aperture and grow it by scale value defined in config to
274 # ensure there are enough pixels around the object to get decent
275 # statistics
276 if not np.all(np.isfinite(measRecord.getCentroid())):
277 raise MeasurementError("Bad centroid and/or shape", self.FAILURE_BAD_CENTROID)
278 aperture = lsst.afw.geom.Ellipse(measRecord.getShape(), measRecord.getCentroid())
279 aperture.scale(self.config.scale)
280 ellipse = lsst.afw.geom.SpanSet.fromShape(aperture)
281 foot = lsst.afw.detection.Footprint(ellipse)
282 foot.clipTo(exposure.getBBox(lsst.afw.image.PARENT))
283 # Filter out any pixels which have mask bits set corresponding to the
284 # planes to be excluded (defined in config.mask)
285 maskedImage = exposure.getMaskedImage()
286 pixels = lsst.afw.detection.makeHeavyFootprint(foot, maskedImage)
287 maskBits = maskedImage.getMask().getPlaneBitMask(self.config.mask)
288 logicalMask = np.logical_not(pixels.getMaskArray() & maskBits)
289 # Compute the median variance value for each pixel not excluded by the
290 # mask and write the record. Numpy median is used here instead of
291 # afw.math makeStatistics because of an issue with data types being
292 # passed into the C++ layer (DM-2379).
293 if np.any(logicalMask):
294 medVar = np.median(pixels.getVarianceArray()[logicalMask])
295 measRecord.set(self.varValue, medVar)
296 else:
297 raise MeasurementError("Footprint empty, or all pixels are masked, can't compute median",
298 self.FAILURE_EMPTY_FOOTPRINT)
300 def fail(self, measRecord, error=None):
301 # Check that we have an error object and that it is of type
302 # MeasurementError
303 if isinstance(error, MeasurementError):
304 assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_EMPTY_FOOTPRINT)
305 # FAILURE_BAD_CENTROID handled by alias to centroid record.
306 if error.getFlagBit() == self.FAILURE_EMPTY_FOOTPRINT:
307 measRecord.set(self.emptyFootprintFlag, True)
308 measRecord.set(self.varValue, np.nan)
309 GenericPlugin.fail(self, measRecord, error)
312SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance")
313"""Single-frame version of `VariancePlugin`.
314"""
316ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance")
317"""Forced version of `VariancePlugin`.
318"""
321class InputCountConfig(BaseMeasurementPluginConfig):
322 """Configuration for the input image counting plugin.
323 """
324 pass
327class InputCountPlugin(GenericPlugin):
328 """Count the number of input images which contributed to a a source.
330 Parameters
331 ----------
332 config : `InputCountConfig`
333 Plugin configuraion.
334 name : `str`
335 Plugin name.
336 schema : `lsst.afw.table.Schema`
337 The schema for the measurement output catalog. New fields will be
338 added to hold measurements produced by this plugin.
339 metadata : `lsst.daf.base.PropertySet`
340 Plugin metadata that will be attached to the output catalog
342 Notes
343 -----
344 Information is derived from the image's `~lsst.afw.image.CoaddInputs`.
345 Note these limitation:
347 - This records the number of images which contributed to the pixel in the
348 center of the source footprint, rather than to any or all pixels in the
349 source.
350 - Clipping in the coadd is not taken into account.
351 """
353 ConfigClass = InputCountConfig
355 FAILURE_BAD_CENTROID = 1
356 """Denotes failures due to bad centroiding (`int`).
357 """
359 FAILURE_NO_INPUTS = 2
360 """Denotes failures due to the image not having coadd inputs. (`int`)
361 """
363 @classmethod
364 def getExecutionOrder(cls):
365 return BasePlugin.SHAPE_ORDER
367 def __init__(self, config, name, schema, metadata):
368 GenericPlugin.__init__(self, config, name, schema, metadata)
369 self.numberKey = schema.addField(name + '_value', type="I",
370 doc="Number of images contributing at center, not including any"
371 "clipping")
372 self.noInputsFlag = schema.addField(name + '_flag_noInputs', type="Flag",
373 doc="No coadd inputs available")
374 # Alias the badCentroid flag to that which is defined for the target of the centroid slot.
375 # We do not simply rely on the alias because that could be changed post-measurement.
376 schema.getAliasMap().set(name + '_flag_badCentroid', schema.getAliasMap().apply("slot_Centroid_flag"))
378 def measure(self, measRecord, exposure, center):
379 if not exposure.getInfo().getCoaddInputs():
380 raise MeasurementError("No coadd inputs defined.", self.FAILURE_NO_INPUTS)
381 if not np.all(np.isfinite(center)):
382 raise MeasurementError("Source has a bad centroid.", self.FAILURE_BAD_CENTROID)
384 ccds = exposure.getInfo().getCoaddInputs().ccds
385 measRecord.set(self.numberKey, len(ccds.subsetContaining(center, exposure.getWcs())))
387 def fail(self, measRecord, error=None):
388 if error is not None:
389 assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_NO_INPUTS)
390 # FAILURE_BAD_CENTROID handled by alias to centroid record.
391 if error.getFlagBit() == self.FAILURE_NO_INPUTS:
392 measRecord.set(self.noInputsFlag, True)
393 GenericPlugin.fail(self, measRecord, error)
396SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount")
397"""Single-frame version of `InputCoutPlugin`.
398"""
400ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount")
401"""Forced version of `InputCoutPlugin`.
402"""
405class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig):
406 """Configuration for the variance calculation plugin.
407 """
408 pass
411class EvaluateLocalPhotoCalibPlugin(GenericPlugin):
412 """Evaluate the local value of the Photometric Calibration in the exposure.
414 The aim is to store the local calib value within the catalog for later
415 use in the Science Data Model functors.
416 """
417 ConfigClass = EvaluateLocalPhotoCalibPluginConfig
419 @classmethod
420 def getExecutionOrder(cls):
421 return BasePlugin.FLUX_ORDER
423 def __init__(self, config, name, schema, metadata):
424 GenericPlugin.__init__(self, config, name, schema, metadata)
425 self.photoKey = schema.addField(
426 name,
427 type="D",
428 doc="Local approximation of the PhotoCalib calibration factor at "
429 "the location of the src.")
430 self.photoErrKey = schema.addField(
431 "%sErr" % name,
432 type="D",
433 doc="Error on the local approximation of the PhotoCalib "
434 "calibration factor at the location of the src.")
436 def measure(self, measRecord, exposure, center):
438 photoCalib = exposure.getPhotoCalib()
439 calib = photoCalib.getLocalCalibration(center)
440 measRecord.set(self.photoKey, calib)
442 calibErr = photoCalib.getCalibrationErr()
443 measRecord.set(self.photoErrKey, calibErr)
446SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin(
447 "base_LocalPhotoCalib")
448"""Single-frame version of `EvaluatePhotoCalibPlugin`.
449"""
451ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin(
452 "base_LocalPhotoCalib")
453"""Forced version of `EvaluatePhotoCalibPlugin`.
454"""
457class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig):
458 """Configuration for the variance calculation plugin.
459 """
460 pass
463class EvaluateLocalWcsPlugin(GenericPlugin):
464 """Evaluate the local, linear approximation of the Wcs.
466 The aim is to store the local calib value within the catalog for later
467 use in the Science Data Model functors.
468 """
469 ConfigClass = EvaluateLocalWcsPluginConfig
470 _scale = (1.0 * lsst.geom.arcseconds).asDegrees()
472 @classmethod
473 def getExecutionOrder(cls):
474 return BasePlugin.FLUX_ORDER
476 def __init__(self, config, name, schema, metadata):
477 GenericPlugin.__init__(self, config, name, schema, metadata)
478 self.cdMatrix11Key = schema.addField(
479 f"{name}_CDMatrix_1_1",
480 type="D",
481 doc=f"(1, 1) element of the CDMatrix for the linear approximation "
482 "of the WCS at the src location. Gives units in radians.")
483 self.cdMatrix12Key = schema.addField(
484 f"{name}_CDMatrix_1_2",
485 type="D",
486 doc=f"(1, 2) element of the CDMatrix for the linear approximation "
487 "of the WCS at the src location. Gives units in radians.")
488 self.cdMatrix21Key = schema.addField(
489 f"{name}_CDMatrix_2_1",
490 type="D",
491 doc=f"(2, 1) element of the CDMatrix for the linear approximation "
492 "of the WCS at the src location. Gives units in radians.")
493 self.cdMatrix22Key = schema.addField(
494 f"{name}_CDMatrix_2_2",
495 type="D",
496 doc=f"(2, 2) element of the CDMatrix for the linear approximation "
497 "of the WCS at the src location. Gives units in radians.")
499 def measure(self, measRecord, exposure, center):
500 wcs = exposure.getWcs()
501 localMatrix = self.makeLocalTransformMatrix(wcs, center)
502 measRecord.set(self.cdMatrix11Key, localMatrix[0, 0])
503 measRecord.set(self.cdMatrix12Key, localMatrix[0, 1])
504 measRecord.set(self.cdMatrix21Key, localMatrix[1, 0])
505 measRecord.set(self.cdMatrix22Key, localMatrix[1, 1])
507 def makeLocalTransformMatrix(self, wcs, center):
508 """Create a local, linear approximation of the wcs transformation
509 matrix.
511 The approximation is created as if the center is at RA=0, DEC=0. All
512 comparing x,y coordinate are relative to the position of center. Matrix
513 is initially calculated with units arcseconds and then converted to
514 radians. This yields higher precision results due to quirks in AST.
516 Parameters
517 ----------
518 wcs : `lsst.afw.geom.SkyWcs`
519 Wcs to approximate
520 center : `lsst.geom.Point2D`
521 Point at which to evaluate the LocalWcs.
523 Returns
524 -------
525 localMatrix : `numpy.ndarray`
526 Matrix representation the local wcs approximation with units
527 radians.
528 """
529 skyCenter = wcs.pixelToSky(center)
530 localGnomonicWcs = lsst.afw.geom.makeSkyWcs(
531 center, skyCenter, np.diag((self._scale, self._scale)))
532 measurementToLocalGnomonic = wcs.getTransform().then(
533 localGnomonicWcs.getTransform().inverted()
534 )
535 localMatrix = measurementToLocalGnomonic.getJacobian(center)
536 return np.radians(localMatrix / 3600)
539SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs")
540"""Single-frame version of `EvaluateLocalWcsPlugin`.
541"""
543ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs")
544"""Forced version of `EvaluateLocalWcsPlugin`.
545"""
548class SingleFramePeakCentroidConfig(SingleFramePluginConfig):
549 """Configuration for the single frame peak centroiding algorithm.
550 """
551 pass
554@register("base_PeakCentroid")
555class SingleFramePeakCentroidPlugin(SingleFramePlugin):
556 """Record the highest peak in a source footprint as its centroid.
558 This is of course a relatively poor measure of the true centroid of the
559 object; this algorithm is provided mostly for testing and debugging.
561 Parameters
562 ----------
563 config : `SingleFramePeakCentroidConfig`
564 Plugin configuraion.
565 name : `str`
566 Plugin name.
567 schema : `lsst.afw.table.Schema`
568 The schema for the measurement output catalog. New fields will be
569 added to hold measurements produced by this plugin.
570 metadata : `lsst.daf.base.PropertySet`
571 Plugin metadata that will be attached to the output catalog
572 """
574 ConfigClass = SingleFramePeakCentroidConfig
576 @classmethod
577 def getExecutionOrder(cls):
578 return cls.CENTROID_ORDER
580 def __init__(self, config, name, schema, metadata):
581 SingleFramePlugin.__init__(self, config, name, schema, metadata)
582 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
583 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
584 self.flag = schema.addField(name + "_flag", type="Flag", doc="Centroiding failed")
586 def measure(self, measRecord, exposure):
587 peak = measRecord.getFootprint().getPeaks()[0]
588 measRecord.set(self.keyX, peak.getFx())
589 measRecord.set(self.keyY, peak.getFy())
591 def fail(self, measRecord, error=None):
592 measRecord.set(self.flag, True)
594 @staticmethod
595 def getTransformClass():
596 return SimpleCentroidTransform
599class SingleFrameSkyCoordConfig(SingleFramePluginConfig):
600 """Configuration for the sky coordinates algorithm.
601 """
602 pass
605@register("base_SkyCoord")
606class SingleFrameSkyCoordPlugin(SingleFramePlugin):
607 """Record the sky position of an object based on its centroid slot and WCS.
609 The position is record in the ``coord`` field, which is part of the
610 `~lsst.afw.table.SourceCatalog` minimal schema.
612 Parameters
613 ----------
614 config : `SingleFrameSkyCoordConfig`
615 Plugin configuraion.
616 name : `str`
617 Plugin name.
618 schema : `lsst.afw.table.Schema`
619 The schema for the measurement output catalog. New fields will be
620 added to hold measurements produced by this plugin.
621 metadata : `lsst.daf.base.PropertySet`
622 Plugin metadata that will be attached to the output catalog
623 """
625 ConfigClass = SingleFrameSkyCoordConfig
627 @classmethod
628 def getExecutionOrder(cls):
629 return cls.SHAPE_ORDER
631 def measure(self, measRecord, exposure):
632 # There should be a base class method for handling this exception. Put
633 # this on a later ticket. Also, there should be a python Exception of
634 # the appropriate type for this error
635 if not exposure.hasWcs():
636 raise RuntimeError("Wcs not attached to exposure. Required for " + self.name + " algorithm")
637 measRecord.updateCoord(exposure.getWcs())
639 def fail(self, measRecord, error=None):
640 # Override fail() to do nothing in the case of an exception: this is
641 # not ideal, but we don't have a place to put failures because we
642 # don't allocate any fields. Should consider fixing as part of
643 # DM-1011
644 pass
647class ForcedPeakCentroidConfig(ForcedPluginConfig):
648 """Configuration for the forced peak centroid algorithm.
649 """
650 pass
653@register("base_PeakCentroid")
654class ForcedPeakCentroidPlugin(ForcedPlugin):
655 """Record the highest peak in a source footprint as its centroid.
657 This is of course a relatively poor measure of the true centroid of the
658 object; this algorithm is provided mostly for testing and debugging.
660 This is similar to `SingleFramePeakCentroidPlugin`, except that transforms
661 the peak coordinate from the original (reference) coordinate system to the
662 coordinate system of the exposure being measured.
664 Parameters
665 ----------
666 config : `ForcedPeakCentroidConfig`
667 Plugin configuraion.
668 name : `str`
669 Plugin name.
670 schemaMapper : `lsst.afw.table.SchemaMapper`
671 A mapping from reference catalog fields to output
672 catalog fields. Output fields are added to the output schema.
673 metadata : `lsst.daf.base.PropertySet`
674 Plugin metadata that will be attached to the output catalog.
675 """
677 ConfigClass = ForcedPeakCentroidConfig
679 @classmethod
680 def getExecutionOrder(cls):
681 return cls.CENTROID_ORDER
683 def __init__(self, config, name, schemaMapper, metadata):
684 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
685 schema = schemaMapper.editOutputSchema()
686 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
687 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
689 def measure(self, measRecord, exposure, refRecord, refWcs):
690 targetWcs = exposure.getWcs()
691 peak = refRecord.getFootprint().getPeaks()[0]
692 result = lsst.geom.Point2D(peak.getFx(), peak.getFy())
693 result = targetWcs.skyToPixel(refWcs.pixelToSky(result))
694 measRecord.set(self.keyX, result.getX())
695 measRecord.set(self.keyY, result.getY())
697 @staticmethod
698 def getTransformClass():
699 return SimpleCentroidTransform
702class ForcedTransformedCentroidConfig(ForcedPluginConfig):
703 """Configuration for the forced transformed centroid algorithm.
704 """
705 pass
708@register("base_TransformedCentroid")
709class ForcedTransformedCentroidPlugin(ForcedPlugin):
710 """Record the transformation of the reference catalog centroid.
712 The centroid recorded in the reference catalog is tranformed to the
713 measurement coordinate system and stored.
715 Parameters
716 ----------
717 config : `ForcedTransformedCentroidConfig`
718 Plugin configuration
719 name : `str`
720 Plugin name
721 schemaMapper : `lsst.afw.table.SchemaMapper`
722 A mapping from reference catalog fields to output
723 catalog fields. Output fields are added to the output schema.
724 metadata : `lsst.daf.base.PropertySet`
725 Plugin metadata that will be attached to the output catalog.
727 Notes
728 -----
729 This is used as the slot centroid by default in forced measurement,
730 allowing subsequent measurements to simply refer to the slot value just as
731 they would in single-frame measurement.
732 """
734 ConfigClass = ForcedTransformedCentroidConfig
736 @classmethod
737 def getExecutionOrder(cls):
738 return cls.CENTROID_ORDER
740 def __init__(self, config, name, schemaMapper, metadata):
741 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
742 schema = schemaMapper.editOutputSchema()
743 # Allocate x and y fields, join these into a single FunctorKey for ease-of-use.
744 xKey = schema.addField(name + "_x", type="D", doc="transformed reference centroid column",
745 units="pixel")
746 yKey = schema.addField(name + "_y", type="D", doc="transformed reference centroid row",
747 units="pixel")
748 self.centroidKey = lsst.afw.table.Point2DKey(xKey, yKey)
749 # Because we're taking the reference position as given, we don't bother transforming its
750 # uncertainty and reporting that here, so there are no sigma or cov fields. We do propagate
751 # the flag field, if it exists.
752 if "slot_Centroid_flag" in schemaMapper.getInputSchema():
753 self.flagKey = schema.addField(name + "_flag", type="Flag",
754 doc="whether the reference centroid is marked as bad")
755 else:
756 self.flagKey = None
758 def measure(self, measRecord, exposure, refRecord, refWcs):
759 targetWcs = exposure.getWcs()
760 if not refWcs == targetWcs:
761 targetPos = targetWcs.skyToPixel(refWcs.pixelToSky(refRecord.getCentroid()))
762 measRecord.set(self.centroidKey, targetPos)
763 else:
764 measRecord.set(self.centroidKey, refRecord.getCentroid())
765 if self.flagKey is not None:
766 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
769class ForcedTransformedShapeConfig(ForcedPluginConfig):
770 """Configuration for the forced transformed shape algorithm.
771 """
772 pass
775@register("base_TransformedShape")
776class ForcedTransformedShapePlugin(ForcedPlugin):
777 """Record the transformation of the reference catalog shape.
779 The shape recorded in the reference catalog is tranformed to the
780 measurement coordinate system and stored.
782 Parameters
783 ----------
784 config : `ForcedTransformedShapeConfig`
785 Plugin configuration
786 name : `str`
787 Plugin name
788 schemaMapper : `lsst.afw.table.SchemaMapper`
789 A mapping from reference catalog fields to output
790 catalog fields. Output fields are added to the output schema.
791 metadata : `lsst.daf.base.PropertySet`
792 Plugin metadata that will be attached to the output catalog.
794 Notes
795 -----
796 This is used as the slot shape by default in forced measurement, allowing
797 subsequent measurements to simply refer to the slot value just as they
798 would in single-frame measurement.
799 """
801 ConfigClass = ForcedTransformedShapeConfig
803 @classmethod
804 def getExecutionOrder(cls):
805 return cls.SHAPE_ORDER
807 def __init__(self, config, name, schemaMapper, metadata):
808 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
809 schema = schemaMapper.editOutputSchema()
810 # Allocate xx, yy, xy fields, join these into a single FunctorKey for ease-of-use.
811 xxKey = schema.addField(name + "_xx", type="D", doc="transformed reference shape x^2 moment",
812 units="pixel^2")
813 yyKey = schema.addField(name + "_yy", type="D", doc="transformed reference shape y^2 moment",
814 units="pixel^2")
815 xyKey = schema.addField(name + "_xy", type="D", doc="transformed reference shape xy moment",
816 units="pixel^2")
817 self.shapeKey = lsst.afw.table.QuadrupoleKey(xxKey, yyKey, xyKey)
818 # Because we're taking the reference position as given, we don't bother transforming its
819 # uncertainty and reporting that here, so there are no sigma or cov fields. We do propagate
820 # the flag field, if it exists.
821 if "slot_Shape_flag" in schemaMapper.getInputSchema():
822 self.flagKey = schema.addField(name + "_flag", type="Flag",
823 doc="whether the reference shape is marked as bad")
824 else:
825 self.flagKey = None
827 def measure(self, measRecord, exposure, refRecord, refWcs):
828 targetWcs = exposure.getWcs()
829 if not refWcs == targetWcs:
830 fullTransform = lsst.afw.geom.makeWcsPairTransform(refWcs, targetWcs)
831 localTransform = lsst.afw.geom.linearizeTransform(fullTransform, refRecord.getCentroid())
832 measRecord.set(self.shapeKey, refRecord.getShape().transform(localTransform.getLinear()))
833 else:
834 measRecord.set(self.shapeKey, refRecord.getShape())
835 if self.flagKey is not None:
836 measRecord.set(self.flagKey, refRecord.getShapeFlag())