Coverage for python/lsst/pipe/tasks/rewrite_images/_rewrite_difference_image.py: 0%
54 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 10:17 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 10:17 -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__ = ("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 )
118 config: RewriteDifferenceImageConfig
120 def __init__(self, config: RewriteDifferenceImageConfig):
121 super().__init__(config=config)
122 if self.config.background_description is None:
123 del self.background
126class RewriteDifferenceImageConfig(
127 PipelineTaskConfig,
128 pipelineConnections=RewriteDifferenceImageConnections,
129):
130 instrumental_unit = Field(
131 "Unit for instrumental flux pixels (i.e. uncalibrated side of a PhotoCalib).",
132 dtype=str,
133 default="electron",
134 )
135 background_description = Field(
136 "Description of the subtracted background, to be stored with the image. "
137 "If None, no background will be attached.",
138 dtype=str,
139 optional=True,
140 default=None,
141 )
144class RewriteDifferenceImageTask(PipelineTask):
145 ConfigClass: ClassVar[type[RewriteDifferenceImageConfig]] = RewriteDifferenceImageConfig
146 config: RewriteDifferenceImageConfig
147 _DefaultName = "rewriteDifferenceImage"
149 def runQuantum(
150 self,
151 butlerQC: QuantumContext,
152 inputRefs: InputQuantizedConnection,
153 outputRefs: OutputQuantizedConnection,
154 ) -> None:
155 inputs = butlerQC.get(inputRefs)
156 difference_image = inputs.pop("legacy_exposure").get(parameters={"preserve_quantization": True})
157 visit_summary = inputs.pop("visit_summary")
158 coadd_data_ids_by_uuid = {h.ref.id: h.ref.dataId for h in inputs.pop("template_coadds")}
159 photo_calib: PhotoCalib = visit_summary.find(butlerQC.quantum.dataId["detector"]).getPhotoCalib()
160 outputs = self.run(
161 difference_image, photo_calib=photo_calib, **inputs, coadd_data_ids_by_uuid=coadd_data_ids_by_uuid
162 )
163 butlerQC.put(outputs, outputRefs)
165 def run(
166 self,
167 difference_image: DifferenceImage,
168 *,
169 photo_calib: PhotoCalib,
170 background: BackgroundList | None = None,
171 difference_kernel: Kernel,
172 template_psf: CoaddPsf,
173 template_metadata: PropertyList,
174 coadd_data_ids_by_uuid: Mapping[uuid.UUID, DataCoordinate],
175 ) -> Struct:
176 instrumental_unit = astropy.units.Unit(self.config.instrumental_unit)
177 difference_image.photometric_scaling = field_from_legacy_photo_calib(
178 photo_calib, bounds=difference_image.bbox, instrumental_unit=instrumental_unit
179 )
180 if background is not None and self.config.background_description:
181 difference_image.backgrounds.add(
182 "subtracted",
183 field_from_legacy_background(
184 background, bounds=difference_image.bbox, unit=difference_image.unit
185 ),
186 self.config.background_description,
187 is_subtracted=True,
188 )
189 difference_image.kernel = ImageBasisConvolutionKernel.from_legacy(difference_kernel)
190 difference_image.templates = DifferenceImageTemplateInfo.from_legacy(
191 difference_image.sky_projection.pixel_frame,
192 template_psf,
193 template_metadata,
194 coadd_data_ids_by_uuid,
195 )
196 return Struct(future_difference_image=difference_image)