Coverage for python/lsst/pipe/tasks/rewrite_images/_rewrite_cell_coadd.py: 0%

83 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-07 08:52 +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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("RewriteCellCoaddConnections", "RewriteCellCoaddTask", "RewriteCellCoaddConfig") 

25 

26from typing import Any, ClassVar 

27 

28import astropy.io.fits 

29import astropy.time 

30import astropy.units 

31 

32import lsst.pipe.base.connectionTypes as cT 

33from lsst.afw.image import Exposure as LegacyExposure 

34from lsst.afw.image import Mask as LegacyMask 

35from lsst.afw.math import BackgroundList 

36from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd 

37from lsst.images import Box, Mask, get_legacy_deep_coadd_mask_planes 

38from lsst.images.cells import CellCoadd 

39from lsst.images.fields import field_from_legacy_background 

40from lsst.images.fits import ExtensionKey, FitsOpaqueMetadata 

41from lsst.meas.algorithms import SubtractBackgroundTask 

42from lsst.pex.config import ConfigurableField, Field 

43from lsst.pipe.base import ( 

44 AlgorithmError, 

45 PipelineTask, 

46 PipelineTaskConfig, 

47 PipelineTaskConnections, 

48 Struct, 

49) 

50from lsst.skymap import BaseSkyMap 

51 

52 

53class RewriteCellCoaddConnections( 

54 PipelineTaskConnections, 

55 dimensions={"patch", "band"}, 

56 defaultTemplates={"legacy_prefix": "legacy_", "future_prefix": ""}, 

57): 

58 legacy_cell_coadd = cT.Input( 

59 "deep_coadd_cell_predetection", 

60 storageClass="MultipleCellCoadd", 

61 dimensions={"patch", "band"}, 

62 doc="The pre-detection cell coadd to convert.", 

63 ) 

64 object_background = cT.Input( 

65 "deep_coadd_background", 

66 storageClass="Background", 

67 dimensions={"patch", "band"}, 

68 doc="The background model used to generate the object catalog, to be subtracted from the coadd.", 

69 minimum=0, 

70 ) 

71 object_mask = cT.Input( 

72 "{legacy_prefix}deep_coadd.mask", 

73 storageClass="Mask", 

74 dimensions={"patch", "band"}, 

75 doc="The mask to use for the coadd, including the final DETECTED mask plane.", 

76 minimum=0, 

77 ) 

78 alternate_background_coadd = cT.Input( 

79 "pretty_coadd_extra_bg_subtracted", 

80 storageClass="ExposureF", 

81 dimensions={"patch", "band"}, 

82 doc="A coadd to subtract from ``legacy_cell_coadd`` and fit a background to.", 

83 minimum=0, 

84 ) 

85 sky_map = cT.Input( 

86 doc="Description of the skymap's tracts and patches.", 

87 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

88 storageClass="SkyMap", 

89 dimensions=("skymap",), 

90 ) 

91 future_cell_coadd = cT.Output( 

92 "{future_prefix}deep_coadd", 

93 storageClass="CellCoadd", 

94 dimensions={"patch", "band"}, 

95 doc="The output CellCoadd.", 

96 ) 

97 

98 config: RewriteCellCoaddConfig 

99 

100 def __init__(self, config: RewriteCellCoaddConfig | None): 

101 super().__init__(config=config) 

102 if not self.config.do_alternate_background_fit: 

103 del self.alternate_background_coadd 

104 

105 

106class RewriteCellCoaddConfig( 

107 PipelineTaskConfig, 

108 pipelineConnections=RewriteCellCoaddConnections, 

109): 

110 object_background_description = Field( 

111 "Description of the object background, to be stored with the image.", 

112 dtype=str, 

113 default=( 

114 "Background subtracted from the image when generating the Object catalog. " 

115 "This intentionally oversubtracts the background to reduce blending and ensure " 

116 "scattered light is subtracted. " 

117 "Restoring this background does not restore all original backgrounds, " 

118 "as the coadd was built from background-subtracted visit images; in most " 

119 "cases this background term is actually quite small." 

120 ), 

121 ) 

122 do_alternate_background_fit = Field( 

123 "Whether to fit an alternate background to the difference between " 

124 "the alternate_background_coadd and the main coadd.", 

125 dtype=bool, 

126 default=True, 

127 ) 

128 alternate_background_fit = ConfigurableField( 

129 target=SubtractBackgroundTask, 

130 doc=( 

131 "Configuration for fitting the alternate background. " 

132 "Ignored if do_alternate_background_fit is False" 

133 ), 

134 ) 

135 alternate_background_name = Field( 

136 "Name for the alternate background attached to the image. " 

137 "ignored if do_alternate_background_fit is False.", 

138 dtype=str, 

139 default="pretty", 

140 ) 

