Coverage for python/lsst/cell_coadds/_stitched_coadd.py: 30%

109 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-08 01:35 -0800

1# This file is part of cell_coadds. 

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__ = ("StitchedCoadd",) 

25 

26import dataclasses 

27from collections.abc import Iterator, Set 

28from functools import partial 

29from typing import TYPE_CHECKING 

30 

31from lsst.afw.image import ExposureF, FilterLabel, PhotoCalib 

32from lsst.geom import Box2I, Point2I 

33 

34from ._common_components import CoaddUnits, CommonComponents, CommonComponentsProperties 

35from ._grid_container import GridContainer 

36from ._identifiers import ObservationIdentifiers 

37from ._image_planes import ImagePlanes, ViewImagePlanes 

38from ._stitched_aperture_correction import StitchedApertureCorrection 

39from ._stitched_exposure_catalog import StitchedExposureCatalog 

40from ._stitched_image_planes import StitchedImagePlanes 

41from ._stitched_psf import StitchedPsf 

42from ._uniform_grid import UniformGrid 

43from .typing_helpers import StitchedCoaddApCorrMap 

44 

45if TYPE_CHECKING: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 from ._multiple_cell_coadd import MultipleCellCoadd 

47 

48 

49class StitchedCoadd(StitchedImagePlanes, CommonComponentsProperties): 

50 """A lazy-evaluation coadd that stitches together images from adjacent 

51 cells. 

52 

53 Parameters 

54 ---------- 

55 cell_coadd : `MultipleCellCoadd` 

56 Cell-based coadd to stitch together. 

57 bbox : `Box2I`, optional 

58 The region over which a contiguous coadd is desired. Defaults to 

59 ``cell_coadd.outer_bbox``. 

60 

61 Notes 

62 ----- 

63 This class simply inserts subimages from each cell into the full image, 

64 doing so when an attribute is first accessed to avoid stitching together 

65 planes that may never be accessed. 

66 

67 A `StitchedCoadd` cannot be serialized in FITS format directly. Instead, 

68 the recommended way is to serialize the `MultipleCellCoadd` instance that 

69 was used to construct the object and reconstruct the `StitchedCoadd` by 

70 calling the `stitch` method on it. A less recommended way is to call the 

71 `asExposure` method to get an `lsst.afw.image.Exposure` object and persist 

72 that to the disk. 

73 """ 

74 

75 def __init__(self, cell_coadd: MultipleCellCoadd, *, bbox: Box2I | None = None): 

76 super().__init__() 

77 if bbox is None: 

78 bbox = cell_coadd.outer_bbox 

79 elif not cell_coadd.outer_bbox.contains(bbox): 

80 raise ValueError( 

81 f"Cell coadd inner bounding box {cell_coadd.outer_bbox} does not " 

82 f"contain stitch target area {bbox}." 

83 ) 

84 self._bbox = bbox 

85 self._cell_coadd = cell_coadd 

86 self._psf: StitchedPsf | None = None 

87 self._ap_corr_map: StitchedCoaddApCorrMap | None = None 

88 self._ccds: StitchedExposureCatalog | None = None 

89 self._visits: StitchedExposureCatalog | None = None 

90 self._common = cell_coadd.common 

91 

92 @property 

93 def bbox(self) -> Box2I: 

94 # Docstring inherited. 

95 return self._bbox 

96 

97 @property 

98 def grid(self) -> UniformGrid: 

99 """Object that defines the piecewise grid (of inner cell regions) that 

100 this object stitches together. 

101 

102 This may include cells outside the region covered by these image 

103 planes. 

104 """ 

105 return self._cell_coadd.grid 

106 

107 def _iter_cell_planes(self) -> Iterator[ImagePlanes]: 

108 # Docstring inherited. 

109 x_max, y_max = self.grid.index(self.bbox.getMax()) 

110 

111 for cell in self._cell_coadd.cells.values(): 

112 bbox = cell.inner.bbox 

113 if cell.identifiers.cell.x == 0: 

114 # This is a special case for the first column of cells. 

115 bbox.include(Point2I(cell.outer.bbox.beginX, cell.outer.bbox.centerY)) 

116 elif cell.identifiers.cell.x == x_max: 

117 # This is a special case for the last column of cells. 

118 bbox.include(Point2I(cell.outer.bbox.endX, cell.outer.bbox.centerY)) 

119 

120 if cell.identifiers.cell.y == 0: 

121 # This is a special case for the last row of cells. 

122 bbox.include(Point2I(cell.outer.bbox.centerX, cell.outer.bbox.beginY)) 

123 elif cell.identifiers.cell.y == y_max: 

124 # This is a special case for the first row of cells. 

125 bbox.include(Point2I(cell.outer.bbox.centerX, cell.outer.bbox.endY)) 

126 

127 bbox.clip(cell.outer.bbox) 

128 make_view = partial(cell.make_view, bbox=bbox) 

129 yield ViewImagePlanes(cell.outer, bbox=bbox, make_view=make_view) 

130 

131 @property 

132 def n_noise_realizations(self) -> int: 

133 # Docstring inherited. 

134 return self._cell_coadd.n_noise_realizations 

135 

136 @property 

137 def mask_fraction_names(self) -> Set[str]: 

138 # Docstring inherited. 

139 return self._cell_coadd.mask_fraction_names 

140 

141 @property 

142 def psf(self) -> StitchedPsf: 

143 """The piecewise PSF of this image.""" 

144 if self._psf is None: 

