diff --git a/python/lsst/pipe/tasks/deblendCoaddSourcesPipeline.py b/python/lsst/pipe/tasks/deblendCoaddSourcesPipeline.py index afde0f140..e960512a6 100644 --- a/python/lsst/pipe/tasks/deblendCoaddSourcesPipeline.py +++ b/python/lsst/pipe/tasks/deblendCoaddSourcesPipeline.py @@ -21,17 +21,21 @@ __all__ = ["DeblendCoaddSourcesMultiConfig", "DeblendCoaddSourcesMultiTask"] +import dataclasses + import numpy as np from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections import lsst.pipe.base.connectionTypes as cT -from lsst.pex.config import ConfigurableField, Field +from lsst.pex.config import ChoiceField, ConfigurableField, Field from lsst.meas.base import SkyMapIdGeneratorConfig from lsst.meas.extensions.scarlet import ScarletDeblendTask import lsst.afw.image as afwImage import lsst.afw.table as afwTable +import lsst.images as imgs +from lsst.images.cells import CellCoadd from .coaddBase import reorderRefs @@ -112,12 +116,16 @@ class DeblendCoaddSourcesMultiConnections(PipelineTaskConnections, def __init__(self, *, config=None): super().__init__(config=config) - if config: - if config.useCellCoadds: - del self.coadds - else: - del self.coadds_cell - del self.backgrounds + if self.config.imageType == "future": + self.coadds = dataclasses.replace(self.coadds, storageClass="CellCoadd") + self.deconvolvedCoadds = dataclasses.replace(self.deconvolvedCoadds, storageClass="MaskedImageV2") + del self.coadds_cell + del self.backgrounds + elif self.config.useCellCoadds: + del self.coadds + else: + del self.coadds_cell + del self.backgrounds class DeblendCoaddSourcesMultiConfig(PipelineTaskConfig, @@ -131,6 +139,25 @@ class DeblendCoaddSourcesMultiConfig(PipelineTaskConfig, doc="Task to deblend an images in multiple bands" ) idGenerator = SkyMapIdGeneratorConfig.make_field() + imageType = ChoiceField( + "Which image type to expect for the input coadds. " + "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' " + "method behavior is determined by which type is actually passed in.", + allowed={ + "legacy": ( + "Read a lsst.cell_coadds.MultipleCellCoadd via 'coadds_cells` and restore 'background' " + "(if useCellCoadd) or lsst.afw.image.Exposure via `coadds` (if not useCellCoadd), and read " + "lsst.afw.image.Exposure via 'deconvolvedCoadds'." + ), + "future": ( + "Read lsst.images.cells.CellCoadd via 'coadds' and lsst.images.MaskedImage via " + "'deconvolvedCoadds'. The useCellCoadds options is ignored." + ), + }, + dtype=str, + optional=False, + default="legacy", + ) class DeblendCoaddSourcesMultiTask(PipelineTask): @@ -160,17 +187,25 @@ def runQuantum(self, butlerQC, inputRefs, outputRefs): inputs = butlerQC.get(inputRefs) bands = [dRef.dataId["band"] for dRef in deconvolvedRefs] mergedDetections = inputs.pop("mergedDetections") - if self.config.useCellCoadds: - exposures = [mcc.stitch().asExposure() for mcc in inputs.pop("coadds_cell")] - backgrounds = inputs.pop("backgrounds") - for exposure, background in zip(exposures, backgrounds): - exposure.image -= background.getImage() - coadds = exposures - else: - coadds = inputs.pop("coadds") + match self.config.imageType: + case "legacy": + if self.config.useCellCoadds: + exposures = [mcc.stitch().asExposure() for mcc in inputs.pop("coadds_cell")] + backgrounds = inputs.pop("backgrounds") + for exposure, background in zip(exposures, backgrounds): + exposure.image -= background.getImage() + coadds = exposures + coaddRefs = inputRefs.coadds_cell + else: + coadds = inputs.pop("coadds") + coaddRefs = inputRefs.coadds + case "future": + coadds = inputs.pop("coadds") # conversion deferred to run(). + coaddRefs = inputRefs.coadds + case _: + raise AssertionError(f"Invalid choice {self.config.imageType!r} for imageType.") # Ensure that the coadd bands and deconvolved coadd bands match - coaddRefs = inputRefs.coadds_cell if self.config.useCellCoadds else inputRefs.coadds coaddBands = [dRef.dataId["band"] for dRef in coaddRefs] if bands != coaddBands: self.log.error("Coadd bands %s != deconvolved coadd bands %s", bands, coaddBands) @@ -194,12 +229,47 @@ def runQuantum(self, butlerQC, inputRefs, outputRefs): butlerQC.put(outputs, outputRefs) def run(self, coadds, bands, mergedDetections, deconvolvedCoadds, idFactory): + """Deblend coadds from multiple bands together. + + Parameters + ---------- + coadds : `list` [`lsst.afw.image.Exposure` | \ + `lsst.images.cells.CellCoadd`] + Coadds to deblend. + bands : `list` [`str`] + Names or the bands for ``coadds`` (zip-iteration compatible). + mergedDetections : `lsst.afw.table.SourceCatalog` + Input catalog of detections, already merged across bands. + deconvolvedCoadds : `list` [`lsst.afw.image.Exposure` | \ + `lsst.images.MaskedImage`] + Deconvolved versions of ``coadds`` (zip-iteration compatible). + idFactory : `lsst.afw.table.IdFactory` + Factory used to generate output source IDs. + + Returns + ------- + struct : `lsst.pipe.base.Struct` + Unmodified outputs of the ``multibandDeblend`` subtask. + """ + coadds = [c.to_legacy() if isinstance(c, CellCoadd) else c for c in coadds] + deconvolvedCoadds = [self._coerceDeconvolvedInput(d, c) for d, c in zip(deconvolvedCoadds, coadds)] sources = self._makeSourceCatalog(mergedDetections, idFactory) multiExposure = afwImage.MultibandExposure.fromExposures(bands, coadds) mDeconvolved = afwImage.MultibandExposure.fromExposures(bands, deconvolvedCoadds) result = self.multibandDeblend.run(multiExposure, mDeconvolved, sources) return result + def _coerceDeconvolvedInput( + self, deconvolved: afwImage.Exposure | imgs.MaskedImage, coadd: afwImage.Exposure + ) -> afwImage.Exposure: + if isinstance(deconvolved, imgs.MaskedImage): + deconvolved = afwImage.Exposure( + maskedImage=deconvolved.to_legacy(plane_map=imgs.get_legacy_deep_coadd_mask_planes()), + exposureInfo=coadd.getInfo(), + dtype=deconvolved.image.array.dtype, + ) + return deconvolved + def _makeSourceCatalog(self, mergedDetections, idFactory): # There may be gaps in the mergeDet catalog, which will cause the # source ids to be inconsistent. So we update the id factory diff --git a/python/lsst/pipe/tasks/fit_coadd_multiband.py b/python/lsst/pipe/tasks/fit_coadd_multiband.py index ca4f03a0c..ab8ae1b64 100644 --- a/python/lsst/pipe/tasks/fit_coadd_multiband.py +++ b/python/lsst/pipe/tasks/fit_coadd_multiband.py @@ -34,6 +34,7 @@ import lsst.pipe.base.connectionTypes as cT import astropy.table +import dataclasses from abc import ABC, abstractmethod from pydantic import Field from pydantic.dataclasses import dataclass @@ -210,7 +211,11 @@ def __init__(self, *, config=None): if config.drop_psf_connection: del self.models_psf - if config.use_cell_coadds: + if config.image_type == "future": + self.coadds = dataclasses.replace(self.coadds, storageClass="CellCoadd") + del self.coadds_cell + del self.backgrounds + elif config.use_cell_coadds: del self.coadds else: del self.coadds_cell @@ -316,6 +321,23 @@ class CoaddMultibandFitBaseConfig( default=False, ) idGenerator = SkyMapIdGeneratorConfig.make_field() + image_type = pexConfig.ChoiceField( + "Which image type to expect for the input coadd. " + "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' " + "method behavior is determined by which type is actually passed in.", + allowed={ + "legacy": ( + "Read a lsst.cell_coadds.MultipleCellCoadd via 'coadds_cell` and restore 'background' " + "(if use_cell_coadd) or lsst.afw.image.Exposure via `coadds` (if not use_cell_coadd)." + ), + "future": ( + "Read lsst.images.cells.CellCoadd via the 'coadds' connection. use_cell_coadd is ignored." + ), + }, + dtype=str, + optional=False, + default="legacy", + ) def get_band_sets(self): """Get the set of bands required by the fit_coadd_multiband subtask. @@ -352,7 +374,7 @@ class CoaddMultibandFitBase: def build_catexps(self, butlerQC, inputRefs, inputs) -> list[CatalogExposureInputs]: id_tp = self.config.idGenerator.apply(butlerQC.quantum.dataId).catalog_id # This is a roundabout way of ensuring all inputs get sorted and matched - if self.config.use_cell_coadds: + if self.config.use_cell_coadds and self.config.image_type == "legacy": keys = ["cats_meas", "coadds_cell", "backgrounds"] else: keys = ["cats_meas", "coadds"] @@ -365,7 +387,9 @@ def build_catexps(self, butlerQC, inputRefs, inputs) -> list[CatalogExposureInpu for key, (refs, objs) in input_refs_objs.items() } cats = inputs_sorted["cats_meas"] - if self.config.use_cell_coadds: + if self.config.image_type == "future": + exps = {data_id: coadd.to_legacy() for data_id, coadd in inputs_sorted["coadds"].items()} + elif self.config.use_cell_coadds: exps = {} for data_id, background in inputs_sorted["backgrounds"].items(): mcc = inputs_sorted["coadds_cell"][data_id] diff --git a/python/lsst/pipe/tasks/fit_coadd_psf.py b/python/lsst/pipe/tasks/fit_coadd_psf.py index 9a54b288e..0bbde0fc2 100644 --- a/python/lsst/pipe/tasks/fit_coadd_psf.py +++ b/python/lsst/pipe/tasks/fit_coadd_psf.py @@ -31,6 +31,7 @@ import lsst.pipe.base as pipeBase import lsst.pipe.base.connectionTypes as cT +import dataclasses from abc import ABC, abstractmethod from pydantic.dataclasses import dataclass @@ -93,7 +94,11 @@ def __init__(self, *, config=None): if config is None: return - if config.use_cell_coadds: + if config.image_type == "future": + self.coadd = dataclasses.replace(self.coadd, storageClass="CellCoadd") + del self.coadd_cell + del self.background + elif config.use_cell_coadds: del self.coadd else: del self.coadd_cell @@ -168,6 +173,23 @@ class CoaddPsfFitConfig( doc="Task to fit PSF models for a single coadd", ) idGenerator = SkyMapIdGeneratorConfig.make_field() + image_type = pexConfig.ChoiceField( + "Which image type to expect for the input coadd. " + "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' " + "method behavior is determined by which type is actually passed in.", + allowed={ + "legacy": ( + "Read a lsst.cell_coadds.MultipleCellCoadd via 'coadd_cell` and restore 'background' " + "(if use_cell_coadd) or lsst.afw.image.Exposure via `coadd` (if not use_cell_coadd)." + ), + "future": ( + "Read lsst.images.cells.CellCoadd via the 'coadd' connection. use_cell_coadd is ignored." + ), + }, + dtype=str, + optional=False, + default="legacy", + ) class CoaddPsfFitTask(pipeBase.PipelineTask): @@ -191,7 +213,10 @@ def runQuantum(self, butlerQC, inputRefs, outputRefs): id_tp = self.config.idGenerator.apply(butlerQC.quantum.dataId).catalog_id dataId = inputRefs.cat_meas.dataId - if self.config.use_cell_coadds: + if self.config.image_type == "future": + coaddDataRef = inputRefs.coadd + exposure = inputs.pop('coadd').to_legacy() + elif self.config.use_cell_coadds: coaddDataRef = inputRefs.coadd_cell multiple_cell_coadd = inputs.pop('coadd_cell') background = inputs.pop('background') diff --git a/python/lsst/pipe/tasks/healSparseMapping.py b/python/lsst/pipe/tasks/healSparseMapping.py index e576fe844..6f74a5ace 100644 --- a/python/lsst/pipe/tasks/healSparseMapping.py +++ b/python/lsst/pipe/tasks/healSparseMapping.py @@ -27,6 +27,7 @@ "ConsolidateHealSparsePropertyMapTask"] from collections import defaultdict +import astropy.units import esutil import warnings import numbers @@ -706,8 +707,9 @@ def run(self, sky_map, tract, band, coadd_dict, input_map_dict, visit_summary_di band, tract, patch) continue - coadd_photo_calib = coadd_dict[patch].get(component="photoCalib") - coadd_zeropoint = 2.5*np.log10(coadd_photo_calib.getInstFluxAtZeroMagnitude()) + # LSST coadds are now always in nJy, and the lsst.images formats + # don't even have a PhotoCalib anymore. + coadd_zeropoint = float((1.0 * astropy.units.nJy).to_value(astropy.units.ABmag)) # Crop input_map to the inner polygon of the patch poly_vertices = patch_info.getInnerSkyPolygon(tract_info.getWcs()).getVertices() diff --git a/python/lsst/pipe/tasks/multiBand.py b/python/lsst/pipe/tasks/multiBand.py index 87055ea42..d1465fc60 100644 --- a/python/lsst/pipe/tasks/multiBand.py +++ b/python/lsst/pipe/tasks/multiBand.py @@ -21,8 +21,12 @@ __all__ = ["DetectCoaddSourcesConfig", "DetectCoaddSourcesTask", "MeasureMergedCoaddSourcesConfig", "MeasureMergedCoaddSourcesTask", + "DEEP_COADD_BACKGROUND_DOCSTRING", ] +import dataclasses + +import astropy.units import numpy as np from lsst.geom import Extent2I @@ -34,7 +38,11 @@ PipelineTaskConnections ) import lsst.pipe.base.connectionTypes as cT -from lsst.pex.config import Field, ConfigurableField +from lsst.pex.config import Field, ChoiceField, ConfigurableField +from lsst.cell_coadds import MultipleCellCoadd +from lsst.images import Mask, get_legacy_deep_coadd_mask_planes +from lsst.images.cells import CellCoadd +from lsst.images.fields import field_from_legacy_background from lsst.meas.algorithms import ( DynamicDetectionTask, ExceedsMaxVarianceScaleError, @@ -82,6 +90,18 @@ ############################################################################################################## + +# The default for DetectCoaddSourcesConfig.backgroundDescription: +DEEP_COADD_BACKGROUND_DOCSTRING = ( + "Background subtracted from the image when generating the Object catalog. " + "This intentionally oversubtracts the background to reduce blending and ensure " + "scattered light is subtracted. " + "Restoring this background does not restore all original backgrounds, " + "as the coadd was built from background-subtracted visit images; in most " + "cases this background term is actually quite small " +) + + class DetectCoaddSourcesConnections(PipelineTaskConnections, dimensions=("tract", "patch", "band", "skymap"), defaultTemplates={"inputCoaddName": "deep", "outputCoaddName": "deep"}): @@ -91,13 +111,13 @@ class DetectCoaddSourcesConnections(PipelineTaskConnections, storageClass="SourceCatalog", ) exposure = cT.Input( - doc="Exposure on which detections are to be performed. ", + doc="Exposure on which detections are to be performed (if useCellCoadd=False). ", name="{inputCoaddName}Coadd", storageClass="ExposureF", dimensions=("tract", "patch", "band", "skymap") ) exposure_cells = cT.Input( - doc="Exposure on which detections are to be performed. ", + doc="Exposure on which detections are to be performed (if useCellCoadd=True). ", name="{inputCoaddName}CoaddCell", storageClass="MultipleCellCoadd", dimensions=("tract", "patch", "band", "skymap"), @@ -131,11 +151,17 @@ def __init__(self, *, config=None): super().__init__(config=config) assert isinstance(config, DetectCoaddSourcesConfig) + if config.imageType == "future": + if config.useCellCoadds: + self.exposure_cells = dataclasses.replace(self.exposure_cells, storageClass="CellCoadd") + else: + self.exposure = dataclasses.replace(self.exposure, storageClass="CellCoadd") + self.outputExposure = dataclasses.replace(self.outputExposure, storageClass="CellCoadd") + del self.outputBackgrounds if config.useCellCoadds: del self.exposure else: del self.exposure_cells - if not self.config.forceExactBinning: del self.skyMap if self.config.writeOnlyBackgrounds: @@ -177,6 +203,38 @@ class DetectCoaddSourcesConfig(PipelineTaskConfig, pipelineConnections=DetectCoa "backgrounds from multiple patches as input." ) ) + imageType = ChoiceField( + "Which image type to use for the input and output coadd. " + "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' " + "method behavior is determined by which type is actually passed in.", + allowed={ + "legacy": ( + "Read a lsst.cell_coadds.MultipleCellCoadd (if useCellCoadd) or " + "lsst.afw.image.Exposure (if not useCellCoadd) and write an lsst.afw.image.Exposure." + ), + "future": ( + "Read and write lsst.images.cells.CellCoadd (useCellCoadd just " + "sets which of 'exposure' or 'exposure_cells' will be used for inputs). " + "The 'outputBackground' is deleted, writeEmptyBackgrounds is ignored, and " + "writeOnlyBackgrounds=True is invalid." + ), + }, + dtype=str, + optional=False, + default="legacy", + ) + backgroundName = Field( + "Name of the subtracted background, to be stored with the image when the input and " + "output images are lsst.images.cells.CellCoadd.", + dtype=str, + default="object", + ) + backgroundDescription = Field( + "Description of the subtracted background, to be stored with the image when the input and " + "output images are lsst.images.cells.CellCoadd.", + dtype=str, + default=DEEP_COADD_BACKGROUND_DOCSTRING, + ) def setDefaults(self): super().setDefaults() @@ -192,6 +250,16 @@ def setDefaults(self): # many bands as are defined", rather than the default of zero). self.idGenerator.packer.n_bands = None + def validate(self): + super().validate() + if self.imageType == "future": + if self.doScaleVariance: + raise ValueError("doScaleVariance=True is not compatible with imageType='future'") + if self.forceExactBinning: + raise ValueError("forceExactBinning=True is not compatible with imageType='future'") + if self.writeOnlyBackgrounds: + raise ValueError("writeOnlyBackgrounds=True is not compatible with imageType='future'") + class DetectCoaddSourcesTask(PipelineTask): """Detect sources on a single filter coadd. @@ -243,7 +311,13 @@ def runQuantum(self, butlerQC, inputRefs, outputRefs): if self.config.useCellCoadds: multiple_cell_coadd = inputs.pop("exposure_cells") - exposure = multiple_cell_coadd.stitch().asExposure() + match self.config.imageType: + case "legacy": + exposure = multiple_cell_coadd.stitch().asExposure() + case "future": + exposure = multiple_cell_coadd # conversion deferred to run(). + case _: + raise AssertionError(f"Invalid choice {self.config.imageType!r} for imageType.") else: exposure = inputs.pop("exposure") @@ -270,18 +344,27 @@ def runQuantum(self, butlerQC, inputRefs, outputRefs): ) as e: if self.config.writeEmptyBackgrounds: butlerQC.put(self._makeEmptyBackground(exposure, patchInfo), outputRefs.outputBackgrounds) - # Detection failed, so clear any leftover the detected mask planes. - for maskName in ["DETECTED", "DETECTED_NEGATIVE"]: - if maskName in exposure.mask.getMaskPlaneDict().keys(): - detectedMask = exposure.mask.getMaskPlane(maskName) - exposure.mask.clearMaskPlane(detectedMask) - butlerQC.put(exposure, outputRefs.outputExposure) + # Detection failed, so clear any leftover detected mask planes. + if isinstance(exposure, CellCoadd): + # If we passed a CellCoadd in, it won't be modified until 'run' + # is about to exit, and hence it can't have picked up a + # DETECTED_NEGATIVE plane, which can only be temporary here. + # But it might have a preexisting DETECTED plane (i.e. a union + # of the DETECTED plane from the warps) that we'd still want to + # clear. + exposure.mask.clear("DETECTED") + else: + for maskName in ["DETECTED", "DETECTED_NEGATIVE"]: + if maskName in exposure.mask.getMaskPlaneDict().keys(): + detectedMask = exposure.mask.getMaskPlane(maskName) + exposure.mask.clearMaskPlane(detectedMask) error = AnnotatedPartialOutputsError.annotate( e, self, exposure, log=self.log, ) + butlerQC.put(exposure, outputRefs.outputExposure) raise error from e butlerQC.put(outputs, outputRefs) @@ -295,7 +378,7 @@ def run(self, exposure, idFactory, expId, patchInfo=None): Parameters ---------- - exposure : `lsst.afw.image.Exposure` + exposure : `lsst.afw.image.Exposure` or `lsst.images.cells.CellCoadd`. Exposure on which to detect (may be background-subtracted and scaled, depending on configuration). idFactory : `lsst.afw.table.IdFactory` @@ -311,14 +394,26 @@ def run(self, exposure, idFactory, expId, patchInfo=None): result : `lsst.pipe.base.Struct` Results as a struct with attributes: - ``sources`` + ``outputSources`` Catalog of detections (`lsst.afw.table.SourceCatalog`). - ``backgrounds`` + ``outputBackgrounds`` List of backgrounds (`list`). + ``outputExposure`` + The background-subtracted coadd image, with its mask plane + updated to include detections. This will have the same type + as ``exposure``. """ + cell_coadd = None + if isinstance(exposure, CellCoadd): + cell_coadd = exposure + exposure = cell_coadd.to_legacy() if self.config.forceExactBinning: + if cell_coadd is not None: + raise ValueError("forceExactBinning=True is not compatible with CellCoadd inputs") exposure = self._cropToExactBinning(exposure, patchInfo) if self.config.doScaleVariance: + if cell_coadd is not None: + raise ValueError("doScaleVariance=True is not compatible with CellCoadd inputs") varScale = self.scaleVariance.run(exposure.maskedImage) exposure.getMetadata().add("VARIANCE_SCALE", varScale) backgrounds = afwMath.BackgroundList() @@ -333,7 +428,19 @@ def run(self, exposure, idFactory, expId, patchInfo=None): # inability to persist empty BackgroundList. emptyBg = self._makeEmptyBackground(exposure, patchInfo) backgrounds.append(emptyBg) - + if cell_coadd is not None: + cell_coadd.image.array[...] = exposure.image.array + cell_coadd.mask = Mask.from_legacy( + exposure.mask, plane_map=get_legacy_deep_coadd_mask_planes() + ).view(sky_projection=cell_coadd.sky_projection) + if backgrounds: + cell_coadd.backgrounds.add( + self.config.backgroundName, + field_from_legacy_background(backgrounds, unit=astropy.units.nJy), + self.config.backgroundDescription, + is_subtracted=True, + ) + exposure = cell_coadd return Struct(outputSources=sources, outputBackgrounds=backgrounds, outputExposure=exposure) def _cropToExactBinning(self, exposure, patchInfo): @@ -528,8 +635,11 @@ def __init__(self, *, config=None): del self.finalizedSourceTableHandles if not config.doAddFootprints: del self.scarletModels - - if config.useCellCoadds: + if self.config.imageType == "future": + self.exposure = dataclasses.replace(self.exposure, storageClass="CellCoadd") + del self.exposure_cells + del self.background + elif self.config.useCellCoadds: del self.exposure else: del self.exposure_cells @@ -593,6 +703,23 @@ class MeasureMergedCoaddSourcesConfig(PipelineTaskConfig, doc="Should be set to True if fake sources have been inserted into the input data." ) idGenerator = SkyMapIdGeneratorConfig.make_field() + imageType = ChoiceField( + "Which image type to expect for the input coadd. " + "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' " + "method behavior is determined by which type is actually passed in.", + allowed={ + "legacy": ( + "Read a lsst.cell_coadds.MultipleCellCoadd via 'exposure_cells` and restore 'background' " + "(if useCellCoadd) or lsst.afw.image.Exposure via `exposure` (if not useCellCoadd)." + ), + "future": ( + "Read lsst.images.cells.CellCoadd via the 'exposure' connection. useCellCoadd is ignored." + ), + }, + dtype=str, + optional=False, + default="legacy", + ) def setDefaults(self): super().setDefaults() @@ -672,25 +799,37 @@ def __init__(self, schema=None, peakSchema=None, initInputs=None, **kwargs): def runQuantum(self, butlerQC, inputRefs, outputRefs): inputs = butlerQC.get(inputRefs) - if self.config.useCellCoadds: - multiple_cell_coadd = inputs.pop("exposure_cells") - stitched_coadd = multiple_cell_coadd.stitch() + if self.config.imageType == "future": + coadd = inputs.pop("exposure") + band = inputRefs.exposure.dataId["band"] + # Instead of going directly from lsst.images.cells.CellCoadd to + # Exposure, it's cleaner for now to go through MultipleCellCoadd + # because the apCorrMap and ccdInputs need special handling - the + # cell-based versions can't be attached to Exposure. Eventually + # we'll rewrite the lower-level code to use the lsst.images + # equivalents natively. + coadd = coadd.to_legacy_cell_coadd() + elif self.config.useCellCoadds: + coadd = inputs.pop("exposure_cells") + band = inputRefs.exposure_cells.dataId["band"] + else: + coadd = inputs.pop("exposure") + band = inputRefs.exposure.dataId["band"] + if isinstance(coadd, MultipleCellCoadd): + stitched_coadd = coadd.stitch() exposure = stitched_coadd.asExposure() - background = inputs.pop("background") - exposure.image -= background.getImage() - + if self.config.imageType == "legacy": + background = inputs.pop("background") + exposure.image -= background.getImage() ccdInputs = stitched_coadd.ccds apCorrMap = stitched_coadd.ap_corr_map - band = inputRefs.exposure_cells.dataId["band"] else: - exposure = inputs.pop("exposure") - # Set psfcache - # move this to run after gen2 deprecation + exposure = coadd + # Set psfcache only when we don't have a cell-based coadd. exposure.getPsf().setCacheCapacity(self.config.psfCache) ccdInputs = exposure.getInfo().getCoaddInputs().ccds apCorrMap = exposure.getInfo().getApCorrMap() - band = inputRefs.exposure.dataId["band"] # Get unique integer ID for IdFactory and RNG seeds; only the latter # should really be used as the IDs all come from the input catalog. @@ -777,7 +916,7 @@ def run(self, exposure, sources, skyInfo, exposureId, ccdInputs=None, Parameters ---------- - exposure : `lsst.afw.exposure.Exposure` + exposure : `lsst.afw.image.Exposure` The input exposure on which measurements are to be performed. sources : `lsst.afw.table.SourceCatalog` A catalog built from the results of merged detections, or @@ -819,6 +958,8 @@ def run(self, exposure, sources, skyInfo, exposureId, ccdInputs=None, for maskPlane in self.config.measurement.plugins["base_PixelFlags"].masksFpCenter: exposure.mask.addMaskPlane(maskPlane) + self._ensureMaskPlanes() + self.measurement.run(sources, exposure, exposureId=exposureId) if self.config.doApCorr: @@ -853,3 +994,22 @@ def run(self, exposure, sources, skyInfo, exposureId, ccdInputs=None, results = Struct() results.outputSources = sources return results + + def _ensureMaskPlanes(self): + """Ensure the global mask dictionary has all of the mask planes + needed for PixelFlags algorithms. + + When mask planes are added, this essentially guarantees that the + corresponding PixelFlags columns will be wholly False, and usually + we'd prefer to remove them from the configuration. But those config + changes imply a schema changes, and that's not always viable (e.g. on + a release branch). + """ + needed = set(self.measurement.plugins["base_PixelFlags"].config.masksFpCenter) + needed.update(self.measurement.plugins["base_PixelFlags"].config.masksFpAnywhere) + existing = afwImage.MaskX().getMaskPlaneDict().keys() + for plane in sorted(needed - existing): + self.log.warning( + "Adding mask plane %r with no pixel set to satisfy PixelFlags configuration.", plane + ) + afwImage.MaskX.addMaskPlane(plane)