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

91 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-30 09:08 +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",) 

15 

16import pydantic 

17import shapely 

18 

19from ._cell_grid import CellGridBounds 

20from ._geom import Bounds, Box, NoOverlapError 

21from ._intersection_bounds import IntersectionBounds 

22from ._polygon import Polygon, Region, RegionSerializationModel 

23 

24 

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

26class IntersectionBoundsSerializationModel(pydantic.BaseModel): 

27 """Serialization model for `IntersectionBounds`.""" 

28 

29 a: SerializableBounds 

30 b: SerializableBounds 

31 

32 def deserialize(self) -> IntersectionBounds: 

33 """Deserialize into an `IntersectionBounds` instance.""" 

34 return IntersectionBounds(self.a.deserialize(), self.b.deserialize()) 

35 

36 

37type SerializableBounds = ( 

38 Box | CellGridBounds | RegionSerializationModel | IntersectionBoundsSerializationModel 

39) 

40 

41 

42IntersectionBoundsSerializationModel.model_rebuild() 

43 

44 

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

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

47 object. 

48 

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

50 """ 

51 match rhs: 

52 case Box(): 

53 return _intersect_box_box(lhs, rhs) 

54 case Region(): 

55 return _intersect_box_region(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_region(lhs: Region, rhs: Bounds) -> Bounds: 

65 """Return the intersection between a `Region` and an arbitrary `Bounds` 

66 object. 

67 

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

69 """ 

70 match rhs: 

71 case Box(): 

72 return _intersect_box_region(rhs, lhs) 

73 case Region(): 

74 return _intersect_region_region(lhs, rhs) 

75 case CellGridBounds(): 

76 return IntersectionBounds(lhs, rhs) 

77 case IntersectionBounds(): 

78 return _intersect_ib(rhs, lhs) 

79 case _: 

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

81 

82 

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

84 """Return the intersection between a `cells.CellGridBounds` and an 

85 arbitrary `Bounds` object. 

86 

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

88 """ 

89 match rhs: 

90 case Box(): 

91 return _intersect_box_cgb(rhs, lhs) 

92 case Region(): 

93 return IntersectionBounds(lhs, rhs) 

94 case CellGridBounds(): 

95 return _intersect_cgb_cgb(lhs, rhs) 

96 case IntersectionBounds(): 

97 return _intersect_ib(rhs, lhs) 

98 case _: 

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

100 

101 

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

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

104 arbitrary `Bounds` object. 

105 

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

107 """ 

108 a_intersection = lhs._a.intersection(rhs) 

109 if isinstance(a_intersection, IntersectionBounds): 

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

111 # operand instead. 

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

113 else: 

114 return a_intersection.intersection(lhs._b) 

115 

116 

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

118 """Return the intersection of two boxes. 

119 

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

121 """ 

122 intervals = [] 

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

124 try: 

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

126 except NoOverlapError as err: 

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

128 raise 

129 return Box(*intervals) 

130 

131 

132def _intersect_box_region(lhs: Box, rhs: Region) -> Region | Box: 

133 """Return the intersection of a box and a region. 

134 

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

136 """ 

137 return _intersect_region_region(Polygon.from_box(lhs), rhs).try_to_box() 

138 

139 

140def _intersect_region_region(lhs: Region, rhs: Region) -> Region: 

141 """Return the intersection of two regions. 

142 

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

144 """ 

145 impl = shapely.intersection(lhs._impl, rhs._impl) 

146 if not impl.area: 

147 raise NoOverlapError(f"No overlap between {lhs} and {rhs}.") 

148 assert isinstance(impl, shapely.Polygon | shapely.MultiPolygon), ( 

149 "Polygon intersections should be polygons or multi-polygons." 

150 ) 

151 return Region(impl).try_to_polygon() 

152 

153 

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

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

156 

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

158 """ 

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

160 if lhs.grid == rhs.grid: 

161 sliced_lhs = lhs[bbox] 

162 sliced_rhs = rhs[bbox] 

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

164 return CellGridBounds( 

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

166 ) 

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

168 return IntersectionBounds(lhs, rhs) 

169 

170 

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

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

173 

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

175 """ 

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

177 if bbox == rhs.bbox: 

178 # lhs wholly contains rhs 

179 return rhs 

180 sliced_rhs = rhs[bbox] 

181 if not sliced_rhs.missing: 

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

183 # is just the bbox intersection. 

184 return bbox 

185 if bbox == sliced_rhs.bbox: 

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

187 return sliced_rhs 

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

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

190 # cell grid bounds. 

191 return IntersectionBounds(lhs, rhs)