Coverage for tests / test_polygon.py: 24%
86 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-20 08:29 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-20 08:29 +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.
12from __future__ import annotations
14import unittest
16import numpy as np
18from lsst.images import Box, NoOverlapError, Polygon, Region, RegionSerializationModel
20try:
21 import lsst.afw.geom # noqa: F401
23 have_legacy = True
24except ImportError:
25 have_legacy = False
28class SimplePolygonTestCase(unittest.TestCase):
29 """Tests for the Polygon class.
31 This includes most of the test coverage for the Region base class.
32 """
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)
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
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))
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 )
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)
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)
99class RegionTestCase(unittest.TestCase):
100 """Tests for `Region` objects that are not necessarily polygons, including
101 point-set operations.
103 Notes
104 -----
105 This test uses test geometries (all boxes) with the following rough layout
106 (with y increasing upwards):
108 .. _code-block::
109 ┌─────┐
110 ┌───┼─┐B┌─┼────┐
111 │ A└─┼─┼─┘ ┌─┐│
112 └─────┘ │ C │D││
113 │ └─┘│
114 └──────┘
115 """
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])
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 )
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)
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)
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)
180if __name__ == "__main__":
181 unittest.main()