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

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
471 @classmethod
472 def getExecutionOrder(cls):
473 return BasePlugin.FLUX_ORDER
475 def __init__(self, config, name, schema, metadata):
476 GenericPlugin.__init__(self, config, name, schema, metadata)
477 self.cdMatrix11Key = schema.addField(
478 "%s_CDMatrix_1_1" % name,
479 type="D",
480 doc="(1, 1) element of the CDMatrix for the linear approximation "
481 "of the WCS at the src location.")
482 self.cdMatrix12Key = schema.addField(
483 "%s_CDMatrix_1_2" % name,
484 type="D",
485 doc="(1, 2) element of the CDMatrix for the linear approximation "
486 "of the WCS at the src location.")
487 self.cdMatrix21Key = schema.addField(
488 "%s_CDMatrix_2_1" % name,
489 type="D",
490 doc="(2, 1) element of the CDMatrix for the linear approximation "
491 "of the WCS at the src location.")
492 self.cdMatrix22Key = schema.addField(
493 "%s_CDMatrix_2_2" % name,
494 type="D",
495 doc="(2, 2) element of the CDMatrix for the linear approximation "
496 "of the WCS at the src location.")
498 def measure(self, measRecord, exposure, center):
499 localCDMatrix = exposure.getWcs().getCdMatrix(center)
500 measRecord.set(self.cdMatrix11Key, localCDMatrix[0, 0])
501 measRecord.set(self.cdMatrix12Key, localCDMatrix[0, 1])
502 measRecord.set(self.cdMatrix21Key, localCDMatrix[1, 0])
503 measRecord.set(self.cdMatrix22Key, localCDMatrix[1, 1])
506SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs")
507"""Single-frame version of `EvaluateLocalWcsPlugin`.
508"""
510ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs")
511"""Forced version of `EvaluateLocalWcsPlugin`.
512"""
515class SingleFramePeakCentroidConfig(SingleFramePluginConfig):
516 """Configuration for the single frame peak centroiding algorithm.
517 """
518 pass
521@register("base_PeakCentroid")
522class SingleFramePeakCentroidPlugin(SingleFramePlugin):
523 """Record the highest peak in a source footprint as its centroid.
525 This is of course a relatively poor measure of the true centroid of the
526 object; this algorithm is provided mostly for testing and debugging.
528 Parameters
529 ----------
530 config : `SingleFramePeakCentroidConfig`
531 Plugin configuraion.
532 name : `str`
533 Plugin name.
534 schema : `lsst.afw.table.Schema`
535 The schema for the measurement output catalog. New fields will be
536 added to hold measurements produced by this plugin.
537 metadata : `lsst.daf.base.PropertySet`
538 Plugin metadata that will be attached to the output catalog
539 """
541 ConfigClass = SingleFramePeakCentroidConfig
543 @classmethod
544 def getExecutionOrder(cls):
545 return cls.CENTROID_ORDER
547 def __init__(self, config, name, schema, metadata):
548 SingleFramePlugin.__init__(self, config, name, schema, metadata)
549 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
550 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
551 self.flag = schema.addField(name + "_flag", type="Flag", doc="Centroiding failed")
553 def measure(self, measRecord, exposure):
554 peak = measRecord.getFootprint().getPeaks()[0]
555 measRecord.set(self.keyX, peak.getFx())
556 measRecord.set(self.keyY, peak.getFy())
558 def fail(self, measRecord, error=None):
559 measRecord.set(self.flag, True)
561 @staticmethod
562 def getTransformClass():
563 return SimpleCentroidTransform
566class SingleFrameSkyCoordConfig(SingleFramePluginConfig):
567 """Configuration for the sky coordinates algorithm.
568 """
569 pass
572@register("base_SkyCoord")
573class SingleFrameSkyCoordPlugin(SingleFramePlugin):
574 """Record the sky position of an object based on its centroid slot and WCS.
576 The position is record in the ``coord`` field, which is part of the
577 `~lsst.afw.table.SourceCatalog` minimal schema.
579 Parameters
580 ----------
581 config : `SingleFrameSkyCoordConfig`
582 Plugin configuraion.
583 name : `str`
584 Plugin name.
585 schema : `lsst.afw.table.Schema`
586 The schema for the measurement output catalog. New fields will be
587 added to hold measurements produced by this plugin.
588 metadata : `lsst.daf.base.PropertySet`
589 Plugin metadata that will be attached to the output catalog
590 """
592 ConfigClass = SingleFrameSkyCoordConfig
594 @classmethod
595 def getExecutionOrder(cls):
596 return cls.SHAPE_ORDER
598 def measure(self, measRecord, exposure):
599 # There should be a base class method for handling this exception. Put
600 # this on a later ticket. Also, there should be a python Exception of
601 # the appropriate type for this error
602 if not exposure.hasWcs():
603 raise Exception("Wcs not attached to exposure. Required for " + self.name + " algorithm")
604 measRecord.updateCoord(exposure.getWcs())
606 def fail(self, measRecord, error=None):
607 # Override fail() to do nothing in the case of an exception: this is
608 # not ideal, but we don't have a place to put failures because we
609 # don't allocate any fields. Should consider fixing as part of
610 # DM-1011
611 pass
614class ForcedPeakCentroidConfig(ForcedPluginConfig):
615 """Configuration for the forced peak centroid algorithm.
616 """
617 pass
620@register("base_PeakCentroid")
621class ForcedPeakCentroidPlugin(ForcedPlugin):
622 """Record the highest peak in a source footprint as its centroid.
624 This is of course a relatively poor measure of the true centroid of the
625 object; this algorithm is provided mostly for testing and debugging.
627 This is similar to `SingleFramePeakCentroidPlugin`, except that transforms
628 the peak coordinate from the original (reference) coordinate system to the
629 coordinate system of the exposure being measured.
631 Parameters
632 ----------
633 config : `ForcedPeakCentroidConfig`
634 Plugin configuraion.
635 name : `str`
636 Plugin name.
637 schemaMapper : `lsst.afw.table.SchemaMapper`
638 A mapping from reference catalog fields to output
639 catalog fields. Output fields are added to the output schema.
640 metadata : `lsst.daf.base.PropertySet`
641 Plugin metadata that will be attached to the output catalog.
642 """
644 ConfigClass = ForcedPeakCentroidConfig
646 @classmethod
647 def getExecutionOrder(cls):
648 return cls.CENTROID_ORDER
650 def __init__(self, config, name, schemaMapper, metadata):
651 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
652 schema = schemaMapper.editOutputSchema()
653 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
654 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
656 def measure(self, measRecord, exposure, refRecord, refWcs):
657 targetWcs = exposure.getWcs()
658 peak = refRecord.getFootprint().getPeaks()[0]
659 result = lsst.geom.Point2D(peak.getFx(), peak.getFy())
660 result = targetWcs.skyToPixel(refWcs.pixelToSky(result))
661 measRecord.set(self.keyX, result.getX())
662 measRecord.set(self.keyY, result.getY())
664 @staticmethod
665 def getTransformClass():
666 return SimpleCentroidTransform
669class ForcedTransformedCentroidConfig(ForcedPluginConfig):
670 """Configuration for the forced transformed centroid algorithm.
671 """
672 pass
675@register("base_TransformedCentroid")
676class ForcedTransformedCentroidPlugin(ForcedPlugin):
677 """Record the transformation of the reference catalog centroid.
679 The centroid recorded in the reference catalog is tranformed to the
680 measurement coordinate system and stored.
682 Parameters
683 ----------
684 config : `ForcedTransformedCentroidConfig`
685 Plugin configuration
686 name : `str`
687 Plugin name
688 schemaMapper : `lsst.afw.table.SchemaMapper`
689 A mapping from reference catalog fields to output
690 catalog fields. Output fields are added to the output schema.
691 metadata : `lsst.daf.base.PropertySet`
692 Plugin metadata that will be attached to the output catalog.
694 Notes
695 -----
696 This is used as the slot centroid by default in forced measurement,
697 allowing subsequent measurements to simply refer to the slot value just as
698 they would in single-frame measurement.
699 """
701 ConfigClass = ForcedTransformedCentroidConfig
703 @classmethod
704 def getExecutionOrder(cls):
705 return cls.CENTROID_ORDER
707 def __init__(self, config, name, schemaMapper, metadata):
708 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
709 schema = schemaMapper.editOutputSchema()
710 # Allocate x and y fields, join these into a single FunctorKey for ease-of-use.
711 xKey = schema.addField(name + "_x", type="D", doc="transformed reference centroid column",
712 units="pixel")
713 yKey = schema.addField(name + "_y", type="D", doc="transformed reference centroid row",
714 units="pixel")
715 self.centroidKey = lsst.afw.table.Point2DKey(xKey, yKey)
716 # Because we're taking the reference position as given, we don't bother transforming its
717 # uncertainty and reporting that here, so there are no sigma or cov fields. We do propagate
718 # the flag field, if it exists.
719 if "slot_Centroid_flag" in schemaMapper.getInputSchema():
720 self.flagKey = schema.addField(name + "_flag", type="Flag",
721 doc="whether the reference centroid is marked as bad")
722 else:
723 self.flagKey = None
725 def measure(self, measRecord, exposure, refRecord, refWcs):
726 targetWcs = exposure.getWcs()
727 if not refWcs == targetWcs:
728 targetPos = targetWcs.skyToPixel(refWcs.pixelToSky(refRecord.getCentroid()))
729 measRecord.set(self.centroidKey, targetPos)
730 else:
731 measRecord.set(self.centroidKey, refRecord.getCentroid())
732 if self.flagKey is not None:
733 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
736class ForcedTransformedShapeConfig(ForcedPluginConfig):
737 """Configuration for the forced transformed shape algorithm.
738 """
739 pass
742@register("base_TransformedShape")
743class ForcedTransformedShapePlugin(ForcedPlugin):
744 """Record the transformation of the reference catalog shape.
746 The shape recorded in the reference catalog is tranformed to the
747 measurement coordinate system and stored.
749 Parameters
750 ----------
751 config : `ForcedTransformedShapeConfig`
752 Plugin configuration
753 name : `str`
754 Plugin name
755 schemaMapper : `lsst.afw.table.SchemaMapper`
756 A mapping from reference catalog fields to output
757 catalog fields. Output fields are added to the output schema.
758 metadata : `lsst.daf.base.PropertySet`
759 Plugin metadata that will be attached to the output catalog.
761 Notes
762 -----
763 This is used as the slot shape by default in forced measurement, allowing
764 subsequent measurements to simply refer to the slot value just as they
765 would in single-frame measurement.
766 """
768 ConfigClass = ForcedTransformedShapeConfig
770 @classmethod
771 def getExecutionOrder(cls):
772 return cls.SHAPE_ORDER
774 def __init__(self, config, name, schemaMapper, metadata):
775 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
776 schema = schemaMapper.editOutputSchema()
777 # Allocate xx, yy, xy fields, join these into a single FunctorKey for ease-of-use.
778 xxKey = schema.addField(name + "_xx", type="D", doc="transformed reference shape x^2 moment",
779 units="pixel^2")
780 yyKey = schema.addField(name + "_yy", type="D", doc="transformed reference shape y^2 moment",
781 units="pixel^2")
782 xyKey = schema.addField(name + "_xy", type="D", doc="transformed reference shape xy moment",
783 units="pixel^2")
784 self.shapeKey = lsst.afw.table.QuadrupoleKey(xxKey, yyKey, xyKey)
785 # Because we're taking the reference position as given, we don't bother transforming its
786 # uncertainty and reporting that here, so there are no sigma or cov fields. We do propagate
787 # the flag field, if it exists.
788 if "slot_Shape_flag" in schemaMapper.getInputSchema():
789 self.flagKey = schema.addField(name + "_flag", type="Flag",
790 doc="whether the reference shape is marked as bad")
791 else:
792 self.flagKey = None
794 def measure(self, measRecord, exposure, refRecord, refWcs):
795 targetWcs = exposure.getWcs()
796 if not refWcs == targetWcs:
797 fullTransform = lsst.afw.geom.makeWcsPairTransform(refWcs, targetWcs)
798 localTransform = lsst.afw.geom.linearizeTransform(fullTransform, refRecord.getCentroid())
799 measRecord.set(self.shapeKey, refRecord.getShape().transform(localTransform.getLinear()))
800 else:
801 measRecord.set(self.shapeKey, refRecord.getShape())
802 if self.flagKey is not None:
803 measRecord.set(self.flagKey, refRecord.getShapeFlag())