Coverage for python/lsst/pipe/tasks/rewrite_images/_rewrite_cell_coadd.py: 0%
73 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-12 00:53 -0700
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-12 00:53 -0700
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = ("RewriteCellCoaddConnections", "RewriteCellCoaddTask", "RewriteCellCoaddConfig")
26from typing import Any, ClassVar
28import astropy.units
30import lsst.pipe.base.connectionTypes as cT
31from lsst.afw.image import Exposure as LegacyExposure
32from lsst.afw.image import Mask as LegacyMask
33from lsst.afw.math import BackgroundList
34from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd
35from lsst.images import Box, Mask, get_legacy_deep_coadd_mask_planes
36from lsst.images.cells import CellCoadd
37from lsst.images.fields import field_from_legacy_background
38from lsst.meas.algorithms import SubtractBackgroundTask
39from lsst.pex.config import ConfigurableField, Field
40from lsst.pipe.base import (
41 AlgorithmError,
42 PipelineTask,
43 PipelineTaskConfig,
44 PipelineTaskConnections,
45 Struct,
46)
47from lsst.skymap import BaseSkyMap
50class RewriteCellCoaddConnections(
51 PipelineTaskConnections,
52 dimensions={"patch", "band"},
53 defaultTemplates={"legacy_prefix": "legacy_", "future_prefix": ""},
54):
55 legacy_cell_coadd = cT.Input(
56 "deep_coadd_cell_predetection",
57 storageClass="MultipleCellCoadd",
58 dimensions={"patch", "band"},
59 doc="The pre-detection cell coadd to convert.",
60 )
61 object_background = cT.Input(
62 "deep_coadd_background",
63 storageClass="Background",
64 dimensions={"patch", "band"},
65 doc="The background model used to generate the object catalog, to be subtracted from the coadd.",
66 minimum=0,
67 )
68 object_mask = cT.Input(
69 "{legacy_prefix}deep_coadd.mask",
70 storageClass="Mask",
71 dimensions={"patch", "band"},
72 doc="The mask to use for the coadd, including the final DETECTED mask plane.",
73 minimum=0,
74 )
75 alternate_background_coadd = cT.Input(
76 "pretty_coadd_extra_bg_subtracted",
77 storageClass="ExposureF",
78 dimensions={"patch", "band"},
79 doc="A coadd to subtract from ``legacy_cell_coadd`` and fit a background to.",
80 minimum=0,
81 )
82 sky_map = cT.Input(
83 doc="Description of the skymap's tracts and patches.",
84 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
85 storageClass="SkyMap",
86 dimensions=("skymap",),
87 )
88 future_cell_coadd = cT.Output(
89 "{future_prefix}deep_coadd",
90 storageClass="CellCoadd",
91 dimensions={"patch", "band"},
92 doc="The output CellCoadd.",
93 )
95 config: RewriteCellCoaddConfig
97 def __init__(self, config: RewriteCellCoaddConfig | None):
98 super().__init__(config=config)
99 if not self.config.do_alternate_background_fit:
100 del self.alternate_background_coadd
103class RewriteCellCoaddConfig(
104 PipelineTaskConfig,
105 pipelineConnections=RewriteCellCoaddConnections,
106):
107 object_background_description = Field(
108 "Description of the object background, to be stored with the image.",
109 dtype=str,
110 default=(
111 "Background subtracted from the image when generating the Object catalog. "
112 "This intentionally oversubtracts the background to reduce blending and ensure "
113 "scattered light is subtracted. "
114 "Restoring this background does not restore all original backgrounds, "
115 "as the coadd was built from background-subtracted visit images; in most "
116 "cases this background term is actually quite small."
117 ),
118 )
119 do_alternate_background_fit = Field(
120 "Whether to fit an alternate background to the difference between "
121 "the alternate_background_coadd and the main coadd.",
122 dtype=bool,
123 default=True,
124 )
125 alternate_background_fit = ConfigurableField(
126 target=SubtractBackgroundTask,
127 doc=(
128 "Configuration for fitting the alternate background. "
129 "Ignored if do_alternate_background_fit is False"
130 ),
131 )
132 alternate_background_name = Field(
133 "Name for the alternate background attached to the image. "
134 "ignored if do_alternate_background_fit is False.",
135 dtype=str,
136 default="pretty",
137 )
138 alternate_background_description = Field(
139 "Description for the alternate background attached to the image. "
140 "Ignored if do_alternate_background_fit is False.",
141 dtype=str,
142 default=(
143 "An alternate background optimized for visually attractive RGB images. "
144 "'Subtracting' this background will generally *add* flux, correcting "
145 "for most oversubtraction problems, but leaving in scattered light and "
146 "some instrumental backgrounds in some cases. "
147 "Because this background is fit to a difference of two wholly different "
148 "coadds that may have different input images, it can also be pulled "
149 "up or down by bright variable or transient objects."
150 ),
151 )
153 def setDefaults(self) -> None:
154 super().setDefaults()
155 self.alternate_background_fit.useApprox = False
156 self.alternate_background_fit.binSize = 64
157 self.alternate_background_fit.ignoredPixelMask = ["NO_DATA", "SAT"]
160class RewriteCellCoaddTask(PipelineTask):
161 ConfigClass: ClassVar[type[RewriteCellCoaddConfig]] = RewriteCellCoaddConfig
162 config: RewriteCellCoaddConfig
163 _DefaultName = "rewriteCellCoadd"
165 def __init__(self, *args: Any, **kwargs: Any) -> None:
166 super().__init__(*args, **kwargs)
167 self.makeSubtask("alternate_background_fit")
169 def run(
170 self,
171 legacy_cell_coadd: LegacyMultipleCellCoadd,
172 sky_map: BaseSkyMap,
173 *,
174 object_background: BackgroundList | None = None,
175 object_mask: LegacyMask | None = None,
176 alternate_background_coadd: LegacyExposure | None = None,
177 ) -> Struct:
178 tract_info = sky_map[legacy_cell_coadd.identifiers.tract]
179 patch_bbox = tract_info[legacy_cell_coadd.identifiers.patch].getOuterBBox()
180 future_cell_coadd = CellCoadd.from_legacy(
181 legacy_cell_coadd,
182 tract_info=tract_info,
183 bbox=Box.from_legacy(patch_bbox),
184 )
185 if object_mask is not None:
186 future_cell_coadd.mask.clear()
187 future_cell_coadd.mask.update(
188 Mask.from_legacy(object_mask, plane_map=get_legacy_deep_coadd_mask_planes())
189 )
190 else:
191 # If we can't load the object DETECTED plane, clear the one that's
192 # present, as it's (misleadingly, in this context) the union of the
193 # calibrateImage detections.
194 self.log.warning(
195 "No object mask found; clearing DETECTED and using other predetection mask planes."
196 )
197 future_cell_coadd.mask.clear("DETECTED")
199 if self.config.do_alternate_background_fit:
200 if alternate_background_coadd is not None:
201 alt_bg_diff = future_cell_coadd.to_legacy_exposure(copy=True)
202 alt_bg_diff.maskedImage -= alternate_background_coadd.getMaskedImage()
203 try:
204 alt_bg_result = self.alternate_background_fit.run(alt_bg_diff, stats=False)
205 except AlgorithmError:
206 self.log.exception("Could not fit alternate background to diff; rewriting without it.")
207 else:
208 alt_bg = field_from_legacy_background(alt_bg_result.background, unit=astropy.units.nJy)
209 future_cell_coadd.backgrounds.add(
210 self.config.alternate_background_name,
211 alt_bg,
212 self.config.alternate_background_description,
213 )
214 else:
215 self.log.warning("No alternate background coadd found; rewriting without it.")
216 elif alternate_background_coadd is not None:
217 raise TypeError(
218 "alternate_background_coadd provided but config.do_alternate_background_fit=False"
219 )
220 if object_background is not None:
221 # We subtract the deep_coadd_background using afw to avoid tiny
222 # differences between the afw and lsst.images spline
223 # implementations, in the definition of deep_coadd itself (though
224 # that will still ultimately differ from the original due to lossy
225 # compression).
226 future_cell_coadd.image.array -= object_background.getImage().array
227 future_cell_coadd.backgrounds.add(
228 "object",
229 field_from_legacy_background(object_background, unit=astropy.units.nJy),
230 self.config.object_background_description,
231 is_subtracted=True,
232 )
233 else:
234 self.log.warning(
235 "No object background model found; rewriting coadd without subtracting background."
236 )
237 return Struct(future_cell_coadd=future_cell_coadd)