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

106 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-09 04:19 -0700

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 asExposure(self) -> ExposureF: 

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

158 result = ExposureF(self.asMaskedImage()) 

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

160 result.setWcs(self._cell_coadd.wcs) 

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

162 if self.units is CoaddUnits.nJy: 

163 result.setPhotoCalib(PhotoCalib(1.0)) 

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

165 

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

167 # 

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

169 # or just a tract+patch ID; 

170 # 

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

172 # 

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

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

175 

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

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

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

179 # point. 

180 

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

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

183 # and transmission curves. 

184 result.setPsf(self.psf) 

185 

186 return result 

187 

188 @property 

189 def ap_corr_map(self) -> StitchedCoaddApCorrMap: 

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

191 

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

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

194 object has the fields to correct as keys and a 

195 `StitchedApertureCorrection` for each field. 

196 

197 Notes 

198 ----- 

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

200 """ 

201 if self._ap_corr_map is None: 

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

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

204 for field_name in field_names: 

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

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

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

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

209 

210 self._ap_corr_map = ap_corr_map 

211 

212 return self._ap_corr_map 

213 

214 @property 

215 def ccds(self) -> StitchedExposureCatalog: 

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

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

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

219 combinations. 

220 """ 

221 if self._ccds is None: 

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

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

224 gc[idx] = scc.inputs 

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

226 

227 return self._ccds 

228 

229 @property 

230 def visits(self) -> StitchedExposureCatalog: 

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

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

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

234 """ 

235 if self._visits is None: 

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

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

238 gc[idx] = tuple( 

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

240 ) 

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

242 

243 return self._visits