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

88 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 09:13 +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 

24import lsst.geom as geom 

25from lsst.skymap import Index2D 

26 

27__all__ = ("UniformGrid",) 

28 

29 

30class UniformGrid: 

31 """A 2-dimensional integer grid. 

32 

33 Parameters 

34 ---------- 

35 cell_size : `lsst.geom.Extent2I` 

36 The size of each interior grid cell. 

37 shape : `lsst.skymap.Index2D` 

38 The number of cells in the grid in each dimension. 

39 padding : `int`, optional 

40 The number of pixels to pad the grid in each dimension. 

41 min : `lsst.geom.Point2I` or None, optional 

42 The minimum (lower left) corner of the interior grid, excluding 

43 ``padding``. If `None`, the minimum corner is set to be (0, 0). 

44 """ 

45 

46 def __init__( 

47 self, cell_size: geom.Extent2I, shape: Index2D, *, padding: int = 0, min: geom.Point2I | None = None 

48 ) -> None: 

49 self._cell_size = cell_size 

50 self._shape = shape 

51 self._padding = padding 

52 if min is None: 

53 min = geom.Point2I(0, 0) 

54 self._bbox = geom.Box2I( 

55 geom.Point2I(min.x, min.y), 

56 geom.Extent2I(cell_size.getX() * shape.x, cell_size.getY() * shape.y), 

57 ) 

58 

59 # Factory methods for constructing a UniformGrid 

60 @classmethod 

61 def from_bbox_shape(cls, bbox: geom.Box2I, shape: Index2D, padding: int = 0) -> UniformGrid: 

62 """Generate a UniformGrid instance from a bounding box and a shape. 

63 

64 Parameters 

65 ---------- 

66 bbox : `lsst.geom.Box2I` 

67 Bounding box of the full grid (without including ``padding``). 

68 shape : `lsst.skymap.Index2D` 

69 Number of cells in the grid in each dimension. 

70 Must divide the ``bbox`` width and height evenly. 

71 padding : `int`, optional 

72 The number of pixels to pad the grid in each dimension. 

73 

74 Returns 

75 ------- 

76 grid : `UniformGrid` 

77 A new UniformGrid instance. 

78 

79 Raises 

80 ------ 

81 LengthError 

82 Raised if ``shape`` dimensions do not divide the ``bbox`` 

83 dimensions evenly. 

84 """ 

85 cls._validate_bbox_shape(bbox, shape) 

86 

