Coverage for python/lsst/pipe/tasks/rewrite_images/_rewrite_difference_image.py: 0%
57 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 23:40 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 23:40 +0000
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__ = ("RewriteDifferenceImageConnections", "RewriteDifferenceImageTask", "RewriteDifferenceImageConfig")
26import uuid
27from collections.abc import Mapping
28from typing import ClassVar
30import astropy.units
32import lsst.pipe.base.connectionTypes as cT
33from lsst.afw.image import PhotoCalib
34from lsst.afw.math import BackgroundList, Kernel
35from lsst.daf.base import PropertyList
36from lsst.daf.butler import DataCoordinate
37from lsst.images import DifferenceImage, DifferenceImageTemplateInfo
38from lsst.images.convolution_kernels import ImageBasisConvolutionKernel
39from lsst.images.fields import field_from_legacy_background, field_from_legacy_photo_calib
40from lsst.meas.algorithms import CoaddPsf
41from lsst.pex.config import Field
42from lsst.pipe.base import (
43 InputQuantizedConnection,
44 OutputQuantizedConnection,
45 PipelineTask,
46 PipelineTaskConfig,
47 PipelineTaskConnections,
48 QuantumContext,
49 Struct,
50)
53class RewriteDifferenceImageConnections(
54 PipelineTaskConnections,
55 dimensions={"visit", "detector"},
56 defaultTemplates={"legacy_prefix": "legacy_", "future_prefix": ""},
57):
58 legacy_exposure = cT.Input(
59 "{legacy_prefix}difference_image",
60 # We expect the repository storage class to be ExposureF, but we set
61 # the storage class we want to DifferenceImage so the butler can do
62 # most of the conversion on read, and in doing so preserve the
63 # quantization so we don't have doubly-lossless compression.
64 storageClass="DifferenceImage",
65 dimensions={"visit", "detector"},
66 deferLoad=True, # So we can pass preserve_quantization=True as a parameter.
67 doc="The input image to convert.",
68 )
69 template_psf = cT.Input(
70 "template_detector.psf",
71 storageClass="Psf",
72 dimensions={"visit", "detector"},
73 doc="The PSF of the detector-level template, which holds some provenance information.",
74 )
75 template_metadata = cT.Input(
76 "template_detector.metadata",
77 storageClass="PropertyList",
78 dimensions={"visit", "detector"},
79 doc="The metadata of the detector-level template, which holds some provenance information.",
80 )
81 template_coadds = cT.Input(
82 "template_coadd",
83 storageClass="ExposureF",
84 dimensions={"tract", "patch", "band"},
85 deferLoad=True,
86 multiple=True,
87 doc=(
88 "The template coadds that may have gone into the detector-level template. "
89 "This is a dummy input that is not actually loaded; it is needed only to connect "
90 "data IDs to butler dataset IDs."
91 ),
92 )
93 visit_summary = cT.Input(
94 "visit_summary",
95 storageClass="ExposureCatalog",
96 dimensions={"visit"},
97 doc="A visit summary catalog with the PhotoCalib that was already applied to the image's pixels.",
98 )
99 difference_kernel = cT.Input(
100 "difference_kernel",
101 storageClass="MatchingKernel",
102 dimensions={"visit", "detector"},
103 doc="The kernel used to match the template to the science image.",
104 )
105 background = cT.Input(
106 "difference_image_background",
107 storageClass="Background",
108 dimensions={"visit", "detector"},
109 doc="The background model that was subtracted from this image.",
110 )
111 future_difference_image = cT.Output(
112 "{future_prefix}difference_image",
113 storageClass="DifferenceImage",
114 dimensions={"visit", "detector"},
115 doc="The output difference image.",
116 )
117 exposure_record_proxy = cT.Input(
118 "raw",
119 storageClass="Exposure",
120 dimensions={"exposure", "detector"},
121 doc="An arbitrary input used to provide an 'exposure' dimension record."
122 )
124 config: RewriteDifferenceImageConfig
126 def __init__(self, config: RewriteDifferenceImageConfig):
127 super().__init__(config=config)
128 if self.config.background_description is None:
129 del self.background
132class RewriteDifferenceImageConfig(
133 PipelineTaskConfig,
134 pipelineConnections=RewriteDifferenceImageConnections,
135):
136 instrumental_unit = Field(
137 "Unit for instrumental flux pixels (i.e. uncalibrated side of a PhotoCalib).",
138 dtype=str,
139 default="electron",
140 )
141 background_description = Field(
142 "Description of the subtracted background, to be stored with the image. "
143 "If None, no background will be attached.",
144 dtype=str,
145 optional=True,
146 default=None,
147 )
150class RewriteDifferenceImageTask(PipelineTask):
151 ConfigClass: ClassVar[type[RewriteDifferenceImageConfig]] = RewriteDifferenceImageConfig
152 config: RewriteDifferenceImageConfig
153 _DefaultName = "rewriteDifferenceImage"
155 def runQuantum(
156 self,
157 butlerQC: QuantumContext,
158 inputRefs: InputQuantizedConnection,
159 outputRefs: OutputQuantizedConnection,
160 ) -> None:
161 exposure_record = inputRefs.exposure_record_proxy.dataId.records["exposure"]
162 del inputRefs.exposure_record_proxy
163 inputs = butlerQC.get(inputRefs)
164 difference_image = inputs.pop("legacy_exposure").get(
165 parameters={"preserve_quantization": True, "exposure_record": exposure_record}
166 )
167 visit_summary = inputs.pop("visit_summary")
168 coadd_data_ids_by_uuid = {h.ref.id: h.ref.dataId for h in inputs.pop("template_coadds")}
169 photo_calib: PhotoCalib = visit_summary.find(butlerQC.quantum.dataId["detector"]).getPhotoCalib()
170 outputs = self.run(
171 difference_image, photo_calib=photo_calib, **inputs, coadd_data_ids_by_uuid=coadd_data_ids_by_uuid
172 )
173 butlerQC.put(outputs, outputRefs)
175 def run(
176 self,
177 difference_image: DifferenceImage,
178 *,
179 photo_calib: PhotoCalib,
180 background: BackgroundList | None = None,
181 difference_kernel: Kernel,
182 template_psf: CoaddPsf,
183 template_metadata: PropertyList,
184 coadd_data_ids_by_uuid: Mapping[uuid.UUID, DataCoordinate],
185 ) -> Struct:
186 instrumental_unit = astropy.units.Unit(self.config.instrumental_unit)
187 difference_image.photometric_scaling = field_from_legacy_photo_calib(
188 photo_calib, bounds=difference_image.bbox, instrumental_unit=instrumental_unit
189 )
190 if background is not None and self.config.background_description:
191 difference_image.backgrounds.add(
192 "subtracted",
193 field_from_legacy_background(
194 background, bounds=difference_image.bbox, unit=difference_image.unit
195 ),
196 self.config.background_description,
197 is_subtracted=True,
198 )
199 difference_image.kernel = ImageBasisConvolutionKernel.from_legacy(difference_kernel)
200 difference_image.templates = DifferenceImageTemplateInfo.from_legacy(
201 difference_image.sky_projection.pixel_frame,
202 template_psf,
203 template_metadata,
204 coadd_data_ids_by_uuid,
205 log=self.log,
206 )
207 return Struct(future_difference_image=difference_image)