Skip to content
102 changes: 86 additions & 16 deletions python/lsst/pipe/tasks/deblendCoaddSourcesPipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -131,6 +139,25 @@ class DeblendCoaddSourcesMultiConfig(PipelineTaskConfig,
doc="Task to deblend an images in multiple bands"
)
idGenerator = SkyMapIdGeneratorConfig.make_field()
imageType = ChoiceField(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are you anticipating keeping this around and adding to allowed at some point, even after legacy is deprecated? Or did you mainly want better documentation via the allowed dict than you'd get from a boolean Field?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not planning to keep it around forever, but on this ticket I really wished that useCellCoadd had been a ChoiceField instead a boolean, because then I would have been able to use that instead of inventing another config field and leaving useCellCoadd sort of dangling in a lot of cases. So this is probably fighting the last war, but I figured I'd leave room for a plot twist in the migration just in case.

"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):
Expand Down Expand Up @@ -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)
Expand All @@ -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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there lists that aren't zip-iteration compatible, or do you just mean that the bands have to be in the same order as the coadds?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I mean it should have corresponding elements in the same order.

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
Expand Down
30 changes: 27 additions & 3 deletions python/lsst/pipe/tasks/fit_coadd_multiband.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"]
Expand All @@ -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]
Expand Down
29 changes: 27 additions & 2 deletions python/lsst/pipe/tasks/fit_coadd_psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -168,6 +173,23 @@ class CoaddPsfFitConfig(
doc="Task to fit PSF models for a single coadd",
)
idGenerator = SkyMapIdGeneratorConfig.make_field()
image_type = pexConfig.ChoiceField(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to define this once in a module somewhere, or is it safer to duplicate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstrings pretty much always differ, even if only slightly sometimes.

"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):
Expand All @@ -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')
Expand Down
6 changes: 4 additions & 2 deletions python/lsst/pipe/tasks/healSparseMapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"ConsolidateHealSparsePropertyMapTask"]

from collections import defaultdict
import astropy.units
import esutil
import warnings
import numbers
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading