Coverage for python/lsst/cell_coadds/_single_cell_coadd.py: 55%

83 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-03 08:02 +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__ = ( 

25 "CoaddInputs", 

26 "SingleCellCoadd", 

27) 

28 

29from collections.abc import Mapping, Set 

30from dataclasses import dataclass 

31from typing import TYPE_CHECKING 

32 

33from lsst.afw.geom import Quadrupole 

34from lsst.afw.image import ImageD, ImageF 

35from lsst.geom import Box2I 

36 

37from ._coadd_ap_corr_map import EMPTY_AP_CORR_MAP 

38from ._common_components import CommonComponents, CommonComponentsProperties 

39from ._image_planes import ViewImagePlanes 

40from .typing_helpers import ImageLike, SingleCellCoaddApCorrMap 

41 

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

43 from ._identifiers import CellIdentifiers, ObservationIdentifiers 

44 from ._image_planes import ImagePlanes, OwnedImagePlanes 

45 

46 

47@dataclass 

48class CoaddInputs: 

49 """Container for inputs to the coaddition process.""" 

50 

51 overlaps_center: bool 

52 """Whether a single (detector, visit) observation overlaps the center 

53 of the cell.""" 

54 

55 overlap_fraction: float 

56 """Fraction of the cell that is covered by the overlap region.""" 

57 

58 unmasked_overlap_fraction: float 

59 """Fraction of the cell covered by this detector, excluding 

60 rejected pixels.""" 

61 

62 weight: float 

63 """Weight to be used for this input.""" 

64 

65 psf_shape: Quadrupole 

66 """Second order moments of the PSF.""" 

67 

68 psf_shape_flag: bool 

69 """Flag indicating whether the PSF shape measurement was successful.""" 

70 

71 

72class SingleCellCoadd(CommonComponentsProperties): 

73 """A single coadd cell, built only from input images that completely 

74 overlap that cell. 

75 

76 Parameters 

77 ---------- 

78 outer : `OwnedImagePlanes` 

79 The actual coadded images. 

80 psf : `ImageD` 

81 The coadded PSF image. 

82 inner_bbox : `Box2I` 

83 The bounding box of the inner region of this cell; must be disjoint 

84 with but adjacent to all other cell inner regions. 

85 inputs : `Mapping` [`ObservationIdentifiers`, `CoaddInputs`] 

86 Identifiers of observations that contributed to this cell. 

87 common : `CommonComponents` 

88 Image attributes common to all cells in a patch. 

89 identifiers : `CellIdentifiers` 

90 Struct of identifiers for this cell. 

91 aperture_correction_map : `frozendict` [`str`, `float`], optional 

92 Mapping of algorithm name to aperture correction value for this cell. 

93 

94 Notes 

95 ----- 

96 At present we assume a single PSF image per cell is sufficient to capture 

97 spatial variability, which seems adequate given the results we have so far 

98 and the cell sizes we intend to use. 

99 """ 

100 

101 def __init__( 

102 self, 

103 outer: OwnedImagePlanes, 

104 *, 

105 psf: ImageD, 

106 inner_bbox: Box2I, 

107 inputs: Mapping[ObservationIdentifiers, CoaddInputs], 

108 common: CommonComponents, 

109 identifiers: CellIdentifiers, 

110 aperture_correction_map: SingleCellCoaddApCorrMap = EMPTY_AP_CORR_MAP, 

111 ): 

112 assert outer.bbox.contains( 

113 inner_bbox 

114 ), f"Cell inner bbox {inner_bbox} is not contained by outer bbox {outer.bbox}." 

115 self._outer = outer 

116 self._psf = psf 

117 self._inner_bbox = inner_bbox 

118 self._inner = ViewImagePlanes(outer, bbox=inner_bbox, make_view=self.make_view) 

119 self._common = common 

120 # Remove any duplicate elements in the input, sort them and pack them 

121 # as a dictionary. 

122 # TODO: Remove support for inputs as None when bumping to v1.0 . 

123 self._inputs: Mapping[ObservationIdentifiers, CoaddInputs] 

124 if inputs: 

125 self._inputs = dict.fromkeys( 

126 sorted(set(inputs)), 

127 CoaddInputs( 

128 overlaps_center=False, 

129 overlap_fraction=0.0, 

130 unmasked_overlap_fraction=0.0, 

131 weight=0.0, 

132 psf_shape=Quadrupole(), 

133 psf_shape_flag=True, 

134 ), 

135 ) 

136 self._inputs.update(inputs) 

137 else: 

138 self._inputs = {} 

139 self._identifiers = identifiers 

140 self._aperture_correction_map = aperture_correction_map 

141 

142 def _set_cell_edges( 

143 self, 

144 *, 

145 edge_width: int = 1, 

146 edge_mask_name: str = "CELL_EDGE", 

147 ) -> None: 

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

149 

150 Parameters 

151 ---------- 

152 edge_width : `int`, optional 

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

154 edge_mask_name : `str`, optional 

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

156 """ 

157 self._inner.mask.addMaskPlane(edge_mask_name) 

158 self._inner.mask.array[:, :edge_width] |= self._inner.mask.getPlaneBitMask(edge_mask_name) 

159 self._inner.mask.array[:, -edge_width:] |= self._inner.mask.getPlaneBitMask(edge_mask_name) 

160 self._inner.mask.array[:edge_width, :] |= self._inner.mask.getPlaneBitMask(edge_mask_name) 

161 self._inner.mask.array[-edge_width:, :] |= self._inner.mask.getPlaneBitMask(edge_mask_name) 

162 

163 @property 

164 def inner(self) -> ImagePlanes: 

165 """Image planes within the inner region of this cell that is disjoint 

166 with all other cell inner regions. 

167 """ 

168 return self._inner 

169 

170 @property 

171 def outer(self) -> ImagePlanes: 

172 """Image planes within the full outer region of this cell.""" 

173 return self._outer 

174 

175 @property 

176 def psf_image(self) -> ImageF: 

177 """The coadded PSF image.""" 

178 return self._psf 

179 

180 @property 

181 # TODO: Remove the option of returning empty tuple in v1.0. 

182 def inputs(self) -> Mapping[ObservationIdentifiers, CoaddInputs]: 

183 """Identifiers for the input images that contributed to this cell, 

184 sorted by their `visit` attribute first, and then by `detector`. 

185 """ 

186 return self._inputs 

187 

188 @property 

189 def visit_count(self) -> int: 

190 """Number of visits that contributed to this cell.""" 

191 return len(self.inputs) 

192 

193 @property 

194 def identifiers(self) -> CellIdentifiers: 

195 """Struct of unique identifiers for this cell.""" 

196 # This overrides the patch-level property from 

197 # CommonComponentsProperties to provide cell-level information. 

198 return self._identifiers 

199 

200 @property 

201 def common(self) -> CommonComponents: 

202 # Docstring inherited. 

203 return self._common 

204 

205 @property 

206 def aperture_correction_map(self) -> SingleCellCoaddApCorrMap: 

207 """Mapping of algorithm name to aperture correction values. 

208 

209 Returns 

210 ------- 

211 aperture_correction_map : `frozendict` [`str`, float] 

212 Mapping of algorithm name to aperture correction values. 

213 """ 

214 return self._aperture_correction_map 

215 

216 @property 

217 def aperture_corrected_algorithms(self) -> Set[str]: 

218 """An iterable of algorithm names that have aperture correction values. 

219 

220 Returns 

221 ------- 

222 aperture_corrected_algorithms : `tuple` [`str`, ...] 

223 List of algorithms that have aperture correction values. 

224 """ 

225 if self._aperture_correction_map: 

226 return self._aperture_correction_map.keys() 

227 

228 return set() 

229 

230 def make_view(self, image: ImageLike, bbox: Box2I | None = None) -> ImageLike: 

231 """Make a view of an image, optionally within a given bounding box. 

232 

233 Parameters 

234 ---------- 

235 image : `ImageLike` 

236 The image to make a view of. 

237 bbox : `Box2I`, optional 

238 The bounding box within which to make the view. 

239 

240 Returns 

241 ------- 

242 image_view: `ImageLike` 

243 The view of the image. 

244 """ 

245 if bbox is None: 

246 bbox = self._inner_bbox 

247 return image[bbox]