Coverage for python/lsst/images/_concrete_bounds.py: 18%
91 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 08:08 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 08:08 +0000
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("SerializableBounds",)
16import pydantic
17import shapely
19from ._cell_grid import CellGridBounds
20from ._geom import Bounds, Box, NoOverlapError
21from ._intersection_bounds import IntersectionBounds
22from ._polygon import Polygon, Region, RegionSerializationModel
25# The cyclic dependencies prevent this from going in _intersection_bounds.py.
26class IntersectionBoundsSerializationModel(pydantic.BaseModel):
27 """Serialization model for `IntersectionBounds`."""
29 a: SerializableBounds
30 b: SerializableBounds
32 def deserialize(self) -> IntersectionBounds:
33 """Deserialize into an `IntersectionBounds` instance."""
34 return IntersectionBounds(self.a.deserialize(), self.b.deserialize())
37type SerializableBounds = (
38 Box | CellGridBounds | RegionSerializationModel | IntersectionBoundsSerializationModel
39)
42IntersectionBoundsSerializationModel.model_rebuild()
45def _intersect_box(lhs: Box, rhs: Bounds) -> Bounds:
46 """Return the intersection between a `Box` and an arbitrary `Bounds`
47 object.
49 When there is no overlap, `NoOverlapError` is raised.
50 """
51 match rhs:
52 case Box():
53 return _intersect_box_box(lhs, rhs)
54 case Region():
55 return _intersect_box_region(lhs, rhs)
56 case CellGridBounds():
57 return _intersect_box_cgb(lhs, rhs)
58 case IntersectionBounds():
59 return _intersect_ib(rhs, lhs)
60 case _:
61 raise TypeError(f"Unrecognized bounds type: {rhs}.")
64def _intersect_region(lhs: Region, rhs: Bounds) -> Bounds:
65 """Return the intersection between a `Region` and an arbitrary `Bounds`
66 object.
68 When there is no overlap, `NoOverlapError` is raised.
69 """
70 match rhs:
71 case Box():
72 return _intersect_box_region(rhs, lhs)
73 case Region():
74 return _intersect_region_region(lhs, rhs)
75 case CellGridBounds():
76 return IntersectionBounds(lhs, rhs)
77 case IntersectionBounds():
78 return _intersect_ib(rhs, lhs)
79 case _:
80 raise TypeError(f"Unrecognized bounds type: {rhs}.")
83def _intersect_cgb(lhs: CellGridBounds, rhs: Bounds) -> Bounds:
84 """Return the intersection between a `cells.CellGridBounds` and an
85 arbitrary `Bounds` object.
87 When there is no overlap, `NoOverlapError` is raised.
88 """
89 match rhs:
90 case Box():
91 return _intersect_box_cgb(rhs, lhs)
92 case Region():
93 return IntersectionBounds(lhs, rhs)
94 case CellGridBounds():
95 return _intersect_cgb_cgb(lhs, rhs)
96 case IntersectionBounds():
97 return _intersect_ib(rhs, lhs)
98 case _:
99 raise TypeError(f"Unrecognized bounds type: {rhs}.")
102def _intersect_ib(lhs: IntersectionBounds, rhs: Bounds) -> Bounds:
103 """Return the intersection between an `IntersectionBounds` and an
104 arbitrary `Bounds` object.
106 When there is no overlap, `NoOverlapError` is raised.
107 """
108 a_intersection = lhs._a.intersection(rhs)
109 if isinstance(a_intersection, IntersectionBounds):
110 # Intersection with the 'a' operand didn't simplify; try the 'b'
111 # operand instead.
112 return lhs._a.intersection(lhs._b.intersection(rhs))
113 else:
114 return a_intersection.intersection(lhs._b)
117def _intersect_box_box(lhs: Box, rhs: Box) -> Box:
118 """Return the intersection of two boxes.
120 When there is no overlap between the boxes, `NoOverlapError` is raised.
121 """
122 intervals = []
123 for a, b in zip(lhs._intervals, rhs._intervals, strict=True):
124 try:
125 intervals.append(a.intersection(b))
126 except NoOverlapError as err:
127 err.add_note(f"In intersection between {a} and {b}.")
128 raise
129 return Box(*intervals)
132def _intersect_box_region(lhs: Box, rhs: Region) -> Region | Box:
133 """Return the intersection of a box and a region.
135 When there is no overlap, `NoOverlapError` is raised.
136 """
137 return _intersect_region_region(Polygon.from_box(lhs), rhs).try_to_box()
140def _intersect_region_region(lhs: Region, rhs: Region) -> Region:
141 """Return the intersection of two regions.
143 When there is no overlap, `NoOverlapError` is raised.
144 """
145 impl = shapely.intersection(lhs._impl, rhs._impl)
146 if not impl.area:
147 raise NoOverlapError(f"No overlap between {lhs} and {rhs}.")
148 assert isinstance(impl, shapely.Polygon | shapely.MultiPolygon), (
149 "Polygon intersections should be polygons or multi-polygons."
150 )
151 return Region(impl).try_to_polygon()
154def _intersect_cgb_cgb(lhs: CellGridBounds, rhs: CellGridBounds) -> CellGridBounds | IntersectionBounds:
155 """Return the intersection of two `cells.CellGridBounds`.
157 When there is no overlap, `NoOverlapError` is raised.
158 """
159 bbox = _intersect_box_box(lhs.bbox, rhs.bbox) # will raise if they don't overlap
160 if lhs.grid == rhs.grid:
161 sliced_lhs = lhs[bbox]
162 sliced_rhs = rhs[bbox]
163 assert sliced_lhs.bbox == sliced_rhs.bbox, "Should be guaranteed by the common grid."
164 return CellGridBounds(
165 grid=sliced_lhs.grid, bbox=bbox, missing=frozenset(sliced_lhs.missing | sliced_rhs.missing)
166 )
167 # When the grids don't align, we just return a lazy intersection.
168 return IntersectionBounds(lhs, rhs)
171def _intersect_box_cgb(lhs: Box, rhs: CellGridBounds) -> Box | CellGridBounds | IntersectionBounds:
172 """Return the intersection of a `Box` and a `cells.CellGridBounds`.
174 When there is no overlap, `NoOverlapError` is raised.
175 """
176 bbox = _intersect_box_box(lhs, rhs.bbox) # will raise if they don't overlap
177 if bbox == rhs.bbox:
178 # lhs wholly contains rhs
179 return rhs
180 sliced_rhs = rhs[bbox]
181 if not sliced_rhs.missing:
182 # There are no missing cells in the intersection, so the intersection
183 # is just the bbox intersection.
184 return bbox
185 if bbox == sliced_rhs.bbox:
186 # The bbox intersection happens to be snapped to the cell grid.
187 return sliced_rhs
188 # General case: the box intersection is not snapped to the cell grid, so
189 # we need to use an IntersectionBounds to apply a lazy window to the
190 # cell grid bounds.
191 return IntersectionBounds(lhs, rhs)