22__all__ = [
"DetectCoaddSourcesConfig",
"DetectCoaddSourcesTask",
23 "MeasureMergedCoaddSourcesConfig",
"MeasureMergedCoaddSourcesTask",
24 "DEEP_COADD_BACKGROUND_DOCSTRING",
33from lsst.pipe.base
import (
34 AnnotatedPartialOutputsError,
38 PipelineTaskConnections
40import lsst.pipe.base.connectionTypes
as cT
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
48 ExceedsMaxVarianceScaleError,
49 InsufficientSourcesError,
51 ReferenceObjectLoader,
54 TooManyMaskedPixelsError,
58 SingleFrameMeasurementTask,
60 CatalogCalculationTask,
61 SkyMapIdGeneratorConfig,
63from lsst.meas.extensions.scarlet.io
import updateCatalogFootprints
73from .mergeDetections
import MergeDetectionsConfig, MergeDetectionsTask
74from .mergeMeasurements
import MergeMeasurementsConfig, MergeMeasurementsTask
75from .multiBandUtils
import CullPeaksConfig
76from .deblendCoaddSourcesPipeline
import DeblendCoaddSourcesMultiConfig
77from .deblendCoaddSourcesPipeline
import DeblendCoaddSourcesMultiTask
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
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 "
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",
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")
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"),
128 doc=
"Description of the skymap's tracts and patches.",
129 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
130 storageClass=
"SkyMap",
131 dimensions=(
"skymap",),
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")
139 outputSources = cT.Output(
140 doc=
"Detected sources catalog",
141 name=
"{outputCoaddName}Coadd_det",
142 storageClass=
"SourceCatalog",
143 dimensions=(
"tract",
"patch",
"band",
"skymap")
145 outputExposure = cT.Output(
146 doc=
"Exposure post detection",
147 name=
"{outputCoaddName}Coadd_calexp",
148 storageClass=
"ExposureF",
149 dimensions=(
"tract",
"patch",
"band",
"skymap")
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")
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:
166 del self.exposure_cells
167 if not self.config.forceExactBinning:
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
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?")
187 doc=
"Should be set to True if fake sources have been inserted into the input data.",
189 idGenerator = SkyMapIdGeneratorConfig.make_field()
190 forceExactBinning = Field(
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."
198 writeOnlyBackgrounds = Field(dtype=bool, default=
False, doc=
"If true, only save the background models.")
199 writeEmptyBackgrounds = Field(
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."
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.",
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."
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."
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.",
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.",
238 default=DEEP_COADD_BACKGROUND_DOCSTRING,
241 def setDefaults(self):
242 super().setDefaults()
243 self.detection.thresholdType =
"pixel_stdev"
244 self.detection.isotropicGrow =
True
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
253 self.idGenerator.packer.n_bands =
None
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.
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.
291 Additional keyword arguments.
294 _DefaultName =
"detectCoaddSources"
295 ConfigClass = DetectCoaddSourcesConfig
297 def __init__(self, schema=None, **kwargs):
300 super().__init__(**kwargs)
302 schema = afwTable.SourceTable.makeMinimalSchema()
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:
318 exposure = multiple_cell_coadd.stitch().asExposure()
320 exposure = multiple_cell_coadd
322 raise AssertionError(f
"Invalid choice {self.config.imageType!r} for imageType.")
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"]]
332 assert not inputs,
"runQuantum got more inputs than expected."
336 idFactory=idGenerator.make_table_id_factory(),
337 expId=idGenerator.catalog_id,
341 TooManyMaskedPixelsError,
342 ExceedsMaxVarianceScaleError,
343 InsufficientSourcesError,
347 if self.config.writeEmptyBackgrounds:
348 butlerQC.put(self._makeEmptyBackground(exposure, patchInfo), outputRefs.outputBackgrounds)
350 if isinstance(exposure, CellCoadd):
357 exposure.mask.clear(
"DETECTED")
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(
369 butlerQC.put(exposure, outputRefs.outputExposure)
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
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.
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`.
396 result : `lsst.pipe.base.Struct`
397 Results as a struct with attributes:
400 Catalog of detections (`lsst.afw.table.SourceCatalog`).
401 ``outputBackgrounds``
402 List of backgrounds (`list`).
404 The background-subtracted coadd image, with its mask plane
405 updated to include detections. This will have the same type
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:
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)
439 cell_coadd.backgrounds.add(
440 self.config.backgroundName,
441 field_from_legacy_background(backgrounds, unit=astropy.units.nJy),
442 self.config.backgroundDescription,
445 exposure = cell_coadd
446 return Struct(outputSources=sources, outputBackgrounds=backgrounds, outputExposure=exposure)
449 """Crop a coadd `~lsst.afw.image.Exposure` instance to ensure exact
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.
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.
470 Raised if the patch inner region width or height is not a multiple
471 of the background bin size.
473 bbox = patchInfo.getInnerBBox()
474 if bbox.width % self.detection.background.binSizeX:
476 f
"Patch inner width {bbox.width} does not evenly "
477 f
"divide bin width {self.detection.background.binSizeX}."
479 if bbox.height % self.detection.background.binSizeY:
481 f
"Patch inner height {bbox.height} does not evenly "
482 f
"divide bin height {self.detection.background.binSizeY}."
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
489 n_bins_grow_x*self.detection.background.binSizeX,
490 n_bins_grow_y*self.detection.background.binSizeY,
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]
500 """Construct an empty `lsst.afw.math.BackgroundList` with NaN values.
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`.
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
520 if self.config.forceExactBinning:
521 exposure = self._cropToExactBinning(exposure, patchInfo).clone()
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
539 PipelineTaskConnections,
540 dimensions=(
"tract",
"patch",
"band",
"skymap"),
542 "inputCoaddName":
"deep",
543 "outputCoaddName":
"deep",
544 "deblendedCatalog":
"deblendedFlux",
546 deprecatedTemplates={
548 "deblendedCatalog":
"Support for old deblender outputs will be removed after v29."
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"
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"
562 refCat = cT.PrerequisiteInput(
563 doc=
"Reference catalog used to match measured sources against known sources",
565 storageClass=
"SimpleCatalog",
566 dimensions=(
"skypix",),
569 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
572 doc=
"Input non-cell-based coadd image",
573 name=
"{inputCoaddName}Coadd_calexp",
574 storageClass=
"ExposureF",
575 dimensions=(
"tract",
"patch",
"band",
"skymap")
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"),
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")
590 doc=
"SkyMap to use in processing",
591 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
592 storageClass=
"SkyMap",
593 dimensions=(
"skymap",),
596 visitCatalogs = cT.Input(
597 doc=
"Deprecated and unused.",
599 dimensions=(
"instrument",
"visit",
"detector"),
600 storageClass=
"SourceCatalog",
602 deprecated=
"Deprecated and unused. Will be removed after v29.",
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 "
608 name=
"sourceTable_visit",
609 storageClass=
"ArrowAstropy",
610 dimensions=(
"instrument",
"visit"),
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"),
623 finalVisitSummaryHandles = cT.Input(
624 doc=
"Final visit summary table",
625 name=
"finalVisitSummary",
626 storageClass=
"ExposureCatalog",
627 dimensions=(
"instrument",
"visit"),
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 "
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"),
644 scarletCatalog = cT.Input(
645 doc=
"Catalogs produced by multiband deblending",
646 name=
"{inputCoaddName}Coadd_deblendedCatalog",
647 storageClass=
"SourceCatalog",
648 dimensions=(
"tract",
"patch",
"skymap"),
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"),
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",
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.",
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.",
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
688 if not config.propagateFlags.source_flags:
689 del self.sourceTableHandles
690 if not config.propagateFlags.finalized_source_flags:
691 del self.finalizedSourceTableHandles
693 if config.inputCatalog ==
"deblendedCatalog":
694 del self.inputCatalog
695 if not config.doAddFootprints:
696 del self.scarletModels
698 del self.deblendedCatalog
699 del self.scarletModels
702 if not config.doMatchSources:
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
713 elif self.config.useCellCoadds:
716 del self.exposure_cells
720class MeasureMergedCoaddSourcesConfig(PipelineTaskConfig,
721 pipelineConnections=MeasureMergedCoaddSourcesConnections):
722 """Configuration parameters for the MeasureMergedCoaddSourcesTask
724 inputCatalog = ChoiceField(
726 default=
"deblendedCatalog",
728 "deblendedCatalog":
"Output catalog from ScarletDeblendTask",
729 "deblendedFlux":
"Output catalog from SourceDeblendTask",
730 "mergeDet":
"The merged detections before deblending."
732 doc=
"The name of the input catalog.",
734 deprecated=
"Support for old deblender outputs will be removed after v29.",
736 doAddFootprints = Field(dtype=bool,
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 "
745 doStripFootprints = Field(dtype=bool, default=
True,
746 doc=
"Whether to strip footprints from the output catalog before "
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)"
756 propagateFlags = ConfigurableField(target=PropagateSourceFlagsTask, doc=
"Propagate source flags to coadd")
757 doMatchSources = Field(
760 doc=
"Match sources to reference catalog?",
761 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
763 match = ConfigurableField(
764 target=DirectMatchTask,
765 doc=
"Matching to reference catalog",
766 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
768 doWriteMatchesDenormalized = Field(
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.",
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'",
785 doc=
"Apply aperture corrections"
787 applyApCorr = ConfigurableField(
788 target=ApplyApCorrTask,
789 doc=
"Subtask to apply aperture corrections"
791 doRunCatalogCalculation = Field(
794 doc=
'Run catalogCalculation task'
796 catalogCalculation = ConfigurableField(
797 target=CatalogCalculationTask,
798 doc=
"Subtask to run catalogCalculation plugins on catalog"
804 doc=
"Should be set to True if fake sources have been inserted into the input data."
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.",
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)."
817 "Read lsst.images.cells.CellCoadd via the 'exposure' connection. useCellCoadd is ignored."
827 return self.match.refObjLoader
829 def setDefaults(self):
830 super().setDefaults()
831 self.measurement.plugins.names |= [
'base_InputCount',
833 'base_LocalPhotoCalib',
839 self.measurement.plugins[
'base_PixelFlags'].masksFpAnywhere = [
'CLIPPED',
'SENSOR_EDGE',
841 self.measurement.plugins[
'base_PixelFlags'].masksFpCenter = [
'CLIPPED',
'SENSOR_EDGE',
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
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.
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``.
889 Additional keyword arguments.
892 _DefaultName =
"measureCoaddSources"
893 ConfigClass = MeasureMergedCoaddSourcesConfig
895 def __init__(self, schema=None, peakSchema=None, refObjLoader=None, initInputs=None,
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
903 raise ValueError(
"Schema must be defined.")
904 self.schemaMapper = afwTable.SchemaMapper(schema)
905 self.schemaMapper.addMinimalSchema(schema)
906 self.schema = self.schemaMapper.getOutputSchema()
908 self.makeSubtask(
"measurement", schema=self.schema, algMetadata=self.algMetadata)
909 self.makeSubtask(
"setPrimaryFlags", schema=self.schema)
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)
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,
933 self.match.setRefObjLoader(refObjLoader)
934 if self.config.imageType ==
"future":
935 coadd = inputs.pop(
"exposure")
936 band = inputRefs.exposure.dataId[
"band"]
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"]
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
961 exposure.getPsf().setCacheCapacity(self.config.psfCache)
963 ccdInputs = exposure.getInfo().getCoaddInputs().ccds
964 apCorrMap = exposure.getInfo().getApCorrMap()
968 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
971 table = afwTable.SourceTable.make(self.schema, idGenerator.make_table_id_factory())
972 sources = afwTable.SourceCatalog(table)
974 if "scarletCatalog" in inputs:
975 inputCatalog = inputs.pop(
"scarletCatalog")
976 catalogRef = inputRefs.scarletCatalog
978 inputCatalog = inputs.pop(
"inputCatalog")
979 catalogRef = inputRefs.inputCatalog
980 sources.extend(inputCatalog, self.schemaMapper)
983 if self.config.doAddFootprints:
984 modelData = inputs.pop(
'scarletModels')
985 if self.config.doConserveFlux:
986 imageForRedistribution = exposure
988 imageForRedistribution =
None
989 updateCatalogFootprints(
993 imageForRedistribution=imageForRedistribution,
994 removeScarletData=
True,
995 updateFluxColumns=
True,
997 table = sources.getTable()
998 table.setMetadata(self.algMetadata)
1000 skyMap = inputs.pop(
'skyMap')
1001 tractNumber = catalogRef.dataId[
'tract']
1002 tractInfo = skyMap[tractNumber]
1003 patchInfo = tractInfo.getPatchInfo(catalogRef.dataId[
'patch'])
1006 tractInfo=tractInfo,
1007 patchInfo=patchInfo,
1008 wcs=tractInfo.getWcs(),
1009 bbox=patchInfo.getOuterBBox()
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}
1017 sourceTableHandleDict =
None
1018 if "finalizedSourceTableHandles" in inputs:
1019 finalizedSourceTableHandles = inputs.pop(
"finalizedSourceTableHandles")
1020 finalizedSourceTableHandleDict = {handle.dataId[
"visit"]: handle
1021 for handle
in finalizedSourceTableHandles}
1023 finalizedSourceTableHandleDict =
None
1024 if "finalVisitSummaryHandles" in inputs:
1025 finalVisitSummaryHandles = inputs.pop(
"finalVisitSummaryHandles")
1026 finalVisitSummaryHandleDict = {handle.dataId[
"visit"]: handle
1027 for handle
in finalVisitSummaryHandles}
1029 finalVisitSummaryHandleDict =
None
1031 assert not inputs,
"runQuantum got more inputs than expected."
1036 exposureId=idGenerator.catalog_id,
1037 ccdInputs=ccdInputs,
1038 sourceTableHandleDict=sourceTableHandleDict,
1039 finalizedSourceTableHandleDict=finalizedSourceTableHandleDict,
1040 finalVisitSummaryHandleDict=finalVisitSummaryHandleDict,
1041 apCorrMap=apCorrMap,
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,
1053 """Run measurement algorithms on the input exposure, and optionally populate the
1054 resulting catalog with extra information.
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
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
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``.
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.
1094 if self.config.doPropagateFlags:
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(
1111 apCorrMap=apCorrMap,
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(
1130 sourceTableHandleDict,
1131 finalizedSourceTableHandleDict,
1132 finalVisitSummaryHandleDict,
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)
1147 self.log.warning(
"No matches, so generating dummy denormalized matches file")
1148 denormMatches = afwTable.BaseCatalog(afwTable.Schema())
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
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
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):
1173 "Adding mask plane %r with no pixel set to satisfy PixelFlags configuration.", plane
1175 afwImage.MaskX.addMaskPlane(plane)
_makeEmptyBackground(self, exposure, patchInfo=None)
_cropToExactBinning(self, exposure, patchInfo)
run(self, exposure, idFactory, expId, patchInfo=None)