141 alternate_background_description = Field( 

142 "Description for the alternate background attached to the image. " 

143 "Ignored if do_alternate_background_fit is False.", 

144 dtype=str, 

145 default=( 

146 "An alternate background optimized for visually attractive RGB images. " 

147 "'Subtracting' this background will generally *add* flux, correcting " 

148 "for most oversubtraction problems, but leaving in scattered light and " 

149 "some instrumental backgrounds in some cases. " 

150 "Because this background is fit to a difference of two wholly different " 

151 "coadds that may have different input images, it can also be pulled " 

152 "up or down by bright variable or transient objects." 

153 ), 

154 ) 

155 

156 def setDefaults(self) -> None: 

157 super().setDefaults() 

158 self.alternate_background_fit.useApprox = False 

159 self.alternate_background_fit.binSize = 64 

160 self.alternate_background_fit.ignoredPixelMask = ["NO_DATA", "SAT"] 

161 

162 

163class RewriteCellCoaddTask(PipelineTask): 

164 ConfigClass: ClassVar[type[RewriteCellCoaddConfig]] = RewriteCellCoaddConfig 

165 config: RewriteCellCoaddConfig 

166 _DefaultName = "rewriteCellCoadd" 

167 

168 def __init__(self, *args: Any, **kwargs: Any) -> None: 

169 super().__init__(*args, **kwargs) 

170 self.makeSubtask("alternate_background_fit") 

171 

172 def run( 

173 self, 

174 legacy_cell_coadd: LegacyMultipleCellCoadd, 

175 sky_map: BaseSkyMap, 

176 *, 

177 object_background: BackgroundList | None = None, 

178 object_mask: LegacyMask | None = None, 

179 alternate_background_coadd: LegacyExposure | None = None, 

180 ) -> Struct: 

181 tract_info = sky_map[legacy_cell_coadd.identifiers.tract] 

182 patch_bbox = tract_info[legacy_cell_coadd.identifiers.patch].getOuterBBox() 

183 future_cell_coadd = CellCoadd.from_legacy( 

184 legacy_cell_coadd, 

185 tract_info=tract_info, 

186 bbox=Box.from_legacy(patch_bbox), 

187 ) 

188 if getattr(future_cell_coadd, "_opaque_metadata", None) is None: 

189 future_cell_coadd._opaque_metadata = FitsOpaqueMetadata() 

190 primary_header = future_cell_coadd._opaque_metadata.headers.setdefault( 

191 ExtensionKey(), astropy.io.fits.Header() 

192 ) 

193 primary_header.set("INSTRUME", "LSSTCam") 

194 primary_header.set("ORIGIN", "NSF-DOE Vera C. Rubin Observatory") 

195 primary_header.set("TELESCOP", "Rubin:Simonyi") 

196 primary_header.set("DATE", astropy.time.Time.now().fits, "UTC date this HDU was written.") 

197 if object_mask is not None: 

198 future_cell_coadd.mask.clear() 

199 future_cell_coadd.mask.update( 

200 Mask.from_legacy(object_mask, plane_map=get_legacy_deep_coadd_mask_planes()) 

201 ) 

202 else: 

203 # If we can't load the object DETECTED plane, clear the one that's 

204 # present, as it's (misleadingly, in this context) the union of the 

205 # calibrateImage detections. 

206 self.log.warning( 

207 "No object mask found; clearing DETECTED and using other predetection mask planes." 

208 ) 

209 future_cell_coadd.mask.clear("DETECTED") 

210 

211 if self.config.do_alternate_background_fit: 

212 if alternate_background_coadd is not None: 

213 alt_bg_diff = future_cell_coadd.to_legacy_exposure(copy=True) 

214 alt_bg_diff.maskedImage -= alternate_background_coadd.getMaskedImage() 

215 try: 

216 alt_bg_result = self.alternate_background_fit.run(alt_bg_diff, stats=False) 

217 except AlgorithmError: 

218 self.log.exception("Could not fit alternate background to diff; rewriting without it.") 

219 else: 

220 alt_bg = field_from_legacy_background(alt_bg_result.background, unit=astropy.units.nJy) 

221 future_cell_coadd.backgrounds.add( 

222 self.config.alternate_background_name, 

223 alt_bg, 

224 self.config.alternate_background_description, 

225 ) 

226 else: 

227 self.log.warning("No alternate background coadd found; rewriting without it.") 

228 elif alternate_background_coadd is not None: 

229 raise TypeError( 

230 "alternate_background_coadd provided but config.do_alternate_background_fit=False" 

231 ) 

232 if object_background is not None: 

233 # We subtract the deep_coadd_background using afw to avoid tiny 

234 # differences between the afw and lsst.images spline 

235 # implementations, in the definition of deep_coadd itself (though 

236 # that will still ultimately differ from the original due to lossy 

237 # compression). 

238 future_cell_coadd.image.array -= object_background.getImage().array 

239 future_cell_coadd.backgrounds.add( 

240 "object", 

241 field_from_legacy_background(object_background, unit=astropy.units.nJy), 

242 self.config.object_background_description, 

243 is_subtracted=True, 

244 ) 

245 else: 

246 self.log.warning( 

247 "No object background model found; rewriting coadd without subtracting background." 

248 ) 

249 return Struct(future_cell_coadd=future_cell_coadd)