Coverage for python / lsst / cell_coadds / _multiple_cell_coadd.py: 38%

91 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-24 08:40 +0000

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

25 

26from collections.abc import Iterable, Set 

27from typing import TYPE_CHECKING 

28 

29from lsst.geom import Box2I 

30 

31from ._common_components import CommonComponents, CommonComponentsProperties 

32from ._exploded_coadd import ExplodedCoadd 

33from ._grid_container import GridContainer 

34from ._single_cell_coadd import SingleCellCoadd 

35from ._stitched_coadd import StitchedCoadd 

36from ._uniform_grid import UniformGrid 

37 

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

39 from lsst.daf.base import PropertySet 

40 from lsst.geom import Extent2I 

41 

42 

43class MultipleCellCoadd(CommonComponentsProperties): 

44 """A data structure for coadds built from many overlapping cells. 

45 

46 Notes 

47 ----- 

48 `MultipleCellCoadd` is designed to be used both by measurement algorithms 

49 that are able to take advantage of cell boundaries and overlap regions 

50 (which can use the ``.cells`` attribute to access `SingleCellCoadd` objects 

51 directly) and measurement algorithms that just want one image and don't 

52 care (or don't care much) about discontinuities (which can use `stitch` to 

53 obtain such an image). 

54 

55 Indexing with `Box2I` yields a `MultipleCellCoadd` view containing just the 

56 cells that overlap that region. 

57 """ 

58 

59 def __init__( 

60 self, 

61 cells: Iterable[SingleCellCoadd], 

62 grid: UniformGrid, 

63 outer_cell_size: Extent2I, 

64 psf_image_size: Extent2I, 

65 *, 

66 common: CommonComponents, 

67 inner_bbox: Box2I | None = None, 

68 ): 

69 self._grid = grid 

70 self._outer_cell_size = outer_cell_size 

71 self._psf_image_size = psf_image_size 

72 self._common = common 

73 cells_builder = GridContainer[SingleCellCoadd](self._grid.shape) 

74 self._mask_fraction_names: set[str] = set() 

75 

76 for cell in cells: 

77 index = cell.identifiers.cell 

78 cells_builder[index] = cell 

79 cell_bbox = self._grid.bbox_of(index) 

80 if not cell.outer.bbox.contains(cell_bbox): 

81 raise ValueError( 

82 f"Cell at index {index} has outer bbox {cell.outer.bbox}, " 

83 f"which does not contain {self._grid.bbox_of(index)}." 

84 ) 

85 if not cell_bbox.contains(cell.inner.bbox): 

86 raise ValueError( 

87 f"Cell at index {index} has inner bbox {cell.inner.bbox}, " 

88 f"which is not contained by {self._grid.bbox_of(index)}." 

89 ) 

90 if cell.outer.bbox.getDimensions() != self._outer_cell_size: 

91 raise ValueError( 

92 f"Cell at index {index} has outer dimensions {cell.outer.bbox.getDimensions()}, " 

93 f"but coadd expects {self._outer_cell_size}." 

94 ) 

95 if cell.psf_image.getDimensions() != self._psf_image_size: 

96 raise ValueError( 

97 f"Cell at index {index} has PSF image with dimensions {cell.psf_image.getDimensions()}, " 

98 f"but coadd expects {self._psf_image_size}." 

99 ) 

100 

101 self._cells = cells_builder 

102 n_noise_realizations = {len(cell.outer.noise_realizations) for cell in self._cells.values()} 

103 self._n_noise_realizations = n_noise_realizations.pop() 

104 if n_noise_realizations: 

105 n_noise_realizations.add(self._n_noise_realizations) 

106 raise ValueError( 

107 f"Inconsistent number of noise realizations ({n_noise_realizations}) between cells." 

108 ) 

109 

110 # Finish the construction without relying on the first and last of 

111 # self._cells so we can construct an instance with partial list. 

112 indices = list(cells_builder.indices()) 

113 max_inner_bbox = Box2I( 

114 grid.bbox_of(indices[0]).getMin(), 

115 grid.bbox_of(indices[-1]).getMax(), 

116 ) 

117 

118 if inner_bbox is None: 

119 inner_bbox = max_inner_bbox 

120 elif not max_inner_bbox.contains(inner_bbox): 

121 raise ValueError( 

122 f"Requested inner bounding box {inner_bbox} is not fully covered by these " 

123 f"cells (bbox is {max_inner_bbox})." 

124 ) 

125 self._inner_bbox = inner_bbox 

126 

127 @property 

128 def cells(self) -> GridContainer[SingleCellCoadd]: 

129 """The grid of single-cell coadds, indexed by (y, x).""" 

130 return self._cells 

131 

132 @property 

133 def n_noise_realizations(self) -> int: 

134 """The number of noise realizations cells are guaranteed to have.""" 

135 return self._n_noise_realizations 

136 

137 @property 

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

139 """The names of all mask planes whose fractions were propagated in any 

140 cell. 

141 

142 Cells that do not have a mask fraction for a particular name may be 

143 assumed to have the fraction for that mask plane uniformly zero. 

144 """ 

