Coverage for python/lsst/cell_coadds/_uniform_grid.py: 40%
88 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-08 11:19 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-08 11:19 +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/>.
22from __future__ import annotations
24import lsst.geom as geom
25from lsst.skymap import Index2D
27__all__ = ("UniformGrid",)
30class UniformGrid:
31 """A 2-dimensional integer grid.
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 """
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 )
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.
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.
74 Returns
75 -------
76 grid : `UniformGrid`
77 A new UniformGrid instance.
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)
87 cell_size = geom.Extent2I(bbox.getWidth() // shape.x, bbox.getHeight() // shape.y)
88 return cls(cell_size, shape, min=bbox.getMin(), padding=padding)
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.
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.
104 Returns
105 -------
106 grid : `UniformGrid`
107 A new UniformGrid instance.
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())
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 )
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 )
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 )
145 # Pythonic property getters
146 @property
147 def bbox(self) -> geom.Box2I:
148 return self._bbox
150 @property
151 def bbox_with_padding(self) -> geom.Box2I:
152 return self._bbox.dilatedBy(self._padding)
154 @property
155 def cell_size(self) -> geom.Extent2I:
156 return self._cell_size
158 @property
159 def shape(self) -> Index2D:
160 return self._shape
162 @property
163 def padding(self) -> int:
164 return self._padding
166 # Implement C++ like getters
167 def get_bbox(self) -> geom.Box2I:
168 return self._bbox
170 def get_bbox_with_padding(self) -> geom.Box2I:
171 return self.bbox_with_padding
173 def get_cell_size(self) -> geom.Extent2I:
174 return self._cell_size
176 def get_shape(self) -> Index2D:
177 return self._shape
179 def get_padding(self) -> int:
180 return self._padding
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
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 )
194 # Convenience methods
195 def index(self, position: geom.Point2I) -> Index2D:
196 """Index of the cell that contains the given point.
198 Parameters
199 ----------
200 position : `lsst.geom.Point2I`
201 A point in the grid.
203 Returns
204 -------
205 index : `lsst.skymap.Index2D`
206 A 2D index of the cell containing ``position``.
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 )
219 offset = position - self.bbox.getBegin()
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
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
235 return Index2D(x, y)
237 def min_of(self, index: Index2D) -> geom.Point2I:
238 """Minimum point of a single cell's bounding box.
240 Parameters
241 ----------
242 index : `~lsst.skymap.Index2D`
243 A 2D index of the cell.
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}.")
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 )
262 def bbox_of(self, index: Index2D) -> geom.Box2I:
263 """Bounding box of the cell at the given index.
265 Parameters
266 ----------
267 index : `~lsst.skymap.Index2D`
268 A 2D index of the cell.
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)