145 self._psf = StitchedPsf( 

146 self._cell_coadd.cells.rebuild_transformed(lambda cell: cell.psf_image.convertD()), 

147 self._cell_coadd.grid, 

148 ) 

149 return self._psf 

150 

151 @property 

152 def common(self) -> CommonComponents: 

153 # Docstring inherited. 

154 return self._cell_coadd.common 

155 

156 def set_cell_edges(self, *, edge_width: int = 1, edge_mask_name: str = "CELL_EDGE") -> None: 

157 """Set a mask bit indicating the inner cell edges. 

158 

159 Parameters 

160 ---------- 

161 edge_width : `int`, optional 

162 The width of the edge region to flag, in pixels. 

163 edge_mask_name : `str`, optional 

164 The name of the mask plane to add for the edge region. 

165 """ 

166 for cell in self._cell_coadd.cells.values(): 

167 cell._set_cell_edges(edge_width=edge_width, edge_mask_name=edge_mask_name) 

168 

169 def asExposure( 

170 self, 

171 *, 

172 noise_index: int | None = None, 

173 ) -> ExposureF: 

174 """Return an `lsst.afw.image.Exposure` view of this piecewise image. 

175 

176 Parameters 

177 ---------- 

178 noise_index : `int` or `None`, optional 

179 If specified, return an exposure containing the specified noise 

180 realization instead of the data image. 

181 

182 Returns 

183 ------- 

184 exposure : `lsst.afw.image.ExposureF` 

185 The stitched exposure. 

186 """ 

187 result = ExposureF(self.asMaskedImage(noise_index=noise_index)) 

188 # Exposure components derived from "common" components are all simple. 

189 result.setWcs(self._cell_coadd.wcs) 

190 result.setFilter(FilterLabel(band=self.band)) 

191 if self.units is CoaddUnits.nJy: 

192 result.setPhotoCalib(PhotoCalib(1.0)) 

193 result.metadata["BUNIT"] = "nJy" 

194 

195 # We can't do result.setId here, because: 

196 # 

197 # - we don't know whether this should be a packed tract+patch+band ID 

198 # or just a tract+patch ID; 

199 # 

200 # - we don't know how to pack the information we have anyway. 

201 # 

202 # Maybe DM-31924 will provide a solution to at least the latter. 

203 # result.setId(self._cell_coadd.identifiers.patch) 

204 

205 # We could add CoaddInputs here, but without WCS, PSF, etc in them; 

206 # it's not clear that's good enough or even useful, given that the cell 

207 # provide a much more clean view of what the inputs are at any given 

208 # point. 

209 

210 # PSF is the first of many components that need piecewise 

211 # implementations. More to do here for at least aperture corrections 

212 # and transmission curves. 

213 result.setPsf(self.psf) 

214 

215 return result 

216 

217 @property 

218 def ap_corr_map(self) -> StitchedCoaddApCorrMap: 

219 """Stitch the aperture correction maps from the cell coadd. 

220 

221 This converts the aperture correction maps from each cell into a single 

222 `ApCorrMap` that quacks like `lsst.afw.image.ApCorrMap`. The resulting 

223 object has the fields to correct as keys and a 

224 `StitchedApertureCorrection` for each field. 

225 

226 Notes 

227 ----- 

228 These cannot be attached to an `~lsst.afw.image.Exposure` object. 

229 """ 

230 if self._ap_corr_map is None: 

231 ap_corr_map: dict[str, StitchedApertureCorrection] = {} 

232 field_names = self._cell_coadd.cells.arbitrary.aperture_corrected_algorithms 

233 for field_name in field_names: 

234 gc = GridContainer[float](shape=self.grid.shape) 

235 for scc in self._cell_coadd.cells.values(): 

236 gc[scc.identifiers.cell] = scc.aperture_correction_map[field_name] 

237 ap_corr_map[field_name] = StitchedApertureCorrection(self.grid, gc) 

238 

239 self._ap_corr_map = ap_corr_map 

240 

241 return self._ap_corr_map 

242 

243 @property 

244 def ccds(self) -> StitchedExposureCatalog: 

245 """An ExposureCatalog-like object that contains the information about 

246 the ``ObservationIdentifiers`` at any point on the coadd. It can also 

247 be used as an iterable to iterate over the unique (visit, detector) 

248 combinations. 

249 """ 

250 if self._ccds is None: 

251 gc = GridContainer[tuple[ObservationIdentifiers, ...]](shape=self.grid.shape) 

252 for idx, scc in self._cell_coadd.cells.items(): 

253 gc[idx] = tuple(scc.inputs.keys()) 

254 self._ccds = StitchedExposureCatalog(self.grid, gc) 

255 

256 return self._ccds 

257 

258 @property 

259 def visits(self) -> StitchedExposureCatalog: 

260 """An ExposureCatalog-like object that contains the information about 

261 the ``ObservationIdentifiers`` at any point on the coadd. It can also 

262 be used as an iterable to iterate over the unique visits. 

263 """ 

264 if self._visits is None: 

265 gc = GridContainer[tuple[ObservationIdentifiers, ...]](shape=self.grid.shape) 

266 for idx, scc in self._cell_coadd.cells.items(): 

267 gc[idx] = tuple( 

268 sorted({dataclasses.replace(ccd_input, detector=-1) for ccd_input in scc.inputs}) 

269 ) 

270 self._visits = StitchedExposureCatalog(self.grid, gc) 

271 

272 return self._visits