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

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

47 

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

49 

50 @overload 

51 def contains(self, point: XY[int | float] | YX[int | float], /) -> bool: ... 51 ↛ exitline 51 didn't return from function 'contains' because

52 

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

55 

56 @overload 

57 def contains(self, /, *, x: int | float, y: int | float) -> bool: ... 57 ↛ exitline 57 didn't return from function 'contains' because

58 

59 @overload 

60 def contains(self, /, *, x: npt.ArrayLike, y: npt.ArrayLike) -> np.ndarray: ... 60 ↛ exitline 60 didn't return from function 'contains' because

61 

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. 

64 

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

78 

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

99 

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

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

102 

103 Parameters 

104 ---------- 

105 other 

106 Bounds to intersect with this one. 

107 

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 

115 

116 return _intersect_ib(self, other) 

117 

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 

123 

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

125 

126 

127if TYPE_CHECKING: 

128 

129 def _test_types() -> None: 

130 arr = np.zeros(3) 

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

132 ib = IntersectionBounds(a, a) 

133 

134 # IntersectionBounds satisfies the Bounds Protocol. 

135 bounds: Bounds = ib 

136 

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) 

145 

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)