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

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. 

11 

12from __future__ import annotations 

13 

14__all__ = ("IntersectionBounds",) 

15 

16from typing import TYPE_CHECKING, Any, assert_type, overload 

17 

18import numpy as np 

19import numpy.typing as npt 

20 

21from ._geom import XY, YX, Bounds, Box 

22 

23if TYPE_CHECKING: 

24 from ._concrete_bounds import IntersectionBoundsSerializationModel 

25 

26 

27class IntersectionBounds: 

28 """An implementation of the `Bounds` protocol that acts as a lazy 

29 intersection of two other `Bounds` objects. 

30 

31 Parameters 

32 ---------- 

33 a 

34 First operand of the intersection. 

35 b 

36 Second operand of the intersection. 

37 """ 

38 

39 def __init__(self, a: Bounds, b: Bounds) -> None: 

40 self._a = a 

41 self._b = b 

42 

43 def __str__(self) -> str: 

44 return f"({self._a}) ∩ ({self._b})" 

45 

46 def __repr__(self) -> str: 

47 return f"IntersectionBounds(a={self._a!r}, b={self._b!r})" 

48 

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 

53 

54 return _intersect_box_box(self._a.bbox, self._b.bbox) 

55 

56 @overload 

57 def contains(self, point: XY[int | float] | YX[int | float], /) -> bool: ... 

58 

59 @overload 

60 def contains(self, point: XY[npt.ArrayLike] | YX[npt.ArrayLike], /) -> np.ndarray: ... 

61 

62 @overload 

63 def contains(self, /, *, x: int | float, y: int | float) -> bool: ... 

64 

65 @overload 

66 def contains(self, /, *, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: ... 

67 

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. 

70 

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``. 

84 

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)) 

105 

106 def intersection(self, other: Bounds) -> Bounds: 

107 """Compute the intersection of this bounds object with another. 

108 

109 Parameters 

110 ---------- 

111 other 

112 Bounds to intersect with this one. 

113 

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 

121 

122 return _intersect_ib(self, other) 

123 

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 

129 

130 return IntersectionBoundsSerializationModel(a=self._a.serialize(), b=self._b.serialize()) 

131 

132 

133if TYPE_CHECKING: 

134 

135 def _test_types() -> None: 

136 arr = np.zeros(3) 

137 a = Box.from_shape((10, 20)) 

138 ib = IntersectionBounds(a, a) 

139 

140 # IntersectionBounds satisfies the Bounds Protocol. 

141 bounds: Bounds = ib 

142 

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) 

151 

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)