Coverage for python / lsst / images / _concrete_bounds.py: 18%

70 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-01 08:36 +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__ = ("SerializableBounds", "deserialize_bounds") 

15 

16import pydantic 

17 

18from ._cell_grid import CellGridBounds 

19from ._geom import Bounds, Box, NoOverlapError 

20from ._intersection_bounds import IntersectionBounds 

21 

22 

23# The cyclic dependencies prevent this from going in _intersection_bounds.py. 

24class IntersectionBoundsSerializationModel(pydantic.BaseModel): 

25 """Serialization model for `IntersectionBounds`.""" 

26 

27 a: SerializableBounds 

28 b: SerializableBounds 

29 

30 

31type SerializableBounds = Box | CellGridBounds | IntersectionBoundsSerializationModel 

32 

33 

34IntersectionBoundsSerializationModel.model_rebuild() 

35 

36 

37def deserialize_bounds(serialized: SerializableBounds) -> Bounds: 

38 """Convert a serialized bounds object into its in-memory form.""" 

39 match serialized: 

40 case Box() | CellGridBounds(): 

41 return serialized # type: ignore[return-value] 

42 case IntersectionBoundsSerializationModel(): 

43 return IntersectionBounds.deserialize(serialized) 

44 raise RuntimeError(f"Cannot deserialize {serialized!r}.") 

45 

46 

47def _intersect_box(lhs: Box, rhs: Bounds) -> Bounds: 

48 """Return the intersection between a `Box` and an arbitrary `Bounds` 

49 object. 

50 

51 When there is no overlap, `NoOverlapError` is raised. 

52 """ 

53 match rhs: 

54 case Box(): 

55 return _intersect_box_box(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}.") 

62 

63 

64def _intersect_cgb(lhs: CellGridBounds, rhs: Bounds) -> Bounds: 

65 """Return the intersection between a `cellsCellGridBounds` and an 

66 arbitrary `Bounds` object. 

67 

68 When there is no overlap, `NoOverlapError` is raised. 

69 """ 

70 match rhs: 

71 case Box(): 

72 return _intersect_box_cgb(rhs, lhs) 

73 case CellGridBounds(): 

74 return _intersect_cgb_cgb(lhs, rhs) 

75 case IntersectionBounds(): 

76 return _intersect_ib(rhs, lhs) 

77 case _: 

78 raise TypeError(f"Unrecognized bounds type: {rhs}.") 

79 

80 

81def _intersect_ib(lhs: IntersectionBounds, rhs: Bounds) -> Bounds: 

82 """Return the intersection between an `IntersectionBounds` and an 

83 arbitrary `Bounds` object. 

84 

85 When there is no overlap, `NoOverlapError` is raised. 

86 """ 

87 a_intersection = lhs._a.intersection(rhs) 

88 if isinstance(a_intersection, IntersectionBounds): 

89 # Intersection with the 'a' operand didn't simplify; try the 'b' 

90 # operand instead. 

91 return lhs._a.intersection(lhs._b.intersection(rhs)) 

92 else: 

93 return a_intersection.intersection(lhs._b) 

94 

95 

96def _intersect_box_box(lhs: Box, rhs: Box) -> Box: 

97 """Return the intersection of two boxes. 

98 

99 When there is no overlap between the boxes, `NoOverlapError` is raised. 

100 """ 

101 intervals = [] 

102 for a, b in zip(lhs._intervals, rhs._intervals, strict=True): 

103 try: 

104 intervals.append(a.intersection(b)) 

105 except NoOverlapError as err: 

106 err.add_note(f"In intersection between {a} and {b}.") 

107 raise 

108 return Box(*intervals) 

109 

110 

111def _intersect_cgb_cgb(lhs: CellGridBounds, rhs: CellGridBounds) -> CellGridBounds | IntersectionBounds: 

112 """Return the intersection of two `cells.CellGridBounds`. 

113 

114 When there is no overlap, `NoOverlapError` is raised. 

115 """ 

116 bbox = _intersect_box_box(lhs.bbox, rhs.bbox) # will raise if they don't overlap 

117 if lhs.grid == rhs.grid: 

118 sliced_lhs = lhs[bbox] 

119 sliced_rhs = rhs[bbox] 

120 assert sliced_lhs.bbox == sliced_rhs.bbox, "Should be guaranteed by the common grid." 

121 return CellGridBounds( 

122 grid=sliced_lhs.grid, bbox=bbox, missing=frozenset(sliced_lhs.missing | sliced_rhs.missing) 

123 ) 

124 # When the grids don't align, we just return a lazy intersection. 

125 return IntersectionBounds(lhs, rhs) 

126 

127 

128def _intersect_box_cgb(lhs: Box, rhs: CellGridBounds) -> Box | CellGridBounds | IntersectionBounds: 

129 """Return the intersection of a `Box` and a `cells.CellGridBounds`. 

130 

131 When there is no overlap, `NoOverlapError` is raised. 

132 """ 

133 bbox = _intersect_box_box(lhs, rhs.bbox) # will raise if they don't overlap 

134 if bbox == rhs.bbox: 

135 # lhs wholly contains rhs 

136 return rhs 

137 sliced_rhs = rhs[bbox] 

138 if not sliced_rhs.missing: 

139 # There are no missing cells in the intersection, so the intersection 

140 # is just the bbox intersection. 

141 return bbox 

142 if bbox == sliced_rhs.bbox: 

143 # The bbox intersection happens to be snapped to the cell grid. 

144 return sliced_rhs 

145 # General case: the box intersection is not snapped to the cell grid, so 

146 # we need to use an IntersectionBounds to apply a lazy window to the 

147 # cell grid bounds. 

148 return IntersectionBounds(lhs, rhs)