LSST Applications g0000d66e7c+4a51730b0a,g0485b4d2cb+be65c9c1d7,g0fba68d861+f95c35e0c3,g1ec0fe41b4+3ea9d11450,g1fd858c14a+41d169aaf2,g2440f9efcc+8c5ae1fdc5,g35bb328faa+8c5ae1fdc5,g4d2262a081+30937b6477,g53246c7159+8c5ae1fdc5,g55585698de+c657de43f9,g56a49b3a55+7eddd92ad8,g60b5630c4e+c657de43f9,g67b6fd64d1+97cc007aa2,g78460c75b0+7e33a9eb6d,g786e29fd12+668abc6043,g7ac00fbb6c+9304e3655a,g8352419a5c+8c5ae1fdc5,g8852436030+3f3bba821f,g89139ef638+97cc007aa2,g94187f82dc+c657de43f9,g989de1cb63+97cc007aa2,g9d31334357+c657de43f9,g9f33ca652e+06d39d8afb,ga815be3f0b+8e7c4d07ad,gabe3b4be73+8856018cbb,gabf8522325+977d9fabaf,gb1101e3267+12c96a40b1,gb89ab40317+97cc007aa2,gc91f06edcd+2ffb87f22b,gcf25f946ba+3f3bba821f,gd6cbbdb0b4+1cc2750d2e,gde0f65d7ad+bbe98f05bf,ge278dab8ac+6b863515ed,ge410e46f29+97cc007aa2,gf35d7ec915+97dd712d81,gf5e32f922b+8c5ae1fdc5,gf67bdafdda+97cc007aa2,gf6800124b1+bb7d8e732a,w.2025.19
LSST Data Management Base Package
Loading...
Searching...
No Matches
plugins.py
Go to the documentation of this file.
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/>.
21
22"""Definition of measurement plugins.
23
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"""
28
29import logging
30import numpy as np
31
33import lsst.geom
35import lsst.afw.geom
36
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)
55
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
63
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)
82
83
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)
100
101wrapSimpleAlgorithm(CircularApertureFluxAlgorithm, needsMetadata=True, Control=ApertureFluxControl,
102 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER)
103wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl,
104 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER)
105
106wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl,
107 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER)
108
109wrapTransform(PsfFluxTransform)
110wrapTransform(PeakLikelihoodFluxTransform)
111wrapTransform(GaussianFluxTransform)
112wrapTransform(SdssCentroidTransform)
113wrapTransform(SdssShapeTransform)
114wrapTransform(ScaledApertureFluxTransform)
115wrapTransform(ApertureFluxTransform)
116wrapTransform(LocalBackgroundTransform)
117
118log = logging.getLogger(__name__)
119
120
122 """Configuration for the focal plane position measurment algorithm.
123 """
124
125
126@register("base_FPPosition")
128 """Algorithm to calculate the position of a centroid on the focal plane.
129
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 """
142
143 ConfigClass = SingleFrameFPPositionConfig
144
145 @classmethod
147 return cls.SHAPE_ORDER
148
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")
156
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)
166
167 def fail(self, measRecord, error=None):
168 measRecord.set(self.focalFlag, True)
169
170
172 """Configuration for the Jacobian calculation plugin.
173 """
174
175 pixelScale = lsst.pex.config.Field(dtype=float, default=0.5, doc="Nominal pixel size (arcsec)")
176
177
178@register("base_Jacobian")
180 """Compute the Jacobian and its ratio with a nominal pixel area.
181
182 This enables one to compare relative, rather than absolute, pixel areas.
183
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 """
196
197 ConfigClass = SingleFrameJacobianConfig
198
199 @classmethod
201 return cls.SHAPE_ORDER
202
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)
210
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)
219
220 def fail(self, measRecord, error=None):
221 measRecord.set(self.jacFlag, True)
222
223
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"])
231
232
234 """Compute the median variance corresponding to a footprint.
235
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.
239
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 """
252
253 ConfigClass = VarianceConfig
254
255 FAILURE_BAD_CENTROID = 1
256 """Denotes failures due to bad centroiding (`int`).
257 """
258
259 FAILURE_EMPTY_FOOTPRINT = 2
260 """Denotes failures due to a lack of usable pixels (`int`).
261 """
262
263 @classmethod
265 return BasePlugin.FLUX_ORDER
266
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")
272
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"))
277
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",
305
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)
316
317
318SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance")
319"""Single-frame version of `VariancePlugin`.
320"""
321
322ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance")
323"""Forced version of `VariancePlugin`.
324"""
325
326
328 """Configuration for the input image counting plugin.
329 """
330
331
332class InputCountPlugin(GenericPlugin):
333 """Count the number of input images which contributed to a source.
334
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
346
347 Notes
348 -----
349 Information is derived from the image's `~lsst.afw.image.CoaddInputs`.
350 Note these limitation:
351
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 """
357
358 ConfigClass = InputCountConfig
359
360 FAILURE_BAD_CENTROID = 1
361 """Denotes failures due to bad centroiding (`int`).
362 """
363
364 FAILURE_NO_INPUTS = 2
365 """Denotes failures due to the image not having coadd inputs. (`int`)
366 """
367
368 @classmethod
370 return BasePlugin.SHAPE_ORDER
371
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"))
383
384 def measure(self, measRecord, exposure, center):
385 if not 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)
389
390 ccds = exposure.getInfo().getCoaddInputs().ccds
391 measRecord.set(self.numberKey, len(ccds.subsetContaining(center, exposure.getWcs())))
392
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)
400
401
402SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount")
403"""Single-frame version of `InputCoutPlugin`.
404"""
405
406ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount")
407"""Forced version of `InputCoutPlugin`.
408"""
409
410
412 """Configuration for the variance calculation plugin.
413 """
414
415
416class EvaluateLocalPhotoCalibPlugin(GenericPlugin):
417 """Evaluate the local value of the Photometric Calibration in the exposure.
418
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
423
424 @classmethod
426 return BasePlugin.FLUX_ORDER
427
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.")
440
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)
457
458
459SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin(
460 "base_LocalPhotoCalib")
461"""Single-frame version of `EvaluatePhotoCalibPlugin`.
462"""
463
464ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin(
465 "base_LocalPhotoCalib")
466"""Forced version of `EvaluatePhotoCalibPlugin`.
467"""
468
469
471 """Configuration for the variance calculation plugin.
472 """
473
474
475class EvaluateLocalWcsPlugin(GenericPlugin):
476 """Evaluate the local, linear approximation of the Wcs.
477
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()
483
484 @classmethod
486 return BasePlugin.FLUX_ORDER
487
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.")
510
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])
527
528 def makeLocalTransformMatrix(self, wcs, center):
529 """Create a local, linear approximation of the wcs transformation
530 matrix.
531
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.
536
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.
543
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)
558
559
560SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs")
561"""Single-frame version of `EvaluateLocalWcsPlugin`.
562"""
563
564ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs")
565"""Forced version of `EvaluateLocalWcsPlugin`.
566"""
567
568
570 """Configuration for the single frame peak centroiding algorithm.
571 """
572
573
574@register("base_PeakCentroid")
576 """Record the highest peak in a source footprint as its centroid.
577
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.
580
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 """
593
594 ConfigClass = SingleFramePeakCentroidConfig
595
596 @classmethod
598 return cls.CENTROID_ORDER
599
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")
605
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())
610
611 def fail(self, measRecord, error=None):
612 measRecord.set(self.flag, True)
613
614 @staticmethod
616 return SimpleCentroidTransform
617
618
620 """Configuration for the sky coordinates algorithm.
621 """
622
623
624@register("base_SkyCoord")
626 """Record the sky position of an object based on its centroid slot and WCS.
627
628 The position is record in the ``coord`` field, which is part of the
629 `~lsst.afw.table.SourceCatalog` minimal schema.
630
631 Parameters
632 ----------
633 config : `SingleFrameSkyCoordConfig`
634 Plugin configuraion.
635 name : `str`
636 Plugin name.
637 schema : `lsst.afw.table.Schema`
638 The schema for the measurement output catalog. New fields will be
639 added to hold measurements produced by this plugin.
640 metadata : `lsst.daf.base.PropertySet`
641 Plugin metadata that will be attached to the output catalog
642 """
643
644 ConfigClass = SingleFrameSkyCoordConfig
645
646 @classmethod
648 return cls.SHAPE_ORDER
649
650 def measure(self, measRecord, exposure):
651 # There should be a base class method for handling this exception. Put
652 # this on a later ticket. Also, there should be a python Exception of
653 # the appropriate type for this error
654 if not exposure.hasWcs():
655 raise RuntimeError("Wcs not attached to exposure. Required for " + self.name + " algorithm")
656 measRecord.updateCoord(exposure.getWcs())
657
658 def fail(self, measRecord, error=None):
659 # Override fail() to do nothing in the case of an exception: this is
660 # not ideal, but we don't have a place to put failures because we
661 # don't allocate any fields. Should consider fixing as part of
662 # DM-1011
663 pass
664
665
666class SingleFrameClassificationSizeExtendednessConfig(SingleFramePluginConfig):
667 """Configuration for moments-based star-galaxy classifier."""
668
669 exponent = lsst.pex.config.Field[float](
670 doc="Exponent to raise the PSF size squared (Ixx + Iyy) to, "
671 "in the likelihood normalization",
672 default=0.5,
673 )
674
675
676@register("base_ClassificationSizeExtendedness")
678 """Classify objects by comparing their moments-based trace radius to PSF's.
679
680 The plugin computes chi^2 as ((T_obj - T_psf)/T_psf^exponent)^2, where
681 T_obj is the sum of Ixx and Iyy moments of the object, and T_psf is the
682 sum of Ixx and Iyy moments of the PSF. The exponent is configurable.
683 The measure of being a galaxy is then 1 - exp(-0.5*chi^2).
684
685 Parameters
686 ----------
687 config : `SingleFrameClassificationSizeExtendednessConfig`
688 Plugin configuration.
689 name : `str`
690 Plugin name.
691 schema : `~lsst.afw.table.Schema`
692 The schema for the measurement output catalog. New fields will be
693 added to hold measurements produced by this plugin.
694 metadata : `~lsst.daf.base.PropertySet`
695 Plugin metadata that will be attached to the output catalog.
696
697 Notes
698 -----
699 The ``measure`` method of the plugin requires a value for the ``exposure``
700 argument to maintain consistent API, but it is not used in the measurement.
701 """
702
703 ConfigClass = SingleFrameClassificationSizeExtendednessConfig
704
705 FAILURE_BAD_SHAPE = 1
706 """Denotes failures due to bad shape (`int`).
707 """
708
709 @classmethod
711 return cls.FLUX_ORDER
712
713 def __init__(self, config, name, schema, metadata):
714 SingleFramePlugin.__init__(self, config, name, schema, metadata)
715 self.key = schema.addField(name + "_value",
716 type="D",
717 doc="Measure of being a galaxy based on trace of second order moments",
718 )
719 self.flag = schema.addField(name + "_flag", type="Flag", doc="Moments-based classification failed")
720
721 def measure(self, measRecord, exposure) -> None:
722 # Docstring inherited.
723
724 if measRecord.getShapeFlag():
725 raise MeasurementError(
726 "Shape flag is set. Required for " + self.name + " algorithm",
728 )
729
730 shape = measRecord.getShape()
731 psf_shape = measRecord.getPsfShape()
732
733 ixx = shape.getIxx()
734 iyy = shape.getIyy()
735 ixx_psf = psf_shape.getIxx()
736 iyy_psf = psf_shape.getIyy()
737
738 object_t = ixx + iyy
739 psf_t = ixx_psf + iyy_psf
740
741 chi_sq = ((object_t - psf_t)/(psf_t**self.config.exponent))**2.
742 likelihood = 1. - np.exp(-0.5*chi_sq)
743 measRecord.set(self.key, likelihood)
744
745 def fail(self, measRecord, error=None) -> None:
746 # Docstring inherited.
747 measRecord.set(self.key, np.nan)
748 measRecord.set(self.flag, True)
749
750
751@deprecated(reason="Use SingleFrameClassificationSizeExtendednessConfig instead", version="v29.0.0",
752 category=FutureWarning)
754 pass
755
756
757@deprecated(reason="Use SingleFrameClassificationSizeExtendednessPlugin instead", version="v29.0.0",
758 category=FutureWarning)
760 ConfigClass = SingleFrameMomentsClassifierConfig
761
762
764 """Configuration for the forced peak centroid algorithm.
765 """
766
767
768@register("base_PeakCentroid")
770 """Record the highest peak in a source footprint as its centroid.
771
772 This is of course a relatively poor measure of the true centroid of the
773 object; this algorithm is provided mostly for testing and debugging.
774
775 This is similar to `SingleFramePeakCentroidPlugin`, except that transforms
776 the peak coordinate from the original (reference) coordinate system to the
777 coordinate system of the exposure being measured.
778
779 Parameters
780 ----------
781 config : `ForcedPeakCentroidConfig`
782 Plugin configuraion.
783 name : `str`
784 Plugin name.
785 schemaMapper : `lsst.afw.table.SchemaMapper`
786 A mapping from reference catalog fields to output
787 catalog fields. Output fields are added to the output schema.
788 metadata : `lsst.daf.base.PropertySet`
789 Plugin metadata that will be attached to the output catalog.
790 """
791
792 ConfigClass = ForcedPeakCentroidConfig
793
794 @classmethod
796 return cls.CENTROID_ORDER
797
798 def __init__(self, config, name, schemaMapper, metadata):
799 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
800 schema = schemaMapper.editOutputSchema()
801 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel")
802 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel")
803
804 def measure(self, measRecord, exposure, refRecord, refWcs):
805 targetWcs = exposure.getWcs()
806 peak = refRecord.getFootprint().getPeaks()[0]
807 result = lsst.geom.Point2D(peak.getFx(), peak.getFy())
808 result = targetWcs.skyToPixel(refWcs.pixelToSky(result))
809 measRecord.set(self.keyX, result.getX())
810 measRecord.set(self.keyY, result.getY())
811
812 @staticmethod
814 return SimpleCentroidTransform
815
816
818 """Configuration for the forced transformed centroid algorithm.
819 """
820
821
822@register("base_TransformedCentroid")
824 """Record the transformation of the reference catalog centroid.
825
826 The centroid recorded in the reference catalog is tranformed to the
827 measurement coordinate system and stored.
828
829 Parameters
830 ----------
831 config : `ForcedTransformedCentroidConfig`
832 Plugin configuration
833 name : `str`
834 Plugin name
835 schemaMapper : `lsst.afw.table.SchemaMapper`
836 A mapping from reference catalog fields to output
837 catalog fields. Output fields are added to the output schema.
838 metadata : `lsst.daf.base.PropertySet`
839 Plugin metadata that will be attached to the output catalog.
840
841 Notes
842 -----
843 This is used as the slot centroid by default in forced measurement,
844 allowing subsequent measurements to simply refer to the slot value just as
845 they would in single-frame measurement.
846 """
847
848 ConfigClass = ForcedTransformedCentroidConfig
849
850 @classmethod
852 return cls.CENTROID_ORDER
853
854 def __init__(self, config, name, schemaMapper, metadata):
855 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
856 schema = schemaMapper.editOutputSchema()
857 # Allocate x and y fields, join these into a single FunctorKey for
858 # ease-of-use.
859 xKey = schema.addField(name + "_x", type="D", doc="transformed reference centroid column",
860 units="pixel")
861 yKey = schema.addField(name + "_y", type="D", doc="transformed reference centroid row",
862 units="pixel")
864 # Because we're taking the reference position as given, we don't bother
865 # transforming its uncertainty and reporting that here, so there are no
866 # sigma or cov fields. We do propagate the flag field, if it exists.
867 if "slot_Centroid_flag" in schemaMapper.getInputSchema():
868 self.flagKey = schema.addField(name + "_flag", type="Flag",
869 doc="whether the reference centroid is marked as bad")
870 else:
871 self.flagKey = None
872
873 def measure(self, measRecord, exposure, refRecord, refWcs):
874 targetWcs = exposure.getWcs()
875 if not refWcs == targetWcs:
876 targetPos = targetWcs.skyToPixel(refWcs.pixelToSky(refRecord.getCentroid()))
877 measRecord.set(self.centroidKey, targetPos)
878 else:
879 measRecord.set(self.centroidKey, refRecord.getCentroid())
880 if self.flagKey is not None:
881 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
882
883
885 """Configuration for the forced transformed coord algorithm.
886 """
887
888
889@register("base_TransformedCentroidFromCoord")
891 """Record the transformation of the reference catalog coord.
892
893 The coord recorded in the reference catalog is tranformed to the
894 measurement coordinate system and stored.
895
896 Parameters
897 ----------
898 config : `ForcedTransformedCentroidFromCoordConfig`
899 Plugin configuration
900 name : `str`
901 Plugin name
902 schemaMapper : `lsst.afw.table.SchemaMapper`
903 A mapping from reference catalog fields to output
904 catalog fields. Output fields are added to the output schema.
905 metadata : `lsst.daf.base.PropertySet`
906 Plugin metadata that will be attached to the output catalog.
907
908 Notes
909 -----
910 This can be used as the slot centroid in forced measurement when only a
911 reference coord exist, allowing subsequent measurements to simply refer to
912 the slot value just as they would in single-frame measurement.
913 """
914
915 ConfigClass = ForcedTransformedCentroidFromCoordConfig
916
917 def measure(self, measRecord, exposure, refRecord, refWcs):
918 targetWcs = exposure.getWcs()
919
920 targetPos = targetWcs.skyToPixel(refRecord.getCoord())
921 measRecord.set(self.centroidKey, targetPos)
922
923 if self.flagKey is not None:
924 measRecord.set(self.flagKey, refRecord.getCentroidFlag())
925
926
928 """Configuration for the forced transformed shape algorithm.
929 """
930
931
932@register("base_TransformedShape")
934 """Record the transformation of the reference catalog shape.
935
936 The shape recorded in the reference catalog is tranformed to the
937 measurement coordinate system and stored.
938
939 Parameters
940 ----------
941 config : `ForcedTransformedShapeConfig`
942 Plugin configuration
943 name : `str`
944 Plugin name
945 schemaMapper : `lsst.afw.table.SchemaMapper`
946 A mapping from reference catalog fields to output
947 catalog fields. Output fields are added to the output schema.
948 metadata : `lsst.daf.base.PropertySet`
949 Plugin metadata that will be attached to the output catalog.
950
951 Notes
952 -----
953 This is used as the slot shape by default in forced measurement, allowing
954 subsequent measurements to simply refer to the slot value just as they
955 would in single-frame measurement.
956 """
957
958 ConfigClass = ForcedTransformedShapeConfig
959
960 @classmethod
962 return cls.SHAPE_ORDER
963
964 def __init__(self, config, name, schemaMapper, metadata):
965 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata)
966 schema = schemaMapper.editOutputSchema()
967 # Allocate xx, yy, xy fields, join these into a single FunctorKey for
968 # ease-of-use.
969 xxKey = schema.addField(name + "_xx", type="D", doc="transformed reference shape x^2 moment",
970 units="pixel^2")
971 yyKey = schema.addField(name + "_yy", type="D", doc="transformed reference shape y^2 moment",
972 units="pixel^2")
973 xyKey = schema.addField(name + "_xy", type="D", doc="transformed reference shape xy moment",
974 units="pixel^2")
975 self.shapeKey = lsst.afw.table.QuadrupoleKey(xxKey, yyKey, xyKey)
976 # Because we're taking the reference position as given, we don't bother
977 # transforming its uncertainty and reporting that here, so there are no
978 # sigma or cov fields. We do propagate the flag field, if it exists.
979 if "slot_Shape_flag" in schemaMapper.getInputSchema():
980 self.flagKey = schema.addField(name + "_flag", type="Flag",
981 doc="whether the reference shape is marked as bad")
982 else:
983 self.flagKey = None
984
985 def measure(self, measRecord, exposure, refRecord, refWcs):
986 targetWcs = exposure.getWcs()
987 if not refWcs == targetWcs:
988 fullTransform = lsst.afw.geom.makeWcsPairTransform(refWcs, targetWcs)
989 localTransform = lsst.afw.geom.linearizeTransform(fullTransform, refRecord.getCentroid())
990 measRecord.set(self.shapeKey, refRecord.getShape().transform(localTransform.getLinear()))
991 else:
992 measRecord.set(self.shapeKey, refRecord.getShape())
993 if self.flagKey is not None:
994 measRecord.set(self.flagKey, refRecord.getShapeFlag())
Class to describe the properties of a detected object from an image.
Definition Footprint.h:63
static std::shared_ptr< geom::SpanSet > fromShape(int r, Stencil s=Stencil::CIRCLE, lsst::geom::Point2I offset=lsst::geom::Point2I())
Factory function for creating SpanSets from a Stencil.
Definition SpanSet.cc:688
An ellipse defined by an arbitrary BaseCore and a center point.
Definition Ellipse.h:51
A FunctorKey used to get or set a geom::ellipses::Quadrupole from a tuple of constituent Keys.
Definition aggregates.h:369
Exception to be thrown when a measurement algorithm experiences a known failure mode.
Definition exceptions.h:48
__init__(self, config, name, schema, metadata)
Definition plugins.py:428
measure(self, measRecord, exposure, center)
Definition plugins.py:441
__init__(self, config, name, schema, metadata)
Definition plugins.py:488
measure(self, measRecord, exposure, center)
Definition plugins.py:511
measure(self, measRecord, exposure, refRecord, refWcs)
Definition plugins.py:804
__init__(self, config, name, schemaMapper, metadata)
Definition plugins.py:798
measure(self, measRecord, exposure, refRecord, refWcs)
Definition plugins.py:917
__init__(self, config, name, schemaMapper, metadata)
Definition plugins.py:854
measure(self, measRecord, exposure, refRecord, refWcs)
Definition plugins.py:873
measure(self, measRecord, exposure, refRecord, refWcs)
Definition plugins.py:985
__init__(self, config, name, schemaMapper, metadata)
Definition plugins.py:964
__init__(self, config, name, schema, metadata)
Definition plugins.py:372
fail(self, measRecord, error=None)
Definition plugins.py:393
measure(self, measRecord, exposure, center)
Definition plugins.py:384
__init__(self, config, name, schema, metadata)
Definition plugins.py:149
__init__(self, config, name, schema, metadata)
Definition plugins.py:203
__init__(self, config, name, schema, metadata)
Definition plugins.py:600
__init__(self, config, name, schema, metadata)
Definition plugins.py:267
fail(self, measRecord, error=None)
Definition plugins.py:306
measure(self, measRecord, exposure, center)
Definition plugins.py:278
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=nullptr)
Create a HeavyFootprint with footprint defined by the given Footprint and pixel values from the given...
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition SkyWcs.cc:521
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
A Transform obtained by putting two SkyWcs objects "back to back".
Definition SkyWcs.cc:146
lsst::geom::AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, lsst::geom::Point2D const &inPoint)
Approximate a Transform by its local linearization.
register(name, shouldApCorr=False, apCorrList=())
wrapSimpleAlgorithm(AlgClass, executionOrder, name=None, needsMetadata=False, hasMeasureN=False, hasLogName=False, deprecated=None, **kwds)
Definition wrappers.py:405
void wrapTransform(WrapperCollection &)
Definition transform.cc:37