LSST Applications g00274db5b6+edbf708997,g00d0e8bbd7+edbf708997,g199a45376c+5137f08352,g1fd858c14a+1d4b6db739,g262e1987ae+f4d9505c4f,g29ae962dfc+7156fb1a53,g2cef7863aa+73c82f25e4,g35bb328faa+edbf708997,g3e17d7035e+5b3adc59f5,g3fd5ace14f+852fa6fbcb,g47891489e3+6dc8069a4c,g53246c7159+edbf708997,g64539dfbff+9f17e571f4,g67b6fd64d1+6dc8069a4c,g74acd417e5+ae494d68d9,g786e29fd12+af89c03590,g7ae74a0b1c+a25e60b391,g7aefaa3e3d+536efcc10a,g7cc15d900a+d121454f8d,g87389fa792+a4172ec7da,g89139ef638+6dc8069a4c,g8d7436a09f+28c28d8d6d,g8ea07a8fe4+db21c37724,g92c671f44c+9f17e571f4,g98df359435+b2e6376b13,g99af87f6a8+b0f4ad7b8d,gac66b60396+966efe6077,gb88ae4c679+7dec8f19df,gbaa8f7a6c5+38b34f4976,gbf99507273+edbf708997,gc24b5d6ed1+9f17e571f4,gca7fc764a6+6dc8069a4c,gcc769fe2a4+97d0256649,gd7ef33dd92+6dc8069a4c,gdab6d2f7ff+ae494d68d9,gdbb4c4dda9+9f17e571f4,ge410e46f29+6dc8069a4c,geaed405ab2+e194be0d2b,w.2025.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
baseMeasurement.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"""Base measurement task, which subclassed by the single frame and forced
23measurement tasks.
24"""
25import warnings
26
27import lsst.pipe.base
28import lsst.pex.config
29
30from .pluginRegistry import PluginMap
31from ._measBaseLib import FatalAlgorithmError, MeasurementError
32from lsst.afw.detection import InvalidPsfError
33from .pluginsBase import BasePluginConfig, BasePlugin
34from .noiseReplacer import NoiseReplacerConfig, NoiseReplacer, DummyNoiseReplacer
35
36__all__ = ("BaseMeasurementPluginConfig", "BaseMeasurementPlugin",
37 "BaseMeasurementConfig", "BaseMeasurementTask")
38
39# Exceptions that the measurement tasks should always propagate up to their
40# callers
41FATAL_EXCEPTIONS = (MemoryError, FatalAlgorithmError)
42
43
45 """Base config class for all measurement plugins.
46
47 Notes
48 -----
49 Most derived classes will want to override `setDefaults` in order to
50 customize the default `executionOrder`.
51
52 A derived class whose corresponding Plugin class implements a do `measureN`
53 method should additionally add a bool `doMeasureN` field to replace the
54 bool class attribute defined here.
55 """
56
57 doMeasure = lsst.pex.config.Field(dtype=bool, default=True,
58 doc="whether to run this plugin in single-object mode")
59
60 doMeasureN = False # replace this class attribute with a Field if measureN-capable
61
62
64 """Base class for all measurement plugins.
65
66 Notes
67 -----
68 This is class is a placeholder for future behavior which will be shared
69 only between measurement plugins and is implemented for symmetry with the
70 measurement base plugin configuration class
71 """
72
73 pass
74
75
77 """Assign named plugins to measurement slots.
78
79 Slot configuration which assigns a particular named plugin to each of a set
80 of slots. Each slot allows a type of measurement to be fetched from the
81 `lsst.afw.table.SourceTable` without knowing which algorithm was used to
82 produced the data.
83
84 Notes
85 -----
86 The default algorithm for each slot must be registered, even if the default
87 is not used.
88 """
89
91 centroid = Field(dtype=str, default="base_SdssCentroid", optional=True,
92 doc="the name of the centroiding algorithm used to set source x,y")
93 shape = Field(dtype=str, default="base_SdssShape", optional=True,
94 doc="the name of the algorithm used to set source moments parameters")
95 psfShape = Field(dtype=str, default="base_SdssShape_psf", optional=True,
96 doc="the name of the algorithm used to set PSF moments parameters")
97 apFlux = Field(dtype=str, default="base_CircularApertureFlux_12_0", optional=True,
98 doc="the name of the algorithm used to set the source aperture instFlux slot")
99 modelFlux = Field(dtype=str, default="base_GaussianFlux", optional=True,
100 doc="the name of the algorithm used to set the source model instFlux slot")
101 psfFlux = Field(dtype=str, default="base_PsfFlux", optional=True,
102 doc="the name of the algorithm used to set the source psf instFlux slot")
103 gaussianFlux = Field(dtype=str, default="base_GaussianFlux", optional=True,
104 doc="the name of the algorithm used to set the source Gaussian instFlux slot")
105 calibFlux = Field(dtype=str, default="base_CircularApertureFlux_12_0", optional=True,
106 doc="the name of the instFlux measurement algorithm used for calibration")
107
108 def setupSchema(self, schema):
109 """Set up a slots in a schema following configuration directives.
110
111 Parameters
112 ----------
113 schema : `lsst.afw.table.Schema`
114 The schema in which slots will be set up.
115
116 Notes
117 -----
118 This is defined in this configuration class to support use in unit
119 tests without needing to construct an `lsst.pipe.base.Task` object.
120 """
121 aliases = schema.getAliasMap()
122 if self.centroid is not None:
123 aliases.set("slot_Centroid", self.centroid)
124 if self.shape is not None:
125 aliases.set("slot_Shape", self.shape)
126 if self.psfShape is not None:
127 aliases.set("slot_PsfShape", self.psfShape)
128 if self.apFlux is not None:
129 aliases.set("slot_ApFlux", self.apFlux)
130 if self.modelFlux is not None:
131 aliases.set("slot_ModelFlux", self.modelFlux)
132 if self.psfFlux is not None:
133 aliases.set("slot_PsfFlux", self.psfFlux)
134 if self.gaussianFlux is not None:
135 aliases.set("slot_GaussianFlux", self.gaussianFlux)
136 if self.calibFlux is not None:
137 aliases.set("slot_CalibFlux", self.calibFlux)
138
139
141 """Base configuration for all measurement driver tasks."""
143 dtype=SourceSlotConfig,
144 doc="Mapping from algorithms to special column aliases."
145 )
146
147 def validate(self):
148 super().validate()
149 if self.slots.centroid is not None and self.slots.centroid not in self.plugins.names:
151 self.__class__.slots,
152 self,
153 "source centroid slot algorithm is not being run."
154 )
155 if self.slots.shape is not None and self.slots.shape not in self.plugins.names:
157 self.__class__.slots,
158 self,
159 "source shape slot algorithm '%s' is not being run." % self.slots.shape
160 )
161 for slot in (self.slots.psfFlux, self.slots.apFlux, self.slots.modelFlux,
162 self.slots.gaussianFlux, self.slots.calibFlux):
163 if slot is not None:
164 for name in self.plugins.names:
165 if len(name) <= len(slot) and name == slot[:len(name)]:
166 break
167 else:
169 self.__class__.slots,
170 self,
171 f"Source instFlux algorithm '{slot}' is not being run, required from "
172 f"non-None slots in: {self.slots}."
173 )
174
175
177 """Base configuration for all measurement driver tasks except
178 SimpleForcedMeasurementTask.
179
180 Parameters
181 ----------
182 ignoreSlotPluginChecks : `bool`, optional
183 Do not check that all slots have an associated plugin to run when
184 validating this config. This is primarily for tests that were written
185 before we made Tasks always call `config.validate()` on init.
186 DEPRECATED DM-35949: this is a temporary workaround while we better
187 define how config/schema validation works for measurement tasks.
188
189 Examples
190 --------
191 Subclasses should define the 'plugins' and 'undeblended' registries, e.g.
192
193 .. code-block:: py
194
195 plugins = PluginBaseClass.registry.makeField(
196 multi=True,
197 default=[],
198 doc="Plugins to be run and their configuration"
199 )
200 undeblended = PluginBaseClass.registry.makeField(
201 multi=True,
202 default=[],
203 doc="Plugins to run on undeblended image"
204 )
205
206 where ``PluginBaseClass`` is the appropriate base class of the plugin
207 (e.g., `SingleFramePlugin` or `ForcedPlugin`).
208 """
209 def __new__(cls, *args, ignoreSlotPluginChecks=False, **kwargs):
210 instance = super().__new__(cls, *args, **kwargs)
211 if ignoreSlotPluginChecks:
212 msg = ("ignoreSlotPluginChecks is deprecated and should only be used in tests."
213 " No removal date has been set; see DM-35949.")
214 warnings.warn(msg, category=FutureWarning, stacklevel=2)
215 object.__setattr__(instance, "_ignoreSlotPluginChecks", ignoreSlotPluginChecks)
216 return instance
217
218 doReplaceWithNoise = lsst.pex.config.Field(
219 dtype=bool, default=True, optional=False,
220 doc='When measuring, replace other detected footprints with noise?')
221
223 dtype=NoiseReplacerConfig,
224 doc="configuration that sets how to replace neighboring sources with noise"
225 )
226 undeblendedPrefix = lsst.pex.config.Field(
227 dtype=str, default="undeblended_",
228 doc="Prefix to give undeblended plugins"
229 )
230
231 def validate(self):
233 return
234 super().validate()
235
236
237class SimpleBaseMeasurementTask(lsst.pipe.base.Task):
238 """Ultimate base class for all measurement tasks.
239
240 Parameters
241 ----------
242 algMetadata : `lsst.daf.base.PropertyList` or `None`
243 Will be modified in-place to contain metadata about the plugins being
244 run. If `None`, an empty `~lsst.daf.base.PropertyList` will be
245 created.
246 **kwds
247 Additional arguments passed to `lsst.pipe.base.Task.__init__`.
248
249 Notes
250 -----
251 This base class was created after `BaseMeasurementTask` already existed
252 to add a common base class for `SimpleForcedMeasurementTask`,
253 `SingleFrameMeasurementTask`, and `ForcedMeasurementTask` without
254 breaking downstream code. It is not intended to be used directly,
255 but rather to be subclassed by those tasks.
256 """
257
258 ConfigClass = SimpleBaseMeasurementConfig
259 _DefaultName = "measurement"
260
261 plugins = None
262 """Plugins to be invoked (`PluginMap`).
263
264 Initially empty, this will be populated as plugins are initialized. It
265 should be considered read-only.
266 """
267
268 algMetadata = None
269 """Metadata about active plugins (`lsst.daf.base.PropertyList`).
270
271 Contains additional information about active plugins to be saved with
272 the output catalog. Will be filled by subclasses.
273 """
274
275 def __init__(self, algMetadata=None, **kwds):
276 super().__init__(**kwds)
277 self.plugins = PluginMap()
278 if algMetadata is None:
279 algMetadata = lsst.daf.base.PropertyList()
280 self.algMetadata = algMetadata
281
282 def initializePlugins(self, **kwds):
283 """Initialize plugins (and slots) according to configuration.
284
285 Parameters
286 ----------
287 **kwds
288 Keyword arguments forwarded directly to plugin constructors.
289
290 Notes
291 -----
292 Derived class constructors should call this method to fill the
293 `plugins` attribute and add corresponding output fields and slot
294 aliases to the output schema.
295
296 In addition to the attributes added by `BaseMeasurementTask.__init__`,
297 a ``schema``` attribute holding the output schema must be present
298 before this method is called.
299
300 Keyword arguments are forwarded directly to plugin constructors,
301 allowing derived classes to use plugins with different signatures.
302 """
303 # Make a place at the beginning for the centroid plugin to run first
304 # (because it's an OrderedDict, adding an empty element in advance
305 # means it will get run first when it's reassigned to the actual
306 # Plugin).
307 if self.config.slots.centroid is not None:
308 self.plugins[self.config.slots.centroid] = None
309 # Init the plugins, sorted by execution order. At the same time add to
310 # the schema
311 for executionOrder, name, config, PluginClass in sorted(self.config.plugins.apply()):
312 # Pass logName to the plugin if the plugin is marked as using it
313 # The task will use this name to log plugin errors, regardless.
314 if getattr(PluginClass, "hasLogName", False):
315 self.plugins[name] = PluginClass(config, name, metadata=self.algMetadata,
316 logName=self.log.getChild(name).name, **kwds)
317 else:
318 self.plugins[name] = PluginClass(config, name, metadata=self.algMetadata, **kwds)
319
320 # In rare circumstances (usually tests), the centroid slot not be
321 # coming from an algorithm, which means we'll have added something we
322 # don't want to the plugins map, and we should remove it.
323 if self.config.slots.centroid is not None and self.plugins[self.config.slots.centroid] is None:
324 del self.plugins[self.config.slots.centroid]
325
326 def addInvalidPsfFlag(self, schema):
327 invalidPsfName = "base_InvalidPsf_flag"
328 if invalidPsfName in schema:
329 self.keyInvalidPsf = schema.find(invalidPsfName).key
330 else:
331 self.keyInvalidPsf = schema.addField(
332 invalidPsfName,
333 type="Flag",
334 doc="Invalid PSF at this location.",
335 )
336
337 def callMeasure(self, measRecord, *args, **kwds):
338 """Call ``measure`` on all plugins and consistently handle exceptions.
339
340 Parameters
341 ----------
342 measRecord : `lsst.afw.table.SourceRecord`
343 The record corresponding to the object being measured. Will be
344 updated in-place with the results of measurement.
345 *args
346 Positional arguments forwarded to ``plugin.measure``
347 **kwds
348 Keyword arguments. Two are handled locally:
349
350 beginOrder : `int`
351 Beginning execution order (inclusive). Measurements with
352 ``executionOrder`` < ``beginOrder`` are not executed. `None`
353 for no limit.
354
355 endOrder : `int`
356 Ending execution order (exclusive). Measurements with
357 ``executionOrder`` >= ``endOrder`` are not executed. `None`
358 for no limit.
359
360 Others are forwarded to ``plugin.measure()``.
361
362 Notes
363 -----
364 This method can be used with plugins that have different signatures;
365 the only requirement is that ``measRecord`` be the first argument.
366 Subsequent positional arguments and keyword arguments are forwarded
367 directly to the plugin.
368
369 This method should be considered "protected": it is intended for use by
370 derived classes, not users.
371 """
372 beginOrder = kwds.pop("beginOrder", None)
373 endOrder = kwds.pop("endOrder", None)
374 for plugin in self.plugins.iter():
375 if beginOrder is not None and plugin.getExecutionOrder() < beginOrder:
376 continue
377 if endOrder is not None and plugin.getExecutionOrder() >= endOrder:
378 break
379 self.doMeasurement(plugin, measRecord, *args, **kwds)
380
381 def doMeasurement(self, plugin, measRecord, *args, **kwds):
382 """Call ``measure`` on the specified plugin.
383
384 Exceptions are handled in a consistent way.
385
386 Parameters
387 ----------
388 plugin : subclass of `BasePlugin`
389 Plugin that will be executed.
390 measRecord : `lsst.afw.table.SourceRecord`
391 The record corresponding to the object being measured. Will be
392 updated in-place with the results of measurement.
393 *args
394 Positional arguments forwarded to ``plugin.measure()``.
395 **kwds
396 Keyword arguments forwarded to ``plugin.measure()``.
397
398 Notes
399 -----
400 This method can be used with plugins that have different signatures;
401 the only requirement is that ``plugin`` and ``measRecord`` be the first
402 two arguments. Subsequent positional arguments and keyword arguments
403 are forwarded directly to the plugin.
404
405 This method should be considered "protected": it is intended for use by
406 derived classes, not users.
407 """
408 try:
409 plugin.measure(measRecord, *args, **kwds)
410 except FATAL_EXCEPTIONS:
411 raise
412 except MeasurementError as error:
413 self.log.getChild(plugin.name).debug(
414 "MeasurementError in %s.measure on record %s: %s",
415 plugin.name, measRecord.getId(), error)
416 plugin.fail(measRecord, error)
417 except InvalidPsfError as error:
418 self.log.getChild(plugin.name).debug(
419 "InvalidPsfError in %s.measure on record %s: %s",
420 plugin.name, measRecord.getId(), error)
421 measRecord.set(self.keyInvalidPsf, True)
422 plugin.fail(measRecord)
423 except Exception as error:
424 self.log.getChild(plugin.name).warning(
425 "Exception in %s.measure on record %s: %s",
426 plugin.name, measRecord.getId(), error)
427 plugin.fail(measRecord)
428
429
431 """Ultimate base class for all measurement tasks
432 other than SimpleForcedMeasurementTask.
433
434 Parameters
435 ----------
436 algMetadata : `lsst.daf.base.PropertyList` or `None`
437 Will be modified in-place to contain metadata about the plugins being
438 run. If `None`, an empty `~lsst.daf.base.PropertyList` will be
439 created.
440 **kwds
441 Additional arguments passed to `lsst.pipe.base.Task.__init__`.
442
443 Notes
444 -----
445 This base class for `SingleFrameMeasurementTask` and
446 `ForcedMeasurementTask` mostly exists to share code between the two, and
447 generally should not be used directly.
448 """
449
450 ConfigClass = BaseMeasurementConfig
451
452 NOISE_SEED_MULTIPLIER = "NOISE_SEED_MULTIPLIER"
453 """Name by which the noise seed multiplier is recorded in metadata ('str').
454 """
455
456 NOISE_SOURCE = "NOISE_SOURCE"
457 """Name by which the noise source is recorded in metadata ('str').
458 """
459
460 NOISE_OFFSET = "NOISE_OFFSET"
461 """Name by which the noise offset is recorded in metadata ('str').
462 """
463
464 NOISE_EXPOSURE_ID = "NOISE_EXPOSURE_ID"
465 """Name by which the noise exposire ID is recorded in metadata ('str').
466 """
467
468 def __init__(self, algMetadata=None, **kwds):
469 super().__init__(algMetadata=algMetadata, **kwds)
471
472 def initializePlugins(self, **kwds):
473 # Docstring inherited.
474 super().initializePlugins(**kwds)
475 for executionOrder, name, config, PluginClass in sorted(self.config.undeblended.apply()):
476 undeblendedName = self.config.undeblendedPrefix + name
477 if getattr(PluginClass, "hasLogName", False):
478 self.undeblendedPlugins[name] = PluginClass(config, undeblendedName,
479 metadata=self.algMetadata,
480 logName=self.log.getChild(undeblendedName).name,
481 **kwds)
482 else:
483 self.undeblendedPlugins[name] = PluginClass(config, undeblendedName,
484 metadata=self.algMetadata, **kwds)
485
486 @staticmethod
488 """Get a set of footprints from a catalog, keyed by id.
489
490 Parameters
491 ----------
492 catalog : `lsst.afw.table.SourceCatalog`
493 Catalog with `lsst.afw.detection.Footprint`s attached.
494
495 Returns
496 -------
497 footprints : `dict` [`int`: (`int`, `lsst.afw.detection.Footprint`)]
498 Dictionary of footprint, keyed by id number, with a tuple of
499 the parent id and footprint.
500 """
501 return {measRecord.getId(): (measRecord.getParent(), measRecord.getFootprint())
502 for measRecord in catalog}
503
504 def initNoiseReplacer(self, exposure, measCat, footprints, exposureId=None, noiseImage=None):
505 """Replace all pixels in the exposure covered by the footprint of
506 ``measRecord`` with noise.
507
508 Parameters
509 ----------
510 exposure : `lsst.afw.image.Exposure`
511 Exposure in which to replace pixels.
512 measCat : `lsst.afw.table.SourceCatalog`
513 Catalog that will be measured.t
514 footprints : `dict` [`int`: (`int`, `lsst.afw.detection.Footprint`)]
515 Dictionary of footprints, keyed by id number, with a tuple of
516 the parent id and footprint.
517 exposureId : `int`, optional
518 Unique identifier for the exposure.
519 noiseImage : `lsst.afw.image.Image` or `None`, optional
520 Image from which to draw noise pixels. If `None`, noise will be
521 drawn from the input exposure.
522 """
523 if self.config.doReplaceWithNoise:
524 noiseReplacer = NoiseReplacer(
525 self.config.noiseReplacer,
526 exposure,
527 footprints,
528 log=self.log,
529 exposureId=exposureId,
530 noiseImage=noiseImage,
531 )
532 algMetadata = measCat.getTable().getMetadata()
533 if algMetadata is not None:
534 algMetadata.addInt(self.NOISE_SEED_MULTIPLIER, self.config.noiseReplacer.noiseSeedMultiplier)
535 algMetadata.addString(self.NOISE_SOURCE, self.config.noiseReplacer.noiseSource)
536 algMetadata.addDouble(self.NOISE_OFFSET, self.config.noiseReplacer.noiseOffset)
537 if exposureId is not None:
538 algMetadata.addLong(self.NOISE_EXPOSURE_ID, exposureId)
539 else:
540 noiseReplacer = DummyNoiseReplacer()
541 return noiseReplacer
Class for storing ordered metadata with comments.
__new__(cls, *args, ignoreSlotPluginChecks=False, **kwargs)
initNoiseReplacer(self, exposure, measCat, footprints, exposureId=None, noiseImage=None)
doMeasurement(self, plugin, measRecord, *args, **kwds)