87 cell_size = geom.Extent2I(bbox.getWidth() // shape.x, bbox.getHeight() // shape.y) 

88 return cls(cell_size, shape, min=bbox.getMin(), padding=padding) 

89 

90 @classmethod 

91 def from_bbox_cell_size(cls, bbox: geom.Box2I, cell_size: geom.Extent2I, padding: int = 0) -> UniformGrid: 

92 """Generate a UniformGrid instance from a bounding box and a cell size. 

93 

94 Parameters 

95 ---------- 

96 bbox : `lsst.geom.Box2I` 

97 Bounding box of the full grid (without including ``padding``). 

98 cell_size : `lsst.geom.Extent2I` 

99 Size of each interior grid cell. 

100 Must divide the ``bbox`` width and height evenly. 

101 padding : `int`, optional 

102 The number of pixels to pad the grid in each dimension. 

103 

104 Returns 

105 ------- 

106 grid : `UniformGrid` 

107 A new UniformGrid instance. 

108 

109 Raises 

110 ------ 

111 IndexError 

112 Raised if ``cell_size`` dimensions do not divide the ``bbox`` 

113 dimensions evenly. 

114 """ 

115 cls._validate_bbox_cell_size(bbox, cell_size) 

116 shape = Index2D(bbox.getWidth() // cell_size.x, bbox.getHeight() // cell_size.y) 

117 return cls(cell_size, shape, padding=padding, min=bbox.getMin()) 

118 

119 # Methods to validate the input parameters 

120 @staticmethod 

121 def _validate_bbox_shape(bbox: geom.Box2I, shape: Index2D) -> None: 

122 if bbox.getWidth() % shape.x != 0: 

123 raise IndexError( 

124 f"Bounding box width {bbox.getWidth()} is not evenly divided by x shape {shape.x}." 

125 ) 

126 if bbox.getHeight() % shape.y != 0: 

127 raise IndexError( 

128 f"Bounding box height {bbox.getHeight()} is not evenly divided by y shape {shape.y}." 

129 ) 

130 

131 @staticmethod 

132 def _validate_bbox_cell_size(bbox: geom.Box2I, cell_size: geom.Extent2I) -> None: 

133 if bbox.getWidth() % cell_size.x != 0: 

134 raise IndexError( 

135 f"Bounding box width {bbox.getWidth()} is not evenly divided by x cell_size " 

136 f"{cell_size.getX()}." 

137 ) 

138 

139 if bbox.getHeight() % cell_size.y != 0: 

140 raise IndexError( 

141 f"Bounding box height {bbox.getHeight()} is not evenly divided by y cell_size " 

142 f"{cell_size.getY()}." 

143 ) 

144 

145 # Pythonic property getters 

146 @property 

147 def bbox(self) -> geom.Box2I: 

148 return self._bbox 

149 

150 @property 

151 def bbox_with_padding(self) -> geom.Box2I: 

152 return self._bbox.dilatedBy(self._padding) 

153 

154 @property 

155 def cell_size(self) -> geom.Extent2I: 

156 return self._cell_size 

157 

158 @property 

159 def shape(self) -> Index2D: 

160 return self._shape 

161 

162 @property 

163 def padding(self) -> int: 

164 return self._padding 

165 

166 # Implement C++ like getters 

167 def get_bbox(self) -> geom.Box2I: 

168 return self._bbox 

169 

170 def get_bbox_with_padding(self) -> geom.Box2I: 

171 return self.bbox_with_padding 

172 

173 def get_cell_size(self) -> geom.Extent2I: 

174 return self._cell_size 

175 

176 def get_shape(self) -> Index2D: 

177 return self._shape 

178 

179 def get_padding(self) -> int: 

180 return self._padding 

181 

182 # Dunder methods 

183 def __eq__(self, other: object) -> bool: 

184 if not isinstance(other, UniformGrid): 

185 return False 

186 return ( 

187 self._bbox == other._bbox 

188 and self._cell_size == other._cell_size 

189 and self._padding == other._padding 

190 ) 

191 

192 def __repr__(self) -> str: 

193 return ( 

194 f"UniformGrid(cell_size={repr(self.cell_size)}, shape={self.shape}, " 

195 f"min={repr(self.bbox.getMin())}, padding={self.padding})" 

196 ) 

197 

198 # Convenience methods 

199 def index(self, position: geom.Point2I) -> Index2D: 

200 """Index of the cell that contains the given point. 

201 

202 Parameters 

203 ---------- 

204 position : `lsst.geom.Point2I` 

205 A point in the grid. 

206 

207 Returns 

208 ------- 

209 index : `lsst.skymap.Index2D` 

210 A 2D index of the cell containing ``position``. 

211 

212 Raises 

213 ------ 

214 ValueError 

215 Raised if ``position`` is not within the grid's bounding box 

216 including the padding. 

217 """ 

218 if not self.bbox_with_padding.contains(position): 

219 raise ValueError( 

220 f"Position {position} is not within outer bounding box {self.bbox_with_padding}.s" 

221 ) 

222 

223 offset = position - self.bbox.getBegin() 

224 

225 if offset.x < 0: 

226 x = 0 

227 elif offset.x >= self.shape.x * self.cell_size.x: 

228 x = self.shape.x - 1 

229 else: 

230 x = offset.x // self.cell_size.x 

231 

232 if offset.y < 0: 

233 y = 0 

234 elif offset.y >= self.shape.y * self.cell_size.y: 

235 y = self.shape.y - 1 

236 else: 

237 y = offset.y // self.cell_size.y 

238 

239 return Index2D(x, y) 

240 

241 def min_of(self, index: Index2D) -> geom.Point2I: 

242 """Minimum point of a single cell's bounding box. 

243 

244 Parameters 

245 ---------- 

246 index : `~lsst.skymap.Index2D` 

247 A 2D index of the cell. 

248 

249 Returns 

250 ------- 

251 point : `lsst.geom.Point2I` 

252 The minimum point of the cell's bounding box. 

253 """ 

254 if not (0 <= index.x < self._shape.x and 0 <= index.y < self._shape.y): 

255 raise ValueError(f"{index} is not within the grid's shape {self._shape}.") 

256 

257 offset = geom.Point2I( 

258 -self._padding if index.x == 0 else 0, 

259 -self._padding if index.y == 0 else 0, 

260 ) 

261 return geom.Point2I( 

262 index.x * self.cell_size.x + self.bbox.getBeginX() + offset.x, 

263 index.y * self.cell_size.y + self.bbox.getBeginY() + offset.y, 

264 ) 

265 

266 def bbox_of(self, index: Index2D) -> geom.Box2I: 

267 """Bounding box of the cell at the given index. 

268 

269 Parameters 

270 ---------- 

271 index : `~lsst.skymap.Index2D` 

272 A 2D index of the cell. 

273 

274 Returns 

275 ------- 

276 bbox : `lsst.geom.Box2I` 

277 The bounding box of the cell. 

278 """ 

279 # Compute the buffer to add if ``index`` corresponds to the leftmost or 

280 # the rightmost cell or the topmost or the bottommost cell. 

281 buffer = geom.Extent2I( 

282 self.padding if index.x in {0, self.shape.x - 1} else 0, 

283 self.padding if index.y in {0, self.shape.y - 1} else 0, 

284 ) 

285 return geom.Box2I(self.min_of(index), self.cell_size + buffer)