145 return self._mask_fraction_names 

146 

147 @property 

148 def grid(self) -> UniformGrid: 

149 """Object that defines the inner geometry for all cells.""" 

150 return self._grid 

151 

152 @property 

153 def outer_cell_size(self) -> Extent2I: 

154 """Dimensions of the outer region of each cell.""" 

155 return self._outer_cell_size 

156 

157 @property 

158 def psf_image_size(self) -> Extent2I: 

159 """Dimensions of PSF model images.""" 

160 return self._psf_image_size 

161 

162 @property 

163 def outer_bbox(self) -> Box2I: 

164 """The rectangular region fully covered by all cell outer bounding 

165 boxes. 

166 """ 

167 return self.grid.bbox_with_padding 

168 

169 @property 

170 def inner_bbox(self) -> Box2I: 

171 """The rectangular region fully covered by all cell inner bounding 

172 boxes. 

173 """ 

174 return self._inner_bbox 

175 

176 @property 

177 def common(self) -> CommonComponents: 

178 # Docstring inherited. 

179 return self._common 

180 

181 def stitch(self, bbox: Box2I | None = None) -> StitchedCoadd: 

182 """Return a contiguous (but in general discontinuous) coadd by 

183 stitching together inner cells. 

184 

185 Parameters 

186 ---------- 

187 bbox : `Box2I`, optional 

188 Region for the returned coadd; default is ``self.inner_bbox``. 

189 

190 Returns 

191 ------- 

192 stitched : `StitchedCellCoadd` 

193 Contiguous coadd covering the given area. Each image plane is 

194 actually constructed when first accessed, not when this method 

195 is called. 

196 """ 

197 # In the future, stitching algorithms that apply ramps to smooth 

198 # discontinuities may also be provided; we'd implement that by having 

199 # this return different types (from a common ABC), perhaps dispatched 

200 # by an enum. 

201 return StitchedCoadd(self, bbox=bbox) 

202 

203 def explode(self, pad_psfs_with: float | None = None) -> ExplodedCoadd: 

204 """Return a coadd whose image planes stitch together the outer regions 

205 of each cell, duplicating pixels in the overlap regions. 

206 

207 Parameters 

208 ---------- 

209 pad_psfs_with : `float` or None, optional 

210 A floating-point value to pad PSF images with so each PSF-image 

211 cell has the same dimensions as the image (outer) cell it 

212 corresponds to. If `None`, PSF images will not be padded and the 

213 full PSF image will generally be smaller than the exploded image it 

214 corresponds to. 

215 

216 Returns 

217 ------- 

218 exploded : `ExplodedCoadd` 

219 Exploded version of the coadd. 

220 """ 

221 return ExplodedCoadd(self, pad_psfs_with=pad_psfs_with) 

222 

223 @classmethod 

224 def read_fits(cls, filename: str) -> MultipleCellCoadd: 

225 """Read a MultipleCellCoadd from a FITS file. 

226 

227 Parameters 

228 ---------- 

229 filename : `str` 

230 The path to the FITS file to read. 

231 

232 Returns 

233 ------- 

234 cell_coadd : `MultipleCellCoadd` 

235 The MultipleCellCoadd object read from the FITS file. 

236 """ 

237 from ._fits import CellCoaddFitsReader # Avoid circular import. 

238 

239 reader = CellCoaddFitsReader(filename) 

240 return reader.readAsMultipleCellCoadd() 

241 

242 @classmethod 

243 def readFits(cls, *args, **kwargs) -> MultipleCellCoadd: # type: ignore[no-untyped-def] 

244 """Alias to `read_fits` method. 

245 

246 Notes 

247 ----- 

248 This method exists for compatability with the rest of the codebase. 

249 The presence of this method allows for reading in via 

250 `lsst.obs.base.formatters.FitsGenericFormatter`. 

251 Whenever possible, use `read_fits` instead, since this method may be 

252 deprecated in the near future. 

253 """ 

254 return cls.read_fits(*args, **kwargs) 

255 

256 def write_fits(self, filename: str, overwrite: bool = False, metadata: PropertySet | None = None) -> None: 

257 """Write the coadd as a FITS file. 

258 

259 Parameters 

260 ---------- 

261 filename : `str` 

262 The path to the FITS file to write. 

263 overwrite : `bool`, optional 

264 Whether to overwrite an existing file? 

265 metadata : `~lsst.daf.base.PropertySet`, optional 

266 Additional metadata to write to the FITS header. 

267 """ 

268 from ._fits import writeMultipleCellCoaddAsFits # Avoid circular import. 

269 

270 writeMultipleCellCoaddAsFits(self, filename, overwrite=overwrite, metadata=metadata) 

271 

272 def writeFits(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] 

273 """Alias to `write_fits` method. 

274 

275 Notes 

276 ----- 

277 This method exists for compatibility with the rest of the codebase. 

278 The presence of this method allows for persistence via 

279 `lsst.obs.base.formatters.FitsGenericFormatter`. 

280 Whenever possible, use `write_fits` instead, since this method may be 

281 deprecated in the near future. 

282 """ 

283 self.write_fits(*args, **kwargs)