Coverage for tests/test_polygon.py: 24%

86 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-03 08:10 +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 

14import unittest 

15 

16import numpy as np 

17 

18from lsst.images import Box, NoOverlapError, Polygon, Region, RegionSerializationModel 

19 

20try: 

21 import lsst.afw.geom # noqa: F401 

22 

23 have_legacy = True 

24except ImportError: 

25 have_legacy = False 

26 

27 

28class SimplePolygonTestCase(unittest.TestCase): 

29 """Tests for the Polygon class. 

30 

31 This includes most of the test coverage for the Region base class. 

32 """ 

33 

34 def setUp(self) -> None: 

35 # A quadrilateral that's almost a box, so it's easy to reason about. 

36 self.x_vertices = [32.0, 31.0, 50.0, 53.0] 

37 self.y_vertices = [-5.0, 7.0, 7.2, -4.8] 

38 self.polygon = Polygon(x_vertices=self.x_vertices, y_vertices=self.y_vertices) 

39 

40 def test_vertices(self) -> None: 

41 """Test the vertices accessors.""" 

42 self.assertEqual(self.polygon.n_vertices, 4) 

43 np.testing.assert_array_equal(self.polygon.x_vertices, np.asarray(self.x_vertices)) 

44 np.testing.assert_array_equal(self.polygon.y_vertices, np.asarray(self.y_vertices)) 

45 with self.assertRaises(ValueError): 

46 self.polygon.x_vertices[0] = 0.0 

47 with self.assertRaises(ValueError): 

48 self.polygon.y_vertices[0] = 0.0 

49 

50 def test_boxes(self) -> None: 

51 """Test 'from_box', the `area` property, and the 'contains' method 

52 with polygon arguments. 

53 """ 

54 small = Polygon.from_box(Box.factory[-3:3, 40:45]) 

55 self.assertEqual(small.area, 30.0) 

56 self.assertEqual(small.bbox, Box.factory[-3:3, 40:45]) 

57 self.assertTrue(self.polygon.contains(small)) 

58 self.assertFalse(small.contains(self.polygon)) 

59 big = Polygon.from_box(Box.factory[-10:10, 20:60]) 

60 self.assertEqual(big.area, 800.0) 

61 self.assertFalse(self.polygon.contains(big)) 

62 self.assertTrue(big.contains(self.polygon)) 

63 medium = Polygon.from_box(Box.factory[-4:8, 31:52]) 

64 self.assertEqual(medium.area, 252.0) 

65 self.assertFalse(self.polygon.contains(medium)) 

66 self.assertFalse(medium.contains(self.polygon)) 

67 self.assertTrue(self.polygon.contains(self.polygon)) 

68 

69 def test_contains_points(self) -> None: 

70 """Test the 'contains' method with points.""" 

71 self.assertTrue(self.polygon.contains(x=40.0, y=0.0)) 

72 self.assertFalse(self.polygon.contains(x=0.0, y=0.0)) 

73 self.assertFalse(self.polygon.contains(x=40.0, y=10.0)) 

74 np.testing.assert_array_equal( 

75 self.polygon.contains(x=np.array([40.0, 0.0, 40.0]), y=np.array([0.0, 0.0, 10.0])), 

76 np.array([True, False, False]), 

77 ) 

78 

79 def test_io(self) -> None: 

80 """Test serialization and stringification.""" 

81 self.assertEqual( 

82 RegionSerializationModel.model_validate_json( 

83 self.polygon.serialize().model_dump_json() 

84 ).deserialize(), 

85 self.polygon, 

86 ) 

87 self.assertEqual(Polygon.from_wkt(self.polygon.wkt), self.polygon) 

88 self.assertEqual(Polygon.from_wkt(str(self.polygon)), self.polygon) 

89 self.assertEqual(eval(repr(self.polygon), {"array": np.array, "Polygon": Polygon}), self.polygon) 

90 

91 @unittest.skipUnless(have_legacy, "lsst legacy packages could not be imported.") 

92 def test_legacy(self) -> None: 

93 """Test conversion to/from lsst.afw.geom.Polygon.""" 

94 legacy_polygon = self.polygon.to_legacy() 

95 self.assertEqual(legacy_polygon.calculateArea(), self.polygon.area) 

