22__all__ = [
"DetectCoaddSourcesConfig",
"DetectCoaddSourcesTask",
23 "MeasureMergedCoaddSourcesConfig",
"MeasureMergedCoaddSourcesTask",
29from lsst.pipe.base
import (
30 AnnotatedPartialOutputsError,
34 PipelineTaskConnections
36import lsst.pipe.base.connectionTypes
as cT
38from lsst.meas.algorithms import DynamicDetectionTask, ReferenceObjectLoader, ScaleVarianceTask, \
42 SingleFrameMeasurementTask,
44 CatalogCalculationTask,
45 SkyMapIdGeneratorConfig,
47from lsst.meas.extensions.scarlet.io
import updateCatalogFootprints
56from .mergeDetections
import MergeDetectionsConfig, MergeDetectionsTask
57from .mergeMeasurements
import MergeMeasurementsConfig, MergeMeasurementsTask
58from .multiBandUtils
import CullPeaksConfig
59from .deblendCoaddSourcesPipeline
import DeblendCoaddSourcesMultiConfig
60from .deblendCoaddSourcesPipeline
import DeblendCoaddSourcesMultiTask
65* deepCoadd_det: detections from what used to be processCoadd (tract, patch, filter)
66* deepCoadd_mergeDet: merged detections (tract, patch)
67* deepCoadd_meas: measurements of merged detections (tract, patch, filter)
68* deepCoadd_ref: reference sources (tract, patch)
69All of these have associated *_schema catalogs that require no data ID and hold no records.
71In addition, we have a schema-only dataset, which saves the schema for the PeakRecords in
72the mergeDet, meas, and ref dataset Footprints:
73* deepCoadd_peak_schema
79 dimensions=(
"tract",
"patch",
"band",
"skymap"),
80 defaultTemplates={
"inputCoaddName":
"deep",
"outputCoaddName":
"deep"}):
81 detectionSchema = cT.InitOutput(
82 doc=
"Schema of the detection catalog",
83 name=
"{outputCoaddName}Coadd_det_schema",
84 storageClass=
"SourceCatalog",
87 doc=
"Exposure on which detections are to be performed. ",
88 name=
"{inputCoaddName}Coadd",
89 storageClass=
"ExposureF",
90 dimensions=(
"tract",
"patch",
"band",
"skymap")
92 exposure_cells = cT.Input(
93 doc=
"Exposure on which detections are to be performed. ",
94 name=
"{inputCoaddName}CoaddCell",
95 storageClass=
"MultipleCellCoadd",
96 dimensions=(
"tract",
"patch",
"band",
"skymap"),
99 doc=
"Description of the skymap's tracts and patches.",
100 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
101 storageClass=
"SkyMap",
102 dimensions=(
"skymap",),
104 outputBackgrounds = cT.Output(
105 doc=
"Output Backgrounds used in detection",
106 name=
"{outputCoaddName}Coadd_calexp_background",
107 storageClass=
"Background",
108 dimensions=(
"tract",
"patch",
"band",
"skymap")
110 outputSources = cT.Output(
111 doc=
"Detected sources catalog",
112 name=
"{outputCoaddName}Coadd_det",
113 storageClass=
"SourceCatalog",
114 dimensions=(
"tract",
"patch",
"band",
"skymap")
116 outputExposure = cT.Output(
117 doc=
"Exposure post detection",
118 name=
"{outputCoaddName}Coadd_calexp",
119 storageClass=
"ExposureF",
120 dimensions=(
"tract",
"patch",
"band",
"skymap")
123 def __init__(self, *, config=None):
124 super().__init__(config=config)
125 assert isinstance(config, DetectCoaddSourcesConfig)
127 if config.useCellCoadds:
130 del self.exposure_cells
132 if not self.config.forceExactBinning:
134 if self.config.writeOnlyBackgrounds:
135 del self.outputExposure
136 del self.outputSources
137 del self.detectionSchema
140class DetectCoaddSourcesConfig(PipelineTaskConfig, pipelineConnections=DetectCoaddSourcesConnections):
141 """Configuration parameters for the DetectCoaddSourcesTask
144 doScaleVariance = Field(dtype=bool, default=
True, doc=
"Scale variance plane using empirical noise?")
145 scaleVariance = ConfigurableField(target=ScaleVarianceTask, doc=
"Variance rescaling")
146 detection = ConfigurableField(target=DynamicDetectionTask, doc=
"Source detection")
147 coaddName = Field(dtype=str, default=
"deep", doc=
"Name of coadd")
148 useCellCoadds = Field(dtype=bool, default=
False, doc=
"Whether to use cell coadds?")
152 doc=
"Should be set to True if fake sources have been inserted into the input data.",
154 idGenerator = SkyMapIdGeneratorConfig.make_field()
155 forceExactBinning = Field(
159 "Check that the background bin size evenly divides the patch inner region, and "
160 "crop the outer region to an integer number of bins."
163 writeOnlyBackgrounds = Field(dtype=bool, default=
False, doc=
"If true, only save the background models.")
164 writeEmptyBackgrounds = Field(
168 "If true, save a placeholder background with NaNs in all bins (but the right geometry) when "
169 "there are no pixels to compute a background from. This can be useful if a later task combines "
170 "backgrounds from multiple patches as input."
174 def setDefaults(self):
175 super().setDefaults()
176 self.detection.thresholdType =
"pixel_stdev"
177 self.detection.isotropicGrow =
True
179 self.detection.reEstimateBackground =
False
180 self.detection.background.useApprox =
False
181 self.detection.background.binSize = 4096
182 self.detection.background.undersampleStyle =
'REDUCE_INTERP_ORDER'
183 self.detection.doTempWideBackground =
True
186 self.idGenerator.packer.n_bands =
None
189class DetectCoaddSourcesTask(PipelineTask):
190 """Detect sources on a single filter coadd.
192 Coadding individual visits requires each exposure to be warped. This
193 introduces covariance in the noise properties across pixels. Before
194 detection, we correct the coadd variance by scaling the variance plane in
195 the coadd to match the observed variance. This is an approximate
196 approach -- strictly, we should propagate the full covariance matrix --
197 but it is simple and works well in practice.
199 After scaling the variance plane, we detect sources and generate footprints
200 by delegating to the @ref SourceDetectionTask_ "detection" subtask.
202 DetectCoaddSourcesTask is meant to be run after assembling a coadded image
203 in a given band. The purpose of the task is to update the background,
204 detect all sources in a single band and generate a set of parent
205 footprints. Subsequent tasks in the multi-band processing procedure will
206 merge sources across bands and, eventually, perform forced photometry.
210 schema : `lsst.afw.table.Schema`, optional
211 Initial schema for the output catalog, modified-in place to include all
212 fields set by this task. If None, the source minimal schema will be used.
214 Additional keyword arguments.
217 _DefaultName =
"detectCoaddSources"
218 ConfigClass = DetectCoaddSourcesConfig
220 def __init__(self, schema=None, **kwargs):
223 super().__init__(**kwargs)
225 schema = afwTable.SourceTable.makeMinimalSchema()
227 self.makeSubtask(
"detection", schema=self.schema)
228 if self.config.doScaleVariance:
229 self.makeSubtask(
"scaleVariance")
231 self.detectionSchema = afwTable.SourceCatalog(self.schema)
233 def runQuantum(self, butlerQC, inputRefs, outputRefs):
234 inputs = butlerQC.get(inputRefs)
235 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
237 if self.config.useCellCoadds:
238 multiple_cell_coadd = inputs.pop(
"exposure_cells")
239 exposure = multiple_cell_coadd.stitch().asExposure()
241 exposure = inputs.pop(
"exposure")
243 skyMap = inputs.pop(
"skyMap",
None)
244 if skyMap
is not None:
245 patchInfo = skyMap[butlerQC.quantum.dataId[
"tract"]][butlerQC.quantum.dataId[
"patch"]]
249 assert not inputs,
"runQuantum got more inputs than expected."
253 idFactory=idGenerator.make_table_id_factory(),
254 expId=idGenerator.catalog_id,
257 except TooManyMaskedPixelsError
as e:
258 if self.config.writeEmptyBackgrounds:
259 butlerQC.put(self._makeEmptyBackground(exposure, patchInfo), outputRefs.outputBackgrounds)
260 error = AnnotatedPartialOutputsError.annotate(
268 butlerQC.put(outputs, outputRefs)
270 def run(self, exposure, idFactory, expId, patchInfo=None):
271 """Run detection on an exposure.
273 First scale the variance plane to match the observed variance
274 using ``ScaleVarianceTask``. Then invoke the ``SourceDetectionTask_`` "detection" subtask to
279 exposure : `lsst.afw.image.Exposure`
280 Exposure on which to detect (may be background-subtracted and scaled,
281 depending on configuration).
282 idFactory : `lsst.afw.table.IdFactory`
283 IdFactory to set source identifiers.
285 Exposure identifier (integer) for RNG seed.
286 patchInfo : `lsst.skymap.PatchInfo`, optional
287 Description of the patch geometry. Only needed if
288 `~DetectCoaddSourceConfig.forceExactBinning` is `True`.
292 result : `lsst.pipe.base.Struct`
293 Results as a struct with attributes:
296 Catalog of detections (`lsst.afw.table.SourceCatalog`).
298 List of backgrounds (`list`).
300 if self.config.forceExactBinning:
301 exposure = self._cropToExactBinning(exposure, patchInfo)
302 if self.config.doScaleVariance:
303 varScale = self.scaleVariance.run(exposure.maskedImage)
304 exposure.getMetadata().add(
"VARIANCE_SCALE", varScale)
305 backgrounds = afwMath.BackgroundList()
306 table = afwTable.SourceTable.make(self.schema, idFactory)
307 detections = self.detection.run(table, exposure, expId=expId)
308 sources = detections.sources
309 if hasattr(detections,
"background")
and detections.background:
310 for bg
in detections.background:
311 backgrounds.append(bg)
312 return Struct(outputSources=sources, outputBackgrounds=backgrounds, outputExposure=exposure)
314 def _cropToExactBinning(self, exposure, patchInfo):
315 """Crop a coadd `~lsst.afw.image.Exposure` instance to ensure exact
320 exposure : `lsst.afw.image.Exposure`
321 Exposure to crop, assumed to cover the patch outer bounding box.
322 patchInfo : `lsst.skymap.PatchInfo`
323 Description of the patch geometry.
327 cropped : `lsst.afw.image.Exposure`
328 View of ``exposure`` with background bins that evenly divide both
329 the full cropped image and the patch inner region. The bounding
330 box is guaranteed to contain the patch inner bounding box and be
331 contained by the patch outer bounding box.
336 Raised if the patch inner region width or height is not a multiple
337 of the background bin size.
339 bbox = patchInfo.getInnerBBox()
340 if bbox.width % self.detection.background.binSizeX:
342 f
"Patch inner width {bbox.width} does not evenly "
343 f
"divide bin width {self.detection.background.binSizeX}."
345 if bbox.height % self.detection.background.binSizeY:
347 f
"Patch inner height {bbox.height} does not evenly "
348 f
"divide bin height {self.detection.background.binSizeY}."
350 outer_bbox = patchInfo.getOuterBBox()
351 n_bins_grow_x = (bbox.x.begin - outer_bbox.x.begin) // self.detection.background.binSizeX
352 n_bins_grow_y = (bbox.y.begin - outer_bbox.y.begin) // self.detection.background.binSizeY
355 n_bins_grow_x*self.detection.background.binSizeX,
356 n_bins_grow_y*self.detection.background.binSizeY,
359 assert outer_bbox.contains(bbox)
360 assert bbox.contains(patchInfo.getInnerBBox())
361 assert bbox.width % self.detection.background.binSizeX == 0
362 assert bbox.height % self.detection.background.binSizeY == 0
363 return exposure[bbox]
365 def _makeEmptyBackground(self, exposure, patchInfo=None):
366 """Construct an empty `lsst.afw.math.BackgroundList` with NaN values.
370 exposure : `lsst.afw.image.Exposure`
371 Exposure that the background should correspond to.
372 patchInfo : `lsst.skymap.PatchInfo`, optional
373 Description of the patch geometry. Only needed if
374 `~DetectCoaddSourceConfig.forceExactBinning` is `True`.
378 background : `lsst.afw.math.BackgroundList`
379 A background object with a single layer and the same bin geometry
380 that a background for that exposure would have had if it had enough
381 usable pixels. This object cannot actually be used for background
388 if self.config.forceExactBinning:
389 exposure = self._cropToExactBinning(exposure, patchInfo).clone()
390 exposure.image.array[:, :] = 0.0
391 exposure.mask[:, :] = 0
392 background = self.detection.background.run(exposure).background
393 for bg, *_
in background:
394 stats = bg.getStatsImage()
395 stats.image.array[:, :] = np.nan
396 stats.mask.array[:, :] = stats.mask.getPlaneBitMask(
"NO_DATA")
397 stats.variance.array[:, :] = 0.0
402 PipelineTaskConnections,
403 dimensions=(
"tract",
"patch",
"band",
"skymap"),
405 "inputCoaddName":
"deep",
406 "outputCoaddName":
"deep",
407 "deblendedCatalog":
"deblendedFlux",
409 deprecatedTemplates={
411 "deblendedCatalog":
"Support for old deblender outputs will be removed after v29."
414 inputSchema = cT.InitInput(
415 doc=
"Input schema for measure merged task produced by a deblender or detection task",
416 name=
"{inputCoaddName}Coadd_deblendedFlux_schema",
417 storageClass=
"SourceCatalog"
419 outputSchema = cT.InitOutput(
420 doc=
"Output schema after all new fields are added by task",
421 name=
"{inputCoaddName}Coadd_meas_schema",
422 storageClass=
"SourceCatalog"
425 refCat = cT.PrerequisiteInput(
426 doc=
"Reference catalog used to match measured sources against known sources",
428 storageClass=
"SimpleCatalog",
429 dimensions=(
"skypix",),
432 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
435 doc=
"Input non-cell-based coadd image",
436 name=
"{inputCoaddName}Coadd_calexp",
437 storageClass=
"ExposureF",
438 dimensions=(
"tract",
"patch",
"band",
"skymap")
440 exposure_cells = cT.Input(
441 doc=
"Input cell-based coadd image",
442 name=
"{inputCoaddName}CoaddCell",
443 storageClass=
"MultipleCellCoadd",
444 dimensions=(
"tract",
"patch",
"band",
"skymap"),
446 background = cT.Input(
447 doc=
"Background to subtract from cell-based coadd image",
448 name=
"{inputCoaddName}Coadd_calexp_background",
449 storageClass=
"Background",
450 dimensions=(
"tract",
"patch",
"band",
"skymap")
453 doc=
"SkyMap to use in processing",
454 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
455 storageClass=
"SkyMap",
456 dimensions=(
"skymap",),
459 visitCatalogs = cT.Input(
460 doc=
"Deprecated and unused.",
462 dimensions=(
"instrument",
"visit",
"detector"),
463 storageClass=
"SourceCatalog",
465 deprecated=
"Deprecated and unused. Will be removed after v29.",
467 sourceTableHandles = cT.Input(
468 doc=(
"Source tables that are derived from the ``CalibrateTask`` sources. "
469 "These tables contain astrometry and photometry flags, and optionally "
471 name=
"sourceTable_visit",
472 storageClass=
"ArrowAstropy",
473 dimensions=(
"instrument",
"visit"),
477 finalizedSourceTableHandles = cT.Input(
478 doc=(
"Finalized source tables from ``FinalizeCalibrationTask``. These "
479 "tables contain PSF flags from the finalized PSF estimation."),
480 name=
"finalized_src_table",
481 storageClass=
"ArrowAstropy",
482 dimensions=(
"instrument",
"visit"),
486 finalVisitSummaryHandles = cT.Input(
487 doc=
"Final visit summary table",
488 name=
"finalVisitSummary",
489 storageClass=
"ExposureCatalog",
490 dimensions=(
"instrument",
"visit"),
495 inputCatalog = cT.Input(
496 doc=(
"Name of the input catalog to use."
497 "If the single band deblender was used this should be 'deblendedFlux."
498 "If the multi-band deblender was used this should be 'deblendedModel, "
499 "or deblendedFlux if the multiband deblender was configured to output "
500 "deblended flux catalogs. If no deblending was performed this should "
502 name=
"{inputCoaddName}Coadd_{deblendedCatalog}",
503 storageClass=
"SourceCatalog",
504 deprecated=
"Support for old deblender outputs will be removed after v29.",
505 dimensions=(
"tract",
"patch",
"band",
"skymap"),
507 scarletCatalog = cT.Input(
508 doc=
"Catalogs produced by multiband deblending",
509 name=
"{inputCoaddName}Coadd_deblendedCatalog",
510 storageClass=
"SourceCatalog",
511 dimensions=(
"tract",
"patch",
"skymap"),
513 scarletModels = cT.Input(
514 doc=
"Multiband scarlet models produced by the deblender",
515 name=
"{inputCoaddName}Coadd_scarletModelData",
516 storageClass=
"ScarletModelData",
517 dimensions=(
"tract",
"patch",
"skymap"),
519 outputSources = cT.Output(
520 doc=
"Source catalog containing all the measurement information generated in this task",
521 name=
"{outputCoaddName}Coadd_meas",
522 dimensions=(
"tract",
"patch",
"band",
"skymap"),
523 storageClass=
"SourceCatalog",
526 matchResult = cT.Output(
527 doc=
"Match catalog produced by configured matcher, optional on doMatchSources",
528 name=
"{outputCoaddName}Coadd_measMatch",
529 dimensions=(
"tract",
"patch",
"band",
"skymap"),
530 storageClass=
"Catalog",
531 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
534 denormMatches = cT.Output(
535 doc=
"Denormalized Match catalog produced by configured matcher, optional on "
536 "doWriteMatchesDenormalized",
537 name=
"{outputCoaddName}Coadd_measMatchFull",
538 dimensions=(
"tract",
"patch",
"band",
"skymap"),
539 storageClass=
"Catalog",
540 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
543 def __init__(self, *, config=None):
544 super().__init__(config=config)
545 del self.visitCatalogs
546 if not config.doPropagateFlags:
547 del self.sourceTableHandles
548 del self.finalizedSourceTableHandles
551 if not config.propagateFlags.source_flags:
552 del self.sourceTableHandles
553 if not config.propagateFlags.finalized_source_flags:
554 del self.finalizedSourceTableHandles
556 if config.inputCatalog ==
"deblendedCatalog":
557 del self.inputCatalog
558 if not config.doAddFootprints:
559 del self.scarletModels
561 del self.deblendedCatalog
562 del self.scarletModels
565 if not config.doMatchSources:
569 if not config.doWriteMatchesDenormalized:
570 del self.denormMatches
572 if config.useCellCoadds:
575 del self.exposure_cells
579class MeasureMergedCoaddSourcesConfig(PipelineTaskConfig,
580 pipelineConnections=MeasureMergedCoaddSourcesConnections):
581 """Configuration parameters for the MeasureMergedCoaddSourcesTask
583 inputCatalog = ChoiceField(
585 default=
"deblendedCatalog",
587 "deblendedCatalog":
"Output catalog from ScarletDeblendTask",
588 "deblendedFlux":
"Output catalog from SourceDeblendTask",
589 "mergeDet":
"The merged detections before deblending."
591 doc=
"The name of the input catalog.",
593 deprecated=
"Support for old deblender outputs will be removed after v29.",
595 doAddFootprints = Field(dtype=bool,
597 doc=
"Whether or not to add footprints to the input catalog from scarlet models. "
598 "This should be true whenever using the multi-band deblender, "
599 "otherwise this should be False.")
600 doConserveFlux = Field(dtype=bool, default=
True,
601 doc=
"Whether to use the deblender models as templates to re-distribute the flux "
602 "from the 'exposure' (True), or to perform measurements on the deblender "
604 doStripFootprints = Field(dtype=bool, default=
True,
605 doc=
"Whether to strip footprints from the output catalog before "
607 "This is usually done when using scarlet models to save disk space.")
608 useCellCoadds = Field(dtype=bool, default=
False, doc=
"Whether to use cell coadds?")
609 measurement = ConfigurableField(target=SingleFrameMeasurementTask, doc=
"Source measurement")
610 setPrimaryFlags = ConfigurableField(target=SetPrimaryFlagsTask, doc=
"Set flags for primary tract/patch")
611 doPropagateFlags = Field(
612 dtype=bool, default=
True,
613 doc=
"Whether to match sources to CCD catalogs to propagate flags (to e.g. identify PSF stars)"
615 propagateFlags = ConfigurableField(target=PropagateSourceFlagsTask, doc=
"Propagate source flags to coadd")
616 doMatchSources = Field(
619 doc=
"Match sources to reference catalog?",
620 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
622 match = ConfigurableField(
623 target=DirectMatchTask,
624 doc=
"Matching to reference catalog",
625 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
627 doWriteMatchesDenormalized = Field(
630 doc=(
"Write reference matches in denormalized format? "
631 "This format uses more disk space, but is more convenient to read."),
632 deprecated=
"Reference matching in measureCoaddSources will be removed after v29.",
634 coaddName = Field(dtype=str, default=
"deep", doc=
"Name of coadd")
635 psfCache = Field(dtype=int, default=100, doc=
"Size of psfCache")
636 checkUnitsParseStrict = Field(
637 doc=
"Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
644 doc=
"Apply aperture corrections"
646 applyApCorr = ConfigurableField(
647 target=ApplyApCorrTask,
648 doc=
"Subtask to apply aperture corrections"
650 doRunCatalogCalculation = Field(
653 doc=
'Run catalogCalculation task'
655 catalogCalculation = ConfigurableField(
656 target=CatalogCalculationTask,
657 doc=
"Subtask to run catalogCalculation plugins on catalog"
663 doc=
"Should be set to True if fake sources have been inserted into the input data."
665 idGenerator = SkyMapIdGeneratorConfig.make_field()
669 return self.match.refObjLoader
671 def setDefaults(self):
672 super().setDefaults()
673 self.measurement.plugins.names |= [
'base_InputCount',
675 'base_LocalPhotoCalib',
681 self.measurement.plugins[
'base_PixelFlags'].masksFpAnywhere = [
'CLIPPED',
'SENSOR_EDGE',
683 self.measurement.plugins[
'base_PixelFlags'].masksFpCenter = [
'CLIPPED',
'SENSOR_EDGE',
689 if not self.doMatchSources
and self.doWriteMatchesDenormalized:
690 raise ValueError(
"Cannot set doWriteMatchesDenormalized if doMatchSources is False.")
693class MeasureMergedCoaddSourcesTask(PipelineTask):
694 """Deblend sources from main catalog in each coadd seperately and measure.
696 Use peaks and footprints from a master catalog to perform deblending and
697 measurement in each coadd.
699 Given a master input catalog of sources (peaks and footprints) or deblender
700 outputs(including a HeavyFootprint in each band), measure each source on
701 the coadd. Repeating this procedure with the same master catalog across
702 multiple coadds will generate a consistent set of child sources.
704 The deblender retains all peaks and deblends any missing peaks (dropouts in
705 that band) as PSFs. Source properties are measured and the @c is-primary
706 flag (indicating sources with no children) is set. Visit flags are
707 propagated to the coadd sources.
709 Optionally, we can match the coadd sources to an external reference
712 After MeasureMergedCoaddSourcesTask has been run on multiple coadds, we
713 have a set of per-band catalogs. The next stage in the multi-band
714 processing procedure will merge these measurements into a suitable catalog
715 for driving forced photometry.
719 schema : ``lsst.afw.table.Schema`, optional
720 The schema of the merged detection catalog used as input to this one.
721 peakSchema : ``lsst.afw.table.Schema`, optional
722 The schema of the PeakRecords in the Footprints in the merged detection catalog.
723 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
724 An instance of ReferenceObjectLoader that supplies an external reference
725 catalog. May be None if the loader can be constructed from the butler argument or all steps
726 requiring a reference catalog are disabled.
727 initInputs : `dict`, optional
728 Dictionary that can contain a key ``inputSchema`` containing the
729 input schema. If present will override the value of ``schema``.
731 Additional keyword arguments.
734 _DefaultName =
"measureCoaddSources"
735 ConfigClass = MeasureMergedCoaddSourcesConfig
737 def __init__(self, schema=None, peakSchema=None, refObjLoader=None, initInputs=None,
739 super().__init__(**kwargs)
740 self.deblended = self.config.inputCatalog.startswith(
"deblended")
741 self.inputCatalog =
"Coadd_" + self.config.inputCatalog
742 if initInputs
is not None:
743 schema = initInputs[
'inputSchema'].schema
745 raise ValueError(
"Schema must be defined.")
746 self.schemaMapper = afwTable.SchemaMapper(schema)
747 self.schemaMapper.addMinimalSchema(schema)
748 self.schema = self.schemaMapper.getOutputSchema()
750 self.makeSubtask(
"measurement", schema=self.schema, algMetadata=self.algMetadata)
751 self.makeSubtask(
"setPrimaryFlags", schema=self.schema)
753 if self.config.doMatchSources:
754 self.makeSubtask(
"match", refObjLoader=refObjLoader)
755 if self.config.doPropagateFlags:
756 self.makeSubtask(
"propagateFlags", schema=self.schema)
757 self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
758 if self.config.doApCorr:
759 self.makeSubtask(
"applyApCorr", schema=self.schema)
760 if self.config.doRunCatalogCalculation:
761 self.makeSubtask(
"catalogCalculation", schema=self.schema)
763 self.outputSchema = afwTable.SourceCatalog(self.schema)
765 def runQuantum(self, butlerQC, inputRefs, outputRefs):
766 inputs = butlerQC.get(inputRefs)
769 if self.config.doMatchSources:
770 refObjLoader = ReferenceObjectLoader([ref.datasetRef.dataId
for ref
in inputRefs.refCat],
771 inputs.pop(
'refCat'),
772 name=self.config.connections.refCat,
773 config=self.config.refObjLoader,
775 self.match.setRefObjLoader(refObjLoader)
777 if self.config.useCellCoadds:
778 multiple_cell_coadd = inputs.pop(
"exposure_cells")
779 stitched_coadd = multiple_cell_coadd.stitch()
780 exposure = stitched_coadd.asExposure()
781 background = inputs.pop(
"background")
782 exposure.image -= background.getImage()
784 ccdInputs = stitched_coadd.ccds
785 apCorrMap = stitched_coadd.ap_corr_map
786 band = inputRefs.exposure_cells.dataId[
"band"]
788 exposure = inputs.pop(
"exposure")
791 exposure.getPsf().setCacheCapacity(self.config.psfCache)
793 ccdInputs = exposure.getInfo().getCoaddInputs().ccds
794 apCorrMap = exposure.getInfo().getApCorrMap()
795 band = inputRefs.exposure.dataId[
"band"]
799 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
802 table = afwTable.SourceTable.make(self.schema, idGenerator.make_table_id_factory())
803 sources = afwTable.SourceCatalog(table)
805 if "scarletCatalog" in inputs:
806 inputCatalog = inputs.pop(
"scarletCatalog")
807 catalogRef = inputRefs.scarletCatalog
809 inputCatalog = inputs.pop(
"inputCatalog")
810 catalogRef = inputRefs.inputCatalog
811 sources.extend(inputCatalog, self.schemaMapper)
814 if self.config.doAddFootprints:
815 modelData = inputs.pop(
'scarletModels')
816 if self.config.doConserveFlux:
817 imageForRedistribution = exposure
819 imageForRedistribution =
None
820 updateCatalogFootprints(
824 imageForRedistribution=imageForRedistribution,
825 removeScarletData=
True,
826 updateFluxColumns=
True,
828 table = sources.getTable()
829 table.setMetadata(self.algMetadata)
831 skyMap = inputs.pop(
'skyMap')
832 tractNumber = catalogRef.dataId[
'tract']
833 tractInfo = skyMap[tractNumber]
834 patchInfo = tractInfo.getPatchInfo(catalogRef.dataId[
'patch'])
839 wcs=tractInfo.getWcs(),
840 bbox=patchInfo.getOuterBBox()
843 if self.config.doPropagateFlags:
844 if "sourceTableHandles" in inputs:
845 sourceTableHandles = inputs.pop(
"sourceTableHandles")
846 sourceTableHandleDict = {handle.dataId[
"visit"]: handle
for handle
in sourceTableHandles}
848 sourceTableHandleDict =
None
849 if "finalizedSourceTableHandles" in inputs:
850 finalizedSourceTableHandles = inputs.pop(
"finalizedSourceTableHandles")
851 finalizedSourceTableHandleDict = {handle.dataId[
"visit"]: handle
852 for handle
in finalizedSourceTableHandles}
854 finalizedSourceTableHandleDict =
None
855 if "finalVisitSummaryHandles" in inputs:
856 finalVisitSummaryHandles = inputs.pop(
"finalVisitSummaryHandles")
857 finalVisitSummaryHandleDict = {handle.dataId[
"visit"]: handle
858 for handle
in finalVisitSummaryHandles}
860 finalVisitSummaryHandleDict =
None
862 assert not inputs,
"runQuantum got more inputs than expected."
867 exposureId=idGenerator.catalog_id,
869 sourceTableHandleDict=sourceTableHandleDict,
870 finalizedSourceTableHandleDict=finalizedSourceTableHandleDict,
871 finalVisitSummaryHandleDict=finalVisitSummaryHandleDict,
875 if self.config.doStripFootprints:
876 sources = outputs.outputSources
877 for source
in sources[sources[
"parent"] != 0]:
878 source.setFootprint(
None)
879 butlerQC.put(outputs, outputRefs)
881 def run(self, exposure, sources, skyInfo, exposureId, ccdInputs=None,
882 sourceTableHandleDict=None, finalizedSourceTableHandleDict=None, finalVisitSummaryHandleDict=None,
884 """Run measurement algorithms on the input exposure, and optionally populate the
885 resulting catalog with extra information.
889 exposure : `lsst.afw.exposure.Exposure`
890 The input exposure on which measurements are to be performed.
891 sources : `lsst.afw.table.SourceCatalog`
892 A catalog built from the results of merged detections, or
894 skyInfo : `lsst.pipe.base.Struct`
895 A struct containing information about the position of the input exposure within
896 a `SkyMap`, the `SkyMap`, its `Wcs`, and its bounding box.
897 exposureId : `int` or `bytes`
898 Packed unique number or bytes unique to the input exposure.
899 ccdInputs : `lsst.afw.table.ExposureCatalog`, optional
900 Catalog containing information on the individual visits which went into making
902 sourceTableHandleDict : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`], optional
903 Dict for sourceTable_visit handles (key is visit) for propagating flags.
904 These tables contain astrometry and photometry flags, and optionally PSF flags.
905 finalizedSourceTableHandleDict : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`], optional
906 Dict for finalized_src_table handles (key is visit) for propagating flags.
907 These tables contain PSF flags from the finalized PSF estimation.
908 finalVisitSummaryHandleDict : `dict` [`int`, `lsst.daf.butler.DeferredDatasetHandle`], optional
909 Dict for visit_summary handles (key is visit) for visit-level information.
910 These tables contain the WCS information of the single-visit input images.
911 apCorrMap : `lsst.afw.image.ApCorrMap`, optional
912 Aperture correction map attached to the ``exposure``. If None, it
913 will be read from the ``exposure``.
917 results : `lsst.pipe.base.Struct`
918 Results of running measurement task. Will contain the catalog in the
919 sources attribute. Optionally will have results of matching to a
920 reference catalog in the matchResults attribute, and denormalized
921 matches in the denormMatches attribute.
923 if self.config.doPropagateFlags:
926 for maskPlane
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
927 exposure.mask.addMaskPlane(maskPlane)
928 for maskPlane
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpCenter:
929 exposure.mask.addMaskPlane(maskPlane)
931 self.measurement.run(sources, exposure, exposureId=exposureId)
933 if self.config.doApCorr:
934 if apCorrMap
is None:
935 apCorrMap = exposure.getInfo().getApCorrMap()
936 self.applyApCorr.run(
945 if not sources.isContiguous():
946 sources = sources.copy(deep=
True)
948 if self.config.doRunCatalogCalculation:
949 self.catalogCalculation.run(sources)
951 self.setPrimaryFlags.run(sources, skyMap=skyInfo.skyMap, tractInfo=skyInfo.tractInfo,
952 patchInfo=skyInfo.patchInfo)
953 if self.config.doPropagateFlags:
954 self.propagateFlags.run(
957 sourceTableHandleDict,
958 finalizedSourceTableHandleDict,
959 finalVisitSummaryHandleDict,
965 if self.config.doMatchSources:
966 matchResult = self.match.run(sources, exposure.getInfo().getFilter().bandLabel)
967 matches = afwTable.packMatches(matchResult.matches)
968 matches.table.setMetadata(matchResult.matchMeta)
969 results.matchResult = matches
970 if self.config.doWriteMatchesDenormalized:
971 if matchResult.matches:
972 denormMatches = denormalizeMatches(matchResult.matches, matchResult.matchMeta)
974 self.log.warning(
"No matches, so generating dummy denormalized matches file")
975 denormMatches = afwTable.BaseCatalog(afwTable.Schema())
977 denormMatches.getMetadata().add(
"COMMENT",
978 "This catalog is empty because no matches were found.")
979 results.denormMatches = denormMatches
980 results.denormMatches = denormMatches
982 results.outputSources = sources