Coverage for python/lsst/pipe/tasks/multiBand.py: 20%
415 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-14 08:10 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-14 08:10 +0000
1# This file is part of pipe_tasks.
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__all__ = ["DetectCoaddSourcesConfig", "DetectCoaddSourcesTask",
23 "MeasureMergedCoaddSourcesConfig", "MeasureMergedCoaddSourcesTask",
24 "DEEP_COADD_BACKGROUND_DOCSTRING",
25 ]
27import dataclasses
29import astropy.units
30import numpy as np
32from lsst.geom import Extent2I
33from lsst.pipe.base import (
34 AnnotatedPartialOutputsError,
35 Struct,
36 PipelineTask,
37 PipelineTaskConfig,
38 PipelineTaskConnections
39)
40import lsst.pipe.base.connectionTypes as cT
41from lsst.pex.config import Field, ChoiceField, ConfigurableField
42from lsst.cell_coadds import MultipleCellCoadd
43from lsst.images import Mask, get_legacy_deep_coadd_mask_planes
44from lsst.images.cells import CellCoadd
45from lsst.images.fields import field_from_legacy_background
46from lsst.meas.algorithms import (
47 DynamicDetectionTask,
48 ExceedsMaxVarianceScaleError,
49 InsufficientSourcesError,
50 PsfGenerationError,
51 ReferenceObjectLoader,
52 ScaleVarianceTask,
53 SetPrimaryFlagsTask,
54 TooManyMaskedPixelsError,
55 ZeroFootprintError,
56)
57from lsst.meas.base import (
58 SingleFrameMeasurementTask,
59 ApplyApCorrTask,
60 CatalogCalculationTask,
61 SkyMapIdGeneratorConfig,
62)
63from lsst.meas.extensions.scarlet.io import updateCatalogFootprints
64from lsst.meas.astrom import DirectMatchTask, denormalizeMatches
65from lsst.pipe.tasks.propagateSourceFlags import PropagateSourceFlagsTask
66import lsst.afw.image as afwImage
67import lsst.afw.math as afwMath
68import lsst.afw.table as afwTable
69from lsst.daf.base import PropertyList
70from lsst.skymap import BaseSkyMap
72# NOTE: these imports are a convenience so multiband users only have to import this file.
73from .mergeDetections import MergeDetectionsConfig, MergeDetectionsTask # noqa: F401
74from .mergeMeasurements import MergeMeasurementsConfig, MergeMeasurementsTask # noqa: F401
75from .multiBandUtils import CullPeaksConfig # noqa: F401
76from .deblendCoaddSourcesPipeline import DeblendCoaddSourcesMultiConfig # noqa: F401
77from .deblendCoaddSourcesPipeline import DeblendCoaddSourcesMultiTask # noqa: F401
80"""
81New set types:
82* deepCoadd_det: detections from what used to be processCoadd (tract, patch, filter)
83* deepCoadd_mergeDet: merged detections (tract, patch)
84* deepCoadd_meas: measurements of merged detections (tract, patch, filter)
85* deepCoadd_ref: reference sources (tract, patch)
86All of these have associated *_schema catalogs that require no data ID and hold no records.
88In addition, we have a schema-only dataset, which saves the schema for the PeakRecords in
89the mergeDet, meas, and ref dataset Footprints:
90* deepCoadd_peak_schema
91"""
94##############################################################################################################
96# The default for DetectCoaddSourcesConfig.backgroundDescription:
97DEEP_COADD_BACKGROUND_DOCSTRING = (
98 "Background subtracted from the image when generating the Object catalog. "
99 "This intentionally oversubtracts the background to reduce blending and ensure "
100 "scattered light is subtracted. "
101 "Restoring this background does not restore all original backgrounds, "
102 "as the coadd was built from background-subtracted visit images; in most "
103 "cases this background term is actually quite small "
104)
107class DetectCoaddSourcesConnections(PipelineTaskConnections,
108 dimensions=("tract", "patch", "band", "skymap"),
109 defaultTemplates={"inputCoaddName": "deep", "outputCoaddName": "deep"}):
110 detectionSchema = cT.InitOutput(
111 doc="Schema of the detection catalog",
112 name="{outputCoaddName}Coadd_det_schema",
113 storageClass="SourceCatalog",
114 )
115 exposure = cT.Input(
116 doc="Exposure on which detections are to be performed (if useCellCoadd=False). ",
117 name="{inputCoaddName}Coadd",
118 storageClass="ExposureF",
119 dimensions=("tract", "patch", "band", "skymap")
120 )
121 exposure_cells = cT.Input(
122 doc="Exposure on which detections are to be performed (if useCellCoadd=True). ",
123 name="{inputCoaddName}CoaddCell",
124 storageClass="MultipleCellCoadd",
125 dimensions=("tract", "patch", "band", "skymap"),
126 )
127 skyMap = cT.Input(
128 doc="Description of the skymap's tracts and patches.",
129 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
130 storageClass="SkyMap",
131 dimensions=("skymap",),
132 )
133 outputBackgrounds = cT.Output(
134 doc="Output Backgrounds used in detection",
135 name="{outputCoaddName}Coadd_calexp_background",
136 storageClass="Background",
137 dimensions=("tract", "patch", "band", "skymap")
138 )
139 outputSources = cT.Output(
140 doc="Detected sources catalog",
141 name="{outputCoaddName}Coadd_det",
142 storageClass="SourceCatalog",
143 dimensions=("tract", "patch", "band", "skymap")
144 )
145 outputExposure = cT.Output(
146 doc="Exposure post detection",
147 name="{outputCoaddName}Coadd_calexp",
148 storageClass="ExposureF",
149 dimensions=("tract", "patch", "band", "skymap")
150 )
152 def __init__(self, *, config=None):
153 super().__init__(config=config)
154 assert isinstance(config, DetectCoaddSourcesConfig)
156 if config.imageType == "future":
157 if config.useCellCoadds:
158 self.exposure_cells = dataclasses.replace(self.exposure_cells, storageClass="CellCoadd")
159 else:
160 self.exposure = dataclasses.replace(self.exposure, storageClass="CellCoadd")
161 self.outputExposure = dataclasses.replace(self.outputExposure, storageClass="CellCoadd")
162 del self.outputBackgrounds
163 if config.useCellCoadds:
164 del self.exposure
165 else:
166 del self.exposure_cells
167 if not self.config.forceExactBinning:
168 del self.skyMap
169 if self.config.writeOnlyBackgrounds:
170 del self.outputExposure
171 del self.outputSources
172 del self.detectionSchema
175class DetectCoaddSourcesConfig(PipelineTaskConfig, pipelineConnections=DetectCoaddSourcesConnections):
176 """Configuration parameters for the DetectCoaddSourcesTask
177 """
179 doScaleVariance = Field(dtype=bool, default=True, doc="Scale variance plane using empirical noise?")
180 scaleVariance = ConfigurableField(target=ScaleVarianceTask, doc="Variance rescaling")
181 detection = ConfigurableField(target=DynamicDetectionTask, doc="Source detection")
182 coaddName = Field(dtype=str, default="deep", doc="Name of coadd")
183 useCellCoadds = Field(dtype=bool, default=False, doc="Whether to use cell coadds?")
184 hasFakes = Field(
185 dtype=bool,
186 default=False,
187 doc="Should be set to True if fake sources have been inserted into the input data.",
188 )
189 idGenerator = SkyMapIdGeneratorConfig.make_field()
190 forceExactBinning = Field(
191 dtype=bool,
192 default=False,
193 doc=(
194 "Check that the background bin size evenly divides the patch inner region, and "
195 "crop the outer region to an integer number of bins."
196 )
197 )
198 writeOnlyBackgrounds = Field(dtype=bool, default=False, doc="If true, only save the background models.")
199 writeEmptyBackgrounds = Field(
200 dtype=bool,
201 default=True,
202 doc=(
203 "If true, save a placeholder background with NaNs in all bins (but the right geometry) when "
204 "there are no pixels to compute a background from. This can be useful if a later task combines "
205 "backgrounds from multiple patches as input."
206 )
207 )
208 imageType = ChoiceField(
209 "Which image type to use for the input and output coadd. "
210 "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' "
211 "method behavior is determined by which type is actually passed in.",
212 allowed={
213 "legacy": (
214 "Read a lsst.cell_coadds.MultipleCellCoadd (if useCellCoadd) or "
215 "lsst.afw.image.Exposure (if not useCellCoadd) and write an lsst.afw.image.Exposure."
216 ),
217 "future": (
218 "Read and write lsst.images.cells.CellCoadd (useCellCoadd just "
219 "sets which of 'exposure' or 'exposure_cells' will be used for inputs). "
220 "The 'outputBackground' is deleted, writeEmptyBackgrounds is ignored, and "
221 "writeOnlyBackgrounds=True is invalid."
222 ),
223 },
224 dtype=str,
225 optional=False,
226 default="legacy",
227 )
228 backgroundName = Field(
229 "Name of the subtracted background, to be stored with the image when the input and "
230 "output images are lsst.images.cells.CellCoadd.",
231 dtype=str,
232 default="object",
233 )
234 backgroundDescription = Field(
235 "Description of the subtracted background, to be stored with the image when the input and "
236 "output images are lsst.images.cells.CellCoadd.",
237 dtype=str,
238 default=DEEP_COADD_BACKGROUND_DOCSTRING,
239 )
241 def setDefaults(self):
242 super().setDefaults()
243 self.detection.thresholdType = "pixel_stdev"
244 self.detection.isotropicGrow = True
245 # Coadds are made from background-subtracted CCDs, so any background subtraction should be very basic
246 self.detection.reEstimateBackground = False
247 self.detection.background.useApprox = False
248 self.detection.background.binSize = 4096
249 self.detection.background.undersampleStyle = 'REDUCE_INTERP_ORDER'
250 self.detection.doTempWideBackground = True # Suppress large footprints that overwhelm the deblender
251 # Include band in packed data IDs that go into object IDs (None -> "as
252 # many bands as are defined", rather than the default of zero).
253 self.idGenerator.packer.n_bands = None
255 def validate(self):
256 super().validate()
257 if self.imageType == "future":
258 if self.doScaleVariance:
259 raise ValueError("doScaleVariance=True is not compatible with imageType='future'")
260 if self.forceExactBinning:
261 raise ValueError("forceExactBinning=True is not compatible with imageType='future'")
262 if self.writeOnlyBackgrounds:
263 raise ValueError("writeOnlyBackgrounds=True is not compatible with imageType='future'")
266class DetectCoaddSourcesTask(PipelineTask):
267 """Detect sources on a single filter coadd.
269 Coadding individual visits requires each exposure to be warped. This
270 introduces covariance in the noise properties across pixels. Before
271 detection, we correct the coadd variance by scaling the variance plane in
272 the coadd to match the observed variance. This is an approximate
273 approach -- strictly, we should propagate the full covariance matrix --
274 but it is simple and works well in practice.
276 After scaling the variance plane, we detect sources and generate footprints
277 by delegating to the @ref SourceDetectionTask_ "detection" subtask.
279 DetectCoaddSourcesTask is meant to be run after assembling a coadded image
280 in a given band. The purpose of the task is to update the background,
281 detect all sources in a single band and generate a set of parent
282 footprints. Subsequent tasks in the multi-band processing procedure will
283 merge sources across bands and, eventually, perform forced photometry.
285 Parameters
286 ----------
287 schema : `lsst.afw.table.Schema`, optional
288 Initial schema for the output catalog, modified-in place to include all
289 fields set by this task. If None, the source minimal schema will be used.
290 **kwargs
291 Additional keyword arguments.
292 """
294 _DefaultName = "detectCoaddSources"
295 ConfigClass = DetectCoaddSourcesConfig
297 def __init__(self, schema=None, **kwargs):
298 # N.B. Super is used here to handle the multiple inheritance of PipelineTasks, the init tree
299 # call structure has been reviewed carefully to be sure super will work as intended.
300 super().__init__(**kwargs)
301 if schema is None:
302 schema = afwTable.SourceTable.makeMinimalSchema()
303 self.schema = schema
304 self.makeSubtask("detection", schema=self.schema)
305 if self.config.doScaleVariance:
306 self.makeSubtask("scaleVariance")
308 self.detectionSchema = afwTable.SourceCatalog(self.schema)
310 def runQuantum(self, butlerQC, inputRefs, outputRefs):
311 inputs = butlerQC.get(inputRefs)
312 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
314 if self.config.useCellCoadds:
315 multiple_cell_coadd = inputs.pop("exposure_cells")
316 match self.config.imageType:
317 case "legacy":
318 exposure = multiple_cell_coadd.stitch().asExposure()
319 case "future":
320 exposure = multiple_cell_coadd # conversion deferred to run().
321 case _:
322 raise AssertionError(f"Invalid choice {self.config.imageType!r} for imageType.")
323 else:
324 exposure = inputs.pop("exposure")
326 skyMap = inputs.pop("skyMap", None)
327 if skyMap is not None:
328 patchInfo = skyMap[butlerQC.quantum.dataId["tract"]][butlerQC.quantum.dataId["patch"]]
329 else:
330 patchInfo = None
332 assert not inputs, "runQuantum got more inputs than expected."
333 try:
334 outputs = self.run(
335 exposure=exposure,
336 idFactory=idGenerator.make_table_id_factory(),
337 expId=idGenerator.catalog_id,
338 patchInfo=patchInfo,
339 )
340 except (
341 TooManyMaskedPixelsError,
342 ExceedsMaxVarianceScaleError,
343 InsufficientSourcesError,
344 PsfGenerationError,
345 ZeroFootprintError,
346 ) as e:
347 if self.config.writeEmptyBackgrounds:
348 butlerQC.put(self._makeEmptyBackground(exposure, patchInfo), outputRefs.outputBackgrounds)
349 # Detection failed, so clear any leftover detected mask planes.
350 if isinstance(exposure, CellCoadd):
351 # If we passed a CellCoadd in, it won't be modified until 'run'
352 # is about to exit, and hence it can't have picked up a
353 # DETECTED_NEGATIVE plane, which can only be temporary here.
354 # But it might have a preexisting DETECTED plane (i.e. a union
355 # of the DETECTED plane from the warps) that we'd still want to
356 # clear.
357 exposure.mask.clear("DETECTED")
358 else:
359 for maskName in ["DETECTED", "DETECTED_NEGATIVE"]:
360 if maskName in exposure.mask.getMaskPlaneDict().keys():
361 detectedMask = exposure.mask.getMaskPlane(maskName)
362 exposure.mask.clearMaskPlane(detectedMask)
363 error = AnnotatedPartialOutputsError.annotate(
364 e,
365 self,
366 exposure,
367 log=self.log,
368 )
369 butlerQC.put(exposure, outputRefs.outputExposure)
370 raise error from e
372 butlerQC.put(outputs, outputRefs)
374 def run(self, exposure, idFactory, expId, patchInfo=None):
375 """Run detection on an exposure.
377 First scale the variance plane to match the observed variance
378 using ``ScaleVarianceTask``. Then invoke the ``SourceDetectionTask_`` "detection" subtask to
379 detect sources.
381 Parameters
382 ----------
383 exposure : `lsst.afw.image.Exposure` or `lsst.images.cells.CellCoadd`.
384 Exposure on which to detect (may be background-subtracted and scaled,
385 depending on configuration).
386 idFactory : `lsst.afw.table.IdFactory`
387 IdFactory to set source identifiers.
388 expId : `int`
389 Exposure identifier (integer) for RNG seed.
390 patchInfo : `lsst.skymap.PatchInfo`, optional
391 Description of the patch geometry. Only needed if
392 `~DetectCoaddSourceConfig.forceExactBinning` is `True`.
394 Returns
395 -------
396 result : `lsst.pipe.base.Struct`
397 Results as a struct with attributes:
399 ``outputSources``
400 Catalog of detections (`lsst.afw.table.SourceCatalog`).
401 ``outputBackgrounds``
402 List of backgrounds (`list`).
403 ``outputExposure``
404 The background-subtracted coadd image, with its mask plane
405 updated to include detections. This will have the same type
406 as ``exposure``.
407 """
408 cell_coadd = None
409 if isinstance(exposure, CellCoadd):
410 cell_coadd = exposure
411 exposure = cell_coadd.to_legacy()
412 if self.config.forceExactBinning:
413 if cell_coadd is not None:
414 raise ValueError("forceExactBinning=True is not compatible with CellCoadd inputs")
415 exposure = self._cropToExactBinning(exposure, patchInfo)
416 if self.config.doScaleVariance:
417 if cell_coadd is not None:
418 raise ValueError("doScaleVariance=True is not compatible with CellCoadd inputs")
419 varScale = self.scaleVariance.run(exposure.maskedImage)
420 exposure.getMetadata().add("VARIANCE_SCALE", varScale)
421 backgrounds = afwMath.BackgroundList()
422 table = afwTable.SourceTable.make(self.schema, idFactory)
423 detections = self.detection.run(table, exposure, expId=expId)
424 sources = detections.sources
425 if hasattr(detections, "background") and detections.background:
426 for bg in detections.background:
427 backgrounds.append(bg)
428 if len(backgrounds) == 0:
429 # Persist a constant background with value of NaN to get around
430 # inability to persist empty BackgroundList.
431 emptyBg = self._makeEmptyBackground(exposure, patchInfo)
432 backgrounds.append(emptyBg)
433 if cell_coadd is not None:
434 cell_coadd.image.array[...] = exposure.image.array
435 cell_coadd.mask = Mask.from_legacy(
436 exposure.mask, plane_map=get_legacy_deep_coadd_mask_planes()
437 ).view(sky_projection=cell_coadd.sky_projection)
438 if backgrounds:
439 cell_coadd.backgrounds.add(
440 self.config.backgroundName,
441 field_from_legacy_background(backgrounds, unit=astropy.units.nJy),
442 self.config.backgroundDescription,
443 is_subtracted=True,
444 )
445 exposure = cell_coadd
446 return Struct(outputSources=sources, outputBackgrounds=backgrounds, outputExposure=exposure)
448 def _cropToExactBinning(self, exposure, patchInfo):
449 """Crop a coadd `~lsst.afw.image.Exposure` instance to ensure exact
450 background binning.
452 Parameters
453 ----------
454 exposure : `lsst.afw.image.Exposure`
455 Exposure to crop, assumed to cover the patch outer bounding box.
456 patchInfo : `lsst.skymap.PatchInfo`
457 Description of the patch geometry.
459 Returns
460 -------
461 cropped : `lsst.afw.image.Exposure`
462 View of ``exposure`` with background bins that evenly divide both
463 the full cropped image and the patch inner region. The bounding
464 box is guaranteed to contain the patch inner bounding box and be
465 contained by the patch outer bounding box.
467 Raises
468 ------
469 ValueError
470 Raised if the patch inner region width or height is not a multiple
471 of the background bin size.
472 """
473 bbox = patchInfo.getInnerBBox()
474 if bbox.width % self.detection.background.binSizeX:
475 raise ValueError(
476 f"Patch inner width {bbox.width} does not evenly "
477 f"divide bin width {self.detection.background.binSizeX}."
478 )
479 if bbox.height % self.detection.background.binSizeY:
480 raise ValueError(
481 f"Patch inner height {bbox.height} does not evenly "
482 f"divide bin height {self.detection.background.binSizeY}."
483 )
484 outer_bbox = patchInfo.getOuterBBox()
485 n_bins_grow_x = (bbox.x.begin - outer_bbox.x.begin) // self.detection.background.binSizeX
486 n_bins_grow_y = (bbox.y.begin - outer_bbox.y.begin) // self.detection.background.binSizeY
487 bbox.grow(
488 Extent2I(
489 n_bins_grow_x*self.detection.background.binSizeX,
490 n_bins_grow_y*self.detection.background.binSizeY,
491 )
492 )
493 assert outer_bbox.contains(bbox)
494 assert bbox.contains(patchInfo.getInnerBBox())
495 assert bbox.width % self.detection.background.binSizeX == 0
496 assert bbox.height % self.detection.background.binSizeY == 0
497 return exposure[bbox]
499 def _makeEmptyBackground(self, exposure, patchInfo=None):
500 """Construct an empty `lsst.afw.math.BackgroundList` with NaN values.
502 Parameters
503 ----------
504 exposure : `lsst.afw.image.Exposure`
505 Exposure that the background should correspond to.
506 patchInfo : `lsst.skymap.PatchInfo`, optional
507 Description of the patch geometry. Only needed if
508 `~DetectCoaddSourceConfig.forceExactBinning` is `True`.
510 Returns
511 -------
512 background : `lsst.afw.math.BackgroundList`
513 A background object with a single layer and the same bin geometry
514 that a background for that exposure would have had if it had enough
515 usable pixels. This object cannot actually be used for background
516 subtraction.
517 """
518 # Create a backgroundList with one entry whose "stats image" is NaNs
519 # and has all pixels set as NO_DATA.
520 if self.config.forceExactBinning:
521 exposure = self._cropToExactBinning(exposure, patchInfo).clone()
523 bgLevel = np.nan
524 bgStats = afwImage.MaskedImageF(1, 1)
525 bgStats.set(bgLevel, 0, bgLevel)
526 bg = afwMath.BackgroundMI(exposure.getBBox(), bgStats)
527 bgData = (bg, afwMath.Interpolate.LINEAR, afwMath.REDUCE_INTERP_ORDER,
528 afwMath.ApproximateControl.UNKNOWN, 0, 0, False)
529 background = afwMath.BackgroundList()
530 background.append(bgData)
531 for bg, *_ in background:
532 stats = bg.getStatsImage()
533 stats.mask.array[:, :] = stats.mask.getPlaneBitMask("NO_DATA")
534 stats.variance.array[:, :] = 0.0
535 return background
538class MeasureMergedCoaddSourcesConnections(
539 PipelineTaskConnections,
540 dimensions=("tract", "patch", "band", "skymap"),
541 defaultTemplates={
542 "inputCoaddName": "deep",
543 "outputCoaddName": "deep",
544 "deblendedCatalog": "deblendedFlux",
545 },
546 deprecatedTemplates={
547 # TODO[DM-47797]: remove this deprecated connection template.
548 "deblendedCatalog": "Support for old deblender outputs will be removed after v29."
549 },
550):
551 inputSchema = cT.InitInput(
552 doc="Input schema for measure merged task produced by a deblender or detection task",
553 name="{inputCoaddName}Coadd_deblendedFlux_schema",
554 storageClass="SourceCatalog"
555 )
556 outputSchema = cT.InitOutput(
557 doc="Output schema after all new fields are added by task",
558 name="{inputCoaddName}Coadd_meas_schema",
559 storageClass="SourceCatalog"
560 )
561 # TODO[DM-47797]: remove this deprecated connection.
562 refCat = cT.PrerequisiteInput(
563 doc="Reference catalog used to match measured sources against known sources",
564 name="ref_cat",
565 storageClass="SimpleCatalog",
566 dimensions=("skypix",),
567 deferLoad=True,
568 multiple=True,
569 deprecated="Reference matching in measureCoaddSources will be removed after v29.",
570 )
571 exposure = cT.Input(
572 doc="Input non-cell-based coadd image",
573 name="{inputCoaddName}Coadd_calexp",
574 storageClass="ExposureF",
575 dimensions=("tract", "patch", "band", "skymap")
576 )
577 exposure_cells = cT.Input(
578 doc="Input cell-based coadd image",
579 name="{inputCoaddName}CoaddCell",
580 storageClass="MultipleCellCoadd",
581 dimensions=("tract", "patch", "band", "skymap"),
582 )
583 background = cT.Input(
584 doc="Background to subtract from cell-based coadd image",
585 name="{inputCoaddName}Coadd_calexp_background",
586 storageClass="Background",
587 dimensions=("tract", "patch", "band", "skymap")
588 )
589 skyMap = cT.Input(
590 doc="SkyMap to use in processing",
591 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
592 storageClass="SkyMap",
593 dimensions=("skymap",),
594 )
595 # TODO[DM-47424]: remove this deprecated connection.
596 visitCatalogs = cT.Input(
597 doc="Deprecated and unused.",
598 name="src",
599 dimensions=("instrument", "visit", "detector"),
600 storageClass="SourceCatalog",
601 multiple=True,
602 deprecated="Deprecated and unused. Will be removed after v29.",
603 )
604 sourceTableHandles = cT.Input(
605 doc=("Source tables that are derived from the ``CalibrateTask`` sources. "
606 "These tables contain astrometry and photometry flags, and optionally "
607 "PSF flags."),
608 name="sourceTable_visit",
609 storageClass="ArrowAstropy",
610 dimensions=("instrument", "visit"),
611 multiple=True,
612 deferLoad=True,
613 )
614 finalizedSourceTableHandles = cT.Input(
615 doc=("Finalized source tables from ``FinalizeCalibrationTask``. These "
616 "tables contain PSF flags from the finalized PSF estimation."),
617 name="finalized_src_table",
618 storageClass="ArrowAstropy",
619 dimensions=("instrument", "visit"),
620 multiple=True,
621 deferLoad=True,
622 )
623 finalVisitSummaryHandles = cT.Input(
624 doc="Final visit summary table",
625 name="finalVisitSummary",
626 storageClass="ExposureCatalog",
627 dimensions=("instrument", "visit"),
628 multiple=True,
629 deferLoad=True,
630 )
631 # TODO[DM-47797]: remove this deprecated connection.
632 inputCatalog = cT.Input(
633 doc=("Name of the input catalog to use."
634 "If the single band deblender was used this should be 'deblendedFlux."
635 "If the multi-band deblender was used this should be 'deblendedModel, "
636 "or deblendedFlux if the multiband deblender was configured to output "
637 "deblended flux catalogs. If no deblending was performed this should "
638 "be 'mergeDet'"),
639 name="{inputCoaddName}Coadd_{deblendedCatalog}",
640 storageClass="SourceCatalog",
641 deprecated="Support for old deblender outputs will be removed after v29.",
642 dimensions=("tract", "patch", "band", "skymap"),
643 )
644 scarletCatalog = cT.Input(
645 doc="Catalogs produced by multiband deblending",
646 name="{inputCoaddName}Coadd_deblendedCatalog",
647 storageClass="SourceCatalog",
648 dimensions=("tract", "patch", "skymap"),
649 )
650 scarletModels = cT.Input(
651 doc="Multiband scarlet models produced by the deblender",
652 name="{inputCoaddName}Coadd_scarletModelData",
653 storageClass="LsstScarletModelData",
654 dimensions=("tract", "patch", "skymap"),
655 )
656 outputSources = cT.Output(
657 doc="Source catalog containing all the measurement information generated in this task",
658 name="{outputCoaddName}Coadd_meas",
659 dimensions=("tract", "patch", "band", "skymap"),
660 storageClass="SourceCatalog",
661 )
662 # TODO[DM-47797]: remove this deprecated connection.
663 matchResult = cT.Output(
664 doc="Match catalog produced by configured matcher, optional on doMatchSources",
665 name="{outputCoaddName}Coadd_measMatch",
666 dimensions=("tract", "patch", "band", "skymap"),
667 storageClass="Catalog",
668 deprecated="Reference matching in measureCoaddSources will be removed after v29.",
669 )
670 # TODO[DM-47797]: remove this deprecated connection.
671 denormMatches = cT.Output(
672 doc="Denormalized Match catalog produced by configured matcher, optional on "
673 "doWriteMatchesDenormalized",
674 name="{outputCoaddName}Coadd_measMatchFull",
675 dimensions=("tract", "patch", "band", "skymap"),
676 storageClass="Catalog",
677 deprecated="Reference matching in measureCoaddSources will be removed after v29.",
678 )
680 def __init__(self, *, config=None):
681 super().__init__(config=config)
682 del self.visitCatalogs
683 if not config.doPropagateFlags:
684 del self.sourceTableHandles
685 del self.finalizedSourceTableHandles
686 else:
687 # Check for types of flags required.
688 if not config.propagateFlags.source_flags:
689 del self.sourceTableHandles
690 if not config.propagateFlags.finalized_source_flags:
691 del self.finalizedSourceTableHandles
692 # TODO[DM-47797]: only the 'if' block contents here should survive.
693 if config.inputCatalog == "deblendedCatalog":
694 del self.inputCatalog
695 if not config.doAddFootprints:
696 del self.scarletModels
697 else:
698 del self.deblendedCatalog
699 del self.scarletModels
701 # TODO[DM-47797]: delete the conditionals below.
702 if not config.doMatchSources:
703 del self.refCat
704 del self.matchResult
706 if not config.doWriteMatchesDenormalized:
707 del self.denormMatches
709 if self.config.imageType == "future":
710 self.exposure = dataclasses.replace(self.exposure, storageClass="CellCoadd")
711 del self.exposure_cells
712 del self.background
713 elif self.config.useCellCoadds:
714 del self.exposure
715 else:
716 del self.exposure_cells
717 del self.background
720class MeasureMergedCoaddSourcesConfig(PipelineTaskConfig,
721 pipelineConnections=MeasureMergedCoaddSourcesConnections):
722 """Configuration parameters for the MeasureMergedCoaddSourcesTask
723 """
724 inputCatalog = ChoiceField(
725 dtype=str,
726 default="deblendedCatalog",
727 allowed={
728 "deblendedCatalog": "Output catalog from ScarletDeblendTask",
729 "deblendedFlux": "Output catalog from SourceDeblendTask",
730 "mergeDet": "The merged detections before deblending."
731 },
732 doc="The name of the input catalog.",
733 # TODO[DM-47797]: remove this config option and anything using it.
734 deprecated="Support for old deblender outputs will be removed after v29.",
735 )
736 doAddFootprints = Field(dtype=bool,
737 default=True,
738 doc="Whether or not to add footprints to the input catalog from scarlet models. "
739 "This should be true whenever using the multi-band deblender, "
740 "otherwise this should be False.")
741 doConserveFlux = Field(dtype=bool, default=True,
742 doc="Whether to use the deblender models as templates to re-distribute the flux "
743 "from the 'exposure' (True), or to perform measurements on the deblender "
744 "model footprints.")
745 doStripFootprints = Field(dtype=bool, default=True,
746 doc="Whether to strip footprints from the output catalog before "
747 "saving to disk. "
748 "This is usually done when using scarlet models to save disk space.")
749 useCellCoadds = Field(dtype=bool, default=False, doc="Whether to use cell coadds?")
750 measurement = ConfigurableField(target=SingleFrameMeasurementTask, doc="Source measurement")
751 setPrimaryFlags = ConfigurableField(target=SetPrimaryFlagsTask, doc="Set flags for primary tract/patch")
752 doPropagateFlags = Field(
753 dtype=bool, default=True,
754 doc="Whether to match sources to CCD catalogs to propagate flags (to e.g. identify PSF stars)"
755 )
756 propagateFlags = ConfigurableField(target=PropagateSourceFlagsTask, doc="Propagate source flags to coadd")
757 doMatchSources = Field(
758 dtype=bool,
759 default=False,
760 doc="Match sources to reference catalog?",
761 deprecated="Reference matching in measureCoaddSources will be removed after v29.",
762 )
763 match = ConfigurableField(
764 target=DirectMatchTask,
765 doc="Matching to reference catalog",
766 deprecated="Reference matching in measureCoaddSources will be removed after v29.",
767 )
768 doWriteMatchesDenormalized = Field(
769 dtype=bool,
770 default=False,
771 doc=("Write reference matches in denormalized format? "
772 "This format uses more disk space, but is more convenient to read."),
773 deprecated="Reference matching in measureCoaddSources will be removed after v29.",
774 )
775 coaddName = Field(dtype=str, default="deep", doc="Name of coadd")
776 psfCache = Field(dtype=int, default=100, doc="Size of psfCache")
777 checkUnitsParseStrict = Field(
778 doc="Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
779 dtype=str,
780 default="raise",
781 )
782 doApCorr = Field(
783 dtype=bool,
784 default=True,
785 doc="Apply aperture corrections"
786 )
787 applyApCorr = ConfigurableField(
788 target=ApplyApCorrTask,
789 doc="Subtask to apply aperture corrections"
790 )
791 doRunCatalogCalculation = Field(
792 dtype=bool,
793 default=True,
794 doc='Run catalogCalculation task'
795 )
796 catalogCalculation = ConfigurableField(
797 target=CatalogCalculationTask,
798 doc="Subtask to run catalogCalculation plugins on catalog"
799 )
801 hasFakes = Field(
802 dtype=bool,
803 default=False,
804 doc="Should be set to True if fake sources have been inserted into the input data."
805 )
806 idGenerator = SkyMapIdGeneratorConfig.make_field()
807 imageType = ChoiceField(
808 "Which image type to expect for the input coadd. "
809 "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' "
810 "method behavior is determined by which type is actually passed in.",
811 allowed={
812 "legacy": (
813 "Read a lsst.cell_coadds.MultipleCellCoadd via 'exposure_cells` and restore 'background' "
814 "(if useCellCoadd) or lsst.afw.image.Exposure via `exposure` (if not useCellCoadd)."
815 ),
816 "future": (
817 "Read lsst.images.cells.CellCoadd via the 'exposure' connection. useCellCoadd is ignored."
818 ),
819 },
820 dtype=str,
821 optional=False,
822 default="legacy",
823 )
825 @property
826 def refObjLoader(self):
827 return self.match.refObjLoader
829 def setDefaults(self):
830 super().setDefaults()
831 self.measurement.plugins.names |= ['base_InputCount',
832 'base_Variance',
833 'base_LocalPhotoCalib',
834 'base_LocalWcs']
836 # TODO: Remove STREAK in DM-44658, streak masking to happen only in
837 # ip_diffim; if we can propagate the streak mask from diffim, we can
838 # still set flags with it here.
839 self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['CLIPPED', 'SENSOR_EDGE',
840 'INEXACT_PSF']
841 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['CLIPPED', 'SENSOR_EDGE',
842 'INEXACT_PSF']
844 def validate(self):
845 super().validate()
847 if not self.doMatchSources and self.doWriteMatchesDenormalized:
848 raise ValueError("Cannot set doWriteMatchesDenormalized if doMatchSources is False.")
851class MeasureMergedCoaddSourcesTask(PipelineTask):
852 """Deblend sources from main catalog in each coadd seperately and measure.
854 Use peaks and footprints from a master catalog to perform deblending and
855 measurement in each coadd.
857 Given a master input catalog of sources (peaks and footprints) or deblender
858 outputs(including a HeavyFootprint in each band), measure each source on
859 the coadd. Repeating this procedure with the same master catalog across
860 multiple coadds will generate a consistent set of child sources.
862 The deblender retains all peaks and deblends any missing peaks (dropouts in
863 that band) as PSFs. Source properties are measured and the @c is-primary
864 flag (indicating sources with no children) is set. Visit flags are
865 propagated to the coadd sources.
867 Optionally, we can match the coadd sources to an external reference
868 catalog.
870 After MeasureMergedCoaddSourcesTask has been run on multiple coadds, we
871 have a set of per-band catalogs. The next stage in the multi-band
872 processing procedure will merge these measurements into a suitable catalog
873 for driving forced photometry.
875 Parameters
876 ----------
877 schema : ``lsst.afw.table.Schema`, optional
878 The schema of the merged detection catalog used as input to this one.
879 peakSchema : ``lsst.afw.table.Schema`, optional
880 The schema of the PeakRecords in the Footprints in the merged detection catalog.
881 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
882 An instance of ReferenceObjectLoader that supplies an external reference
883 catalog. May be None if the loader can be constructed from the butler argument or all steps
884 requiring a reference catalog are disabled.
885 initInputs : `dict`, optional
886 Dictionary that can contain a key ``inputSchema`` containing the
887 input schema. If present will override the value of ``schema``.
888 **kwargs
889 Additional keyword arguments.
890 """
892 _DefaultName = "measureCoaddSources"
893 ConfigClass = MeasureMergedCoaddSourcesConfig
895 def __init__(self, schema=None, peakSchema=None, refObjLoader=None, initInputs=None,
896 **kwargs):
897 super().__init__(**kwargs)
898 self.deblended = self.config.inputCatalog.startswith("deblended")
899 self.inputCatalog = "Coadd_" + self.config.inputCatalog
900 if initInputs is not None:
901 schema = initInputs['inputSchema'].schema
902 if schema is None:
903 raise ValueError("Schema must be defined.")
904 self.schemaMapper = afwTable.SchemaMapper(schema)
905 self.schemaMapper.addMinimalSchema(schema)
906 self.schema = self.schemaMapper.getOutputSchema()
907 self.algMetadata = PropertyList()
908 self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
909 self.makeSubtask("setPrimaryFlags", schema=self.schema)
910 # TODO[DM-47797]: remove match subtask
911 if self.config.doMatchSources:
912 self.makeSubtask("match", refObjLoader=refObjLoader)
913 if self.config.doPropagateFlags:
914 self.makeSubtask("propagateFlags", schema=self.schema)
915 self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
916 if self.config.doApCorr:
917 self.makeSubtask("applyApCorr", schema=self.schema)
918 if self.config.doRunCatalogCalculation:
919 self.makeSubtask("catalogCalculation", schema=self.schema)
921 self.outputSchema = afwTable.SourceCatalog(self.schema)
923 def runQuantum(self, butlerQC, inputRefs, outputRefs):
924 inputs = butlerQC.get(inputRefs)
926 # TODO[DM-47797]: remove this block
927 if self.config.doMatchSources:
928 refObjLoader = ReferenceObjectLoader([ref.datasetRef.dataId for ref in inputRefs.refCat],
929 inputs.pop('refCat'),
930 name=self.config.connections.refCat,
931 config=self.config.refObjLoader,
932 log=self.log)
933 self.match.setRefObjLoader(refObjLoader)
934 if self.config.imageType == "future":
935 coadd = inputs.pop("exposure")
936 band = inputRefs.exposure.dataId["band"]
937 # Instead of going directly from lsst.images.cells.CellCoadd to
938 # Exposure, it's cleaner for now to go through MultipleCellCoadd
939 # because the apCorrMap and ccdInputs need special handling - the
940 # cell-based versions can't be attached to Exposure. Eventually
941 # we'll rewrite the lower-level code to use the lsst.images
942 # equivalents natively.
943 coadd = coadd.to_legacy_cell_coadd()
944 elif self.config.useCellCoadds:
945 coadd = inputs.pop("exposure_cells")
946 band = inputRefs.exposure_cells.dataId["band"]
947 else:
948 coadd = inputs.pop("exposure")
949 band = inputRefs.exposure.dataId["band"]
950 if isinstance(coadd, MultipleCellCoadd):
951 stitched_coadd = coadd.stitch()
952 exposure = stitched_coadd.asExposure()
953 if self.config.imageType == "legacy":
954 background = inputs.pop("background")
955 exposure.image -= background.getImage()
956 ccdInputs = stitched_coadd.ccds
957 apCorrMap = stitched_coadd.ap_corr_map
958 else:
959 exposure = coadd
960 # Set psfcache only when we don't have a cell-based coadd.
961 exposure.getPsf().setCacheCapacity(self.config.psfCache)
963 ccdInputs = exposure.getInfo().getCoaddInputs().ccds
964 apCorrMap = exposure.getInfo().getApCorrMap()
966 # Get unique integer ID for IdFactory and RNG seeds; only the latter
967 # should really be used as the IDs all come from the input catalog.
968 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
970 # Transform inputCatalog
971 table = afwTable.SourceTable.make(self.schema, idGenerator.make_table_id_factory())
972 sources = afwTable.SourceCatalog(table)
973 # Load the correct input catalog
974 if "scarletCatalog" in inputs:
975 inputCatalog = inputs.pop("scarletCatalog")
976 catalogRef = inputRefs.scarletCatalog
977 else:
978 inputCatalog = inputs.pop("inputCatalog")
979 catalogRef = inputRefs.inputCatalog
980 sources.extend(inputCatalog, self.schemaMapper)
981 del inputCatalog
982 # Add the HeavyFootprints to the deblended sources
983 if self.config.doAddFootprints:
984 modelData = inputs.pop('scarletModels')
985 if self.config.doConserveFlux:
986 imageForRedistribution = exposure
987 else:
988 imageForRedistribution = None
989 updateCatalogFootprints(
990 modelData=modelData,
991 catalog=sources,
992 band=band,
993 imageForRedistribution=imageForRedistribution,
994 removeScarletData=True,
995 updateFluxColumns=True,
996 )
997 table = sources.getTable()
998 table.setMetadata(self.algMetadata) # Capture algorithm metadata to write out to the source catalog.
1000 skyMap = inputs.pop('skyMap')
1001 tractNumber = catalogRef.dataId['tract']
1002 tractInfo = skyMap[tractNumber]
1003 patchInfo = tractInfo.getPatchInfo(catalogRef.dataId['patch'])
1004 skyInfo = Struct(
1005 skyMap=skyMap,
1006 tractInfo=tractInfo,
1007 patchInfo=patchInfo,
1008 wcs=tractInfo.getWcs(),
1009 bbox=patchInfo.getOuterBBox()
1010 )
1012 if self.config.doPropagateFlags:
1013 if "sourceTableHandles" in inputs:
1014 sourceTableHandles = inputs.pop("sourceTableHandles")
1015 sourceTableHandleDict = {handle.dataId["visit"]: handle for handle in sourceTableHandles}
1016 else:
1017 sourceTableHandleDict = None
1018 if "finalizedSourceTableHandles" in inputs:
1019 finalizedSourceTableHandles = inputs.pop("finalizedSourceTableHandles")
1020 finalizedSourceTableHandleDict = {handle.dataId["visit"]: handle
1021 for handle in finalizedSourceTableHandles}
1022 else:
1023 finalizedSourceTableHandleDict = None
1024 if "finalVisitSummaryHandles" in inputs:
1025 finalVisitSummaryHandles = inputs.pop("finalVisitSummaryHandles")
1026 finalVisitSummaryHandleDict = {handle.dataId["visit"]: handle
1027 for handle in finalVisitSummaryHandles}
1028 else:
1029 finalVisitSummaryHandleDict = None
1031 assert not inputs, "runQuantum got more inputs than expected."
1032 outputs = self.run(
1033 exposure=exposure,
1034 sources=sources,
1035 skyInfo=skyInfo,
1036 exposureId=idGenerator.catalog_id,
1037 ccdInputs=ccdInputs,
1038 sourceTableHandleDict=sourceTableHandleDict,
1039 finalizedSourceTableHandleDict=finalizedSourceTableHandleDict,
1040 finalVisitSummaryHandleDict=finalVisitSummaryHandleDict,
1041 apCorrMap=apCorrMap,
1042 )
1043 # Strip HeavyFootprints to save space on disk
1044 if self.config.doStripFootprints:
1045 sources = outputs.outputSources
1046 for source in sources[sources["parent"] != 0]:
1047 source.setFootprint(None)
1048 butlerQC.put(outputs, outputRefs)
1050 def run(self, exposure, sources, skyInfo, exposureId, ccdInputs=None,
1051 sourceTableHandleDict=None, finalizedSourceTableHandleDict=None, finalVisitSummaryHandleDict=None,
1052 apCorrMap=None):
1053 """Run measurement algorithms on the input exposure, and optionally populate the
1054 resulting catalog with extra information.
1056 Parameters
1057 ----------
1058 exposure : `lsst.afw.image.Exposure`
1059 The input exposure on which measurements are to be performed.
1060 sources : `lsst.afw.table.SourceCatalog`
1061 A catalog built from the results of merged detections, or
1062 deblender outputs.
1063 parentCatalog : `lsst.afw.table.SourceCatalog`
1064 Catalog of parent sources corresponding to sources.
1065 skyInfo : `lsst.pipe.base.Struct`
1066 A struct containing information about the position of the input exposure within
1067 a `SkyMap`, the `SkyMap`, its `Wcs`, and its bounding box.
1068 exposureId : `int` or `bytes`
1069 Packed unique number or bytes unique to the input exposure.
1070 ccdInputs : `lsst.afw.table.ExposureCatalog`, optional
1071 Catalog containing information on the individual visits which went into making
1072 the coadd.
1073 sourceTableHandleDict : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`], optional
1074 Dict for sourceTable_visit handles (key is visit) for propagating flags.
1075 These tables contain astrometry and photometry flags, and optionally PSF flags.
1076 finalizedSourceTableHandleDict : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`], optional
1077 Dict for finalized_src_table handles (key is visit) for propagating flags.
1078 These tables contain PSF flags from the finalized PSF estimation.
1079 finalVisitSummaryHandleDict : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`], optional
1080 Dict for visit_summary handles (key is visit) for visit-level information.
1081 These tables contain the WCS information of the single-visit input images.
1082 apCorrMap : `lsst.afw.image.ApCorrMap`, optional
1083 Aperture correction map attached to the ``exposure``. If None, it
1084 will be read from the ``exposure``.
1086 Returns
1087 -------
1088 results : `lsst.pipe.base.Struct`
1089 Results of running measurement task. Will contain the catalog in the
1090 sources attribute. Optionally will have results of matching to a
1091 reference catalog in the matchResults attribute, and denormalized
1092 matches in the denormMatches attribute.
1093 """
1094 if self.config.doPropagateFlags:
1095 # These mask planes may not be defined on the coadds always.
1096 # We add the mask planes, which is a no-op if already defined.
1097 for maskPlane in self.config.measurement.plugins["base_PixelFlags"].masksFpAnywhere:
1098 exposure.mask.addMaskPlane(maskPlane)
1099 for maskPlane in self.config.measurement.plugins["base_PixelFlags"].masksFpCenter:
1100 exposure.mask.addMaskPlane(maskPlane)
1102 self._ensureMaskPlanes()
1104 self.measurement.run(sources, exposure, exposureId=exposureId)
1106 if self.config.doApCorr:
1107 if apCorrMap is None:
1108 apCorrMap = exposure.getInfo().getApCorrMap()
1109 self.applyApCorr.run(
1110 catalog=sources,
1111 apCorrMap=apCorrMap,
1112 )
1114 # TODO DM-11568: this contiguous check-and-copy could go away if we
1115 # reserve enough space during SourceDetection and/or SourceDeblend.
1116 # NOTE: sourceSelectors require contiguous catalogs, so ensure
1117 # contiguity now, so views are preserved from here on.
1118 if not sources.isContiguous():
1119 sources = sources.copy(deep=True)
1121 if self.config.doRunCatalogCalculation:
1122 self.catalogCalculation.run(sources)
1124 self.setPrimaryFlags.run(sources, skyMap=skyInfo.skyMap, tractInfo=skyInfo.tractInfo,
1125 patchInfo=skyInfo.patchInfo)
1126 if self.config.doPropagateFlags:
1127 self.propagateFlags.run(
1128 sources,
1129 ccdInputs,
1130 sourceTableHandleDict,
1131 finalizedSourceTableHandleDict,
1132 finalVisitSummaryHandleDict,
1133 )
1135 results = Struct()
1137 # TODO[DM-47797]: remove this block
1138 if self.config.doMatchSources:
1139 matchResult = self.match.run(sources, exposure.getInfo().getFilter().bandLabel)
1140 matches = afwTable.packMatches(matchResult.matches)
1141 matches.table.setMetadata(matchResult.matchMeta)
1142 results.matchResult = matches
1143 if self.config.doWriteMatchesDenormalized:
1144 if matchResult.matches:
1145 denormMatches = denormalizeMatches(matchResult.matches, matchResult.matchMeta)
1146 else:
1147 self.log.warning("No matches, so generating dummy denormalized matches file")
1148 denormMatches = afwTable.BaseCatalog(afwTable.Schema())
1149 denormMatches.setMetadata(PropertyList())
1150 denormMatches.getMetadata().add("COMMENT",
1151 "This catalog is empty because no matches were found.")
1152 results.denormMatches = denormMatches
1153 results.denormMatches = denormMatches
1155 results.outputSources = sources
1156 return results
1158 def _ensureMaskPlanes(self):
1159 """Ensure the global mask dictionary has all of the mask planes
1160 needed for PixelFlags algorithms.
1162 When mask planes are added, this essentially guarantees that the
1163 corresponding PixelFlags columns will be wholly False, and usually
1164 we'd prefer to remove them from the configuration. But those config
1165 changes imply a schema changes, and that's not always viable (e.g. on
1166 a release branch).
1167 """
1168 needed = set(self.measurement.plugins["base_PixelFlags"].config.masksFpCenter)
1169 needed.update(self.measurement.plugins["base_PixelFlags"].config.masksFpAnywhere)
1170 existing = afwImage.MaskX().getMaskPlaneDict().keys()
1171 for plane in sorted(needed - existing):
1172 self.log.warning(
1173 "Adding mask plane %r with no pixel set to satisfy PixelFlags configuration.", plane
1174 )
1175 afwImage.MaskX.addMaskPlane(plane)