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

88 statements  

« prev     ^ index     » next       coverage.py v7.3.3, created at 2023-12-16 14:04 +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 self._bbox == other._bbox and self._cell_size == other._cell_size 

187 

188 def __repr__(self) -> str: 

189 return ( 

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

191 f"min={repr(self.bbox.getMin())})" 

192 ) 

193 

194 # Convenience methods 

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

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

197 

198 Parameters 

199 ---------- 

200 position : `lsst.geom.Point2I` 

201 A point in the grid. 

202 

203 Returns 

204 ------- 

205 index : `lsst.skymap.Index2D` 

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

207 

208 Raises 

209 ------ 

210 ValueError 

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

212 including the padding. 

213 """ 

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

215 raise ValueError( 

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

217 ) 

218 

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

220 

221 if offset.x < 0: 

222 x = 0 

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

224 x = self.shape.x - 1 

225 else: 

226 x = offset.x // self.cell_size.x 

227 

228 if offset.y < 0: 

229 y = 0 

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

231 y = self.shape.y - 1 

232 else: 

233 y = offset.y // self.cell_size.y 

234 

235 return Index2D(x, y) 

236 

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

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

239 

240 Parameters 

241 ---------- 

242 index : `~lsst.skymap.Index2D` 

243 A 2D index of the cell. 

244 

245 Returns 

246 ------- 

247 point : `lsst.geom.Point2I` 

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

249 """ 

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

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

252 

253 offset = geom.Point2I( 

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

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

256 ) 

257 return geom.Point2I( 

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

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

260 ) 

261 

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

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

264 

265 Parameters 

266 ---------- 

267 index : `~lsst.skymap.Index2D` 

268 A 2D index of the cell. 

269 

270 Returns 

271 ------- 

272 bbox : `lsst.geom.Box2I` 

273 The bounding box of the cell. 

274 """ 

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

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

277 buffer = geom.Extent2I( 

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

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

280 ) 

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