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-08 02:08 -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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("RewriteDifferenceImageConnections", "RewriteDifferenceImageTask", "RewriteDifferenceImageConfig") 

25 

26import uuid 

27from collections.abc import Mapping 

28from typing import ClassVar 

29 

30import astropy.units 

31 

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) 

51 

52 

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 

118 config: RewriteDifferenceImageConfig 

119 

120 def __init__(self, config: RewriteDifferenceImageConfig): 

121 super().__init__(config=config) 

122 if self.config.background_description is None: 

123 del self.background 

124 

125 

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 ) 

142 

143 

144class RewriteDifferenceImageTask(PipelineTask): 

145 ConfigClass: ClassVar[type[RewriteDifferenceImageConfig]] = RewriteDifferenceImageConfig 

146 config: RewriteDifferenceImageConfig 

147 _DefaultName = "rewriteDifferenceImage" 

148 

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) 

164 

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)