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-19 03:42 -0400

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.units 

29 

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 

48 

49 

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 ) 

94 

95 config: RewriteCellCoaddConfig 

96 

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 

101 

102 

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 ) 

152 

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"] 

158 

159 

160class RewriteCellCoaddTask(PipelineTask): 

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

162 config: RewriteCellCoaddConfig 

163 _DefaultName = "rewriteCellCoadd" 

164 

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

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

167 self.makeSubtask("alternate_background_fit") 

168 

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") 

198 

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)