Coverage for python/lsst/images/_intersection_bounds.py: 30%
44 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +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__ = ("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 def __str__(self) -> str:
44 return f"({self._a}) ∩ ({self._b})"
46 def __repr__(self) -> str:
47 return f"IntersectionBounds(a={self._a!r}, b={self._b!r})"
49 @property
50 def bbox(self) -> Box:
51 """The intersection of the bounding boxes of the operands (`.Box`)."""
52 from ._concrete_bounds import _intersect_box_box
54 return _intersect_box_box(self._a.bbox, self._b.bbox)
56 @overload
57 def contains(self, point: XY[int | float] | YX[int | float], /) -> bool: ...
59 @overload
60 def contains(self, point: XY[npt.ArrayLike] | YX[npt.ArrayLike], /) -> np.ndarray: ...
62 @overload
63 def contains(self, /, *, x: int | float, y: int | float) -> bool: ...
65 @overload
66 def contains(self, /, *, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: ...
68 def contains(self, point: XY[Any] | YX[Any] | None = None, /, *, x: Any = None, y: Any = None) -> Any:
69 """Test whether these bounds contain one or more points.
71 Parameters
72 ----------
73 point
74 An `XY` or `YX` coordinate pair to test for containment.
75 Mutually exclusive with ``x`` and ``y``.
76 x
77 One or more X coordinates to test for containment, as a scalar or
78 any array-like. Results are broadcast against ``y``.
79 Mutually exclusive with ``point``.
80 y
81 One or more Y coordinates to test for containment, as a scalar or
82 any array-like. Results are broadcast against ``x``.
83 Mutually exclusive with ``point``.
85 Returns
86 -------
87 `bool` | `numpy.ndarray`
88 If ``x`` and ``y`` are both scalars, a single `bool` value. If
89 ``x`` and ``y`` are array-like, a boolean array with their
90 broadcasted shape.
91 """
92 match point:
93 case None:
94 if x is None or y is None: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 raise TypeError("Pass either a point or both x= and y= to 'IntersectionBounds.contains'.")
96 case XY() | YX(): 96 ↛ 102line 96 didn't jump to line 102 because the pattern on line 96 always matched
97 if x is not None or y is not None: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true
98 raise TypeError(
99 "'IntersectionBounds.contains' point argument is mutually exclusive with x= and y=."
100 )
101 x, y = point.x, point.y
102 case _:
103 raise TypeError(f"Unexpected positional argument type: {type(point)!r}.")
104 return np.logical_and(self._a.contains(x=x, y=y), self._b.contains(x=x, y=y))
106 def intersection(self, other: Bounds) -> Bounds:
107 """Compute the intersection of this bounds object with another.
109 Parameters
110 ----------
111 other
112 Bounds to intersect with this one.
114 Notes
115 -----
116 Bounds intersection is guaranteed to raise `NoOverlapError` when the
117 operand bounding boxes do not overlap, but it may return a bounds
118 implementation that contains no points in more complex cases.
119 """
120 from ._concrete_bounds import _intersect_ib
122 return _intersect_ib(self, other)
124 def serialize(self) -> IntersectionBoundsSerializationModel:
125 """Convert a bounds instance into a serializable object."""
126 # Cyclic dependencies prevent IntersectionBoundsSerializationModel
127 # from being defined here.
128 from ._concrete_bounds import IntersectionBoundsSerializationModel
130 return IntersectionBoundsSerializationModel(a=self._a.serialize(), b=self._b.serialize())
133if TYPE_CHECKING:
135 def _test_types() -> None:
136 arr = np.zeros(3)
137 a = Box.from_shape((10, 20))
138 ib = IntersectionBounds(a, a)
140 # IntersectionBounds satisfies the Bounds Protocol.
141 bounds: Bounds = ib
143 # IntersectionBounds.contains: XY/YX, scalar, array-like
144 assert_type(ib.contains(x=1, y=2), bool)
145 assert_type(ib.contains(x=1.0, y=2.0), bool)
146 assert_type(ib.contains(x=arr, y=arr), np.ndarray)
147 assert_type(ib.contains(XY(1, 2)), bool)
148 assert_type(ib.contains(YX(2, 1)), bool)
149 assert_type(ib.contains(XY(arr, arr)), np.ndarray)
150 assert_type(ib.contains(YX(arr, arr)), np.ndarray)
152 # Via the Bounds Protocol view, same signatures hold.
153 assert_type(bounds.contains(x=1, y=1), bool)
154 assert_type(bounds.contains(x=1.0, y=1.0), bool)
155 assert_type(bounds.contains(x=arr, y=arr), np.ndarray)
156 assert_type(bounds.contains(XY(1, 1)), bool)
157 assert_type(bounds.contains(YX(1, 1)), bool)
158 assert_type(bounds.contains(XY(arr, arr)), np.ndarray)
159 assert_type(bounds.contains(YX(arr, arr)), np.ndarray)