96 self.assertEqual(Polygon.from_legacy(legacy_polygon), self.polygon) 

97 

98 

99class RegionTestCase(unittest.TestCase): 

100 """Tests for `Region` objects that are not necessarily polygons, including 

101 point-set operations. 

102 

103 Notes 

104 ----- 

105 This test uses test geometries (all boxes) with the following rough layout 

106 (with y increasing upwards): 

107 

108 .. _code-block:: 

109 ┌─────┐ 

110 ┌───┼─┐B┌─┼────┐ 

111 │ A└─┼─┼─┘ ┌─┐│ 

112 └─────┘ │ C │D││ 

113 │ └─┘│ 

114 └──────┘ 

115 """ 

116 

117 def setUp(self) -> None: 

118 self.a = Polygon.from_box(Box.factory[3:6, 0:5]) 

119 self.b = Polygon.from_box(Box.factory[4:7, 3:8]) 

120 self.c = Polygon.from_box(Box.factory[0:6, 6:12]) 

121 self.d = Polygon.from_box(Box.factory[1:4, 9:10]) 

122 

123 def test_intersection(self) -> None: 

124 """Test region intersection.""" 

125 # Usual case: 

126 self.assertEqual(self.a.intersection(self.b), Polygon.from_box(Box.factory[4:6, 3:5])) 

127 # No-overlap case: 

128 with self.assertRaises(NoOverlapError): 

129 self.a.intersection(self.c) 

130 # LHS fully contains RHS: 

131 self.assertEqual(self.c.intersection(self.d), self.d) 

132 # Intersections with the boxes themselves should return boxes when 

133 # possible. 

134 self.assertEqual(self.a.intersection(self.b.bbox), Box.factory[4:6, 3:5]) 

135 self.assertEqual(self.a.bbox.intersection(self.b), Box.factory[4:6, 3:5]) 

136 self.assertEqual( 

137 # A Box is not possible when the result is not simple. 

138 self.a.union(self.c).intersection(self.b.bbox), 

139 self.a.union(self.c).intersection(self.b), 

140 ) 

141 

142 def test_union(self) -> None: 

143 """Test region union.""" 

144 # Usual case: 

145 self.assertEqual(self.a.union(self.b).bbox, Box.factory[3:7, 0:8]) 

146 self.assertEqual(self.a.union(self.b).area, 15 + 15 - 4) 

147 # Operands are disjoint, so union is not a single Polygon: 

148 self.assertNotIsInstance(self.a.union(self.c), Polygon) 

149 self.assertEqual(self.a.union(self.c).area, self.a.area + self.c.area) 

150 # LHS fully contains RHS: 

151 self.assertEqual(self.c.union(self.d), self.c) 

152 

153 def test_difference(self) -> None: 

154 """Test region difference.""" 

155 # Usual case: 

156 self.assertEqual(self.a.difference(self.b).bbox, self.a.bbox) 

157 self.assertEqual(self.a.difference(self.b).area, 15 - 4) 

158 # Operands are disjoint, so difference is just the LHS. 

159 self.assertEqual(self.a.difference(self.c), self.a) 

160 # LHS fully contains RHS -> polygon with hole is not a Polygon: 

161 self.assertNotIsInstance(self.c.difference(self.d), Polygon) 

162 self.assertEqual(self.c.difference(self.d).bbox, self.c.bbox) 

163 self.assertEqual(self.c.difference(self.d).area, self.c.area - self.d.area) 

164 # RHS fully contains LHS -> region is empty. 

165 self.assertEqual(self.d.difference(self.d).area, 0) 

166 

167 def test_io(self) -> None: 

168 """Test serialization and stringification of non-polygon regions.""" 

169 # A two-polygon region with a hole: 

170 region = self.a.union(self.c).difference(self.d) 

171 self.assertEqual( 

172 RegionSerializationModel.model_validate_json(region.serialize().model_dump_json()).deserialize(), 

173 region, 

174 ) 

175 self.assertEqual(Region.from_wkt(region.wkt), region) 

176 self.assertEqual(Region.from_wkt(str(region)), region) 

177 self.assertEqual(eval(repr(region), {"Region": Region}), region) 

178 

179 

180if __name__ == "__main__": 

181 unittest.main()