Coverage for python/lsst/images/_intersection_bounds.py: 73%
40 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
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__ = ("IntersectionBounds",)
16from typing import TYPE_CHECKING, Any, assert_type, overload
18import numpy as np
19import numpy.typing as npt
21from ._geom import XY, YX, Bounds, Box
23if TYPE_CHECKING:
24 from ._concrete_bounds import IntersectionBoundsSerializationModel
27class IntersectionBounds:
28 """An implementation of the `Bounds` protocol that acts as a lazy
29 intersection of two other `Bounds` objects.
31 Parameters
32 ----------
33 a
34 First operand of the intersection.
35 b
36 Second operand of the intersection.
37 """
39 def __init__(self, a: Bounds, b: Bounds) -> None:
40 self._a = a
41 self._b = b
43 @property
44 def bbox(self) -> Box:
45 """The intersection of the bounding boxes of the operands (`.Box`)."""
46 from ._concrete_bounds import _intersect_box_box
48 return _intersect_box_box(self._a.bbox, self._b.bbox)
50 @overload
51 def contains(self, point: XY[int | float] | YX[int | float], /) -> bool: ... 51 ↛ exitline 51 didn't return from function 'contains' because
53 @overload
54 def contains(self, point: XY[npt.ArrayLike] | YX[npt.ArrayLike], /) -> np.ndarray: ... 54 ↛ exitline 54 didn't return from function 'contains' because
56 @overload
57 def contains(self, /, *, x: int | float, y: int | float) -> bool: ... 57 ↛ exitline 57 didn't return from function 'contains' because
59 @overload
60 def contains(self, /, *, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: ... 60 ↛ exitline 60 didn't return from function 'contains' because
62 def contains(self, point: XY[Any] | YX[Any] | None = None, /, *, x: Any = None, y: Any = None) -> Any:
63 """Test whether these bounds contain one or more points.
65 Parameters
66 ----------
67 point
68 An `XY` or `YX` coordinate pair to test for containment.
69 Mutually exclusive with ``x`` and ``y``.
70 x
71 One or more X coordinates to test for containment, as a scalar or
72 any array-like. Results are broadcast against ``y``.
73 Mutually exclusive with ``point``.
74 y
75 One or more Y coordinates to test for containment, as a scalar or
76 any array-like. Results are broadcast against ``x``.
77 Mutually exclusive with ``point``.
79 Returns
80 -------
81 `bool` | `numpy.ndarray`
82 If ``x`` and ``y`` are both scalars, a single `bool` value. If
83 ``x`` and ``y`` are array-like, a boolean array with their
84 broadcasted shape.
85 """
86 match point:
87 case None:
88 if x is None or y is None: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 raise TypeError("Pass either a point or both x= and y= to 'IntersectionBounds.contains'.")
90 case XY() | YX(): 90 ↛ 96line 90 didn't jump to line 96 because the pattern on line 90 always matched
91 if x is not None or y is not None: 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true
92 raise TypeError(
93 "'IntersectionBounds.contains' point argument is mutually exclusive with x= and y=."
94 )
95 x, y = point.x, point.y
96 case _:
97 raise TypeError(f"Unexpected positional argument type: {type(point)!r}.")
98 return np.logical_and(self._a.contains(x=x, y=y), self._b.contains(x=x, y=y))
100 def intersection(self, other: Bounds) -> Bounds:
101 """Compute the intersection of this bounds object with another.
103 Parameters
104 ----------
105 other
106 Bounds to intersect with this one.
108 Notes
109 -----
110 Bounds intersection is guaranteed to raise `NoOverlapError` when the
111 operand bounding boxes do not overlap, but it may return a bounds
112 implementation that contains no points in more complex cases.
113 """
114 from ._concrete_bounds import _intersect_ib
116 return _intersect_ib(self, other)
118 def serialize(self) -> IntersectionBoundsSerializationModel:
119 """Convert a bounds instance into a serializable object."""
120 # Cyclic dependencies prevent IntersectionBoundsSerializationModel
121 # from being defined here.
122 from ._concrete_bounds import IntersectionBoundsSerializationModel
124 return IntersectionBoundsSerializationModel(a=self._a.serialize(), b=self._b.serialize())
127if TYPE_CHECKING:
129 def _test_types() -> None:
130 arr = np.zeros(3)
131 a = Box.from_shape((10, 20))
132 ib = IntersectionBounds(a, a)
134 # IntersectionBounds satisfies the Bounds Protocol.
135 bounds: Bounds = ib
137 # IntersectionBounds.contains: XY/YX, scalar, array-like
138 assert_type(ib.contains(x=1, y=2), bool)
139 assert_type(ib.contains(x=1.0, y=2.0), bool)
140 assert_type(ib.contains(x=arr, y=arr), np.ndarray)
141 assert_type(ib.contains(XY(1, 2)), bool)
142 assert_type(ib.contains(YX(2, 1)), bool)
143 assert_type(ib.contains(XY(arr, arr)), np.ndarray)
144 assert_type(ib.contains(YX(arr, arr)), np.ndarray)
146 # Via the Bounds Protocol view, same signatures hold.
147 assert_type(bounds.contains(x=1, y=1), bool)
148 assert_type(bounds.contains(x=1.0, y=1.0), bool)
149 assert_type(bounds.contains(x=arr, y=arr), np.ndarray)
150 assert_type(bounds.contains(XY(1, 1)), bool)
151 assert_type(bounds.contains(YX(1, 1)), bool)
152 assert_type(bounds.contains(XY(arr, arr)), np.ndarray)
153 assert_type(bounds.contains(YX(arr, arr)), np.ndarray)