Coverage for tests / test_geom.py: 12%
252 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-02 01:36 -0800
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-02 01:36 -0800
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 pickle
15import unittest
17import numpy as np
18import pydantic
20from lsst.images import XY, YX, Box, Interval
21from lsst.images.tests import assert_close
24class IntervalModel(pydantic.BaseModel):
25 """Test Pydantic model with an Interval."""
27 interval1: Interval
28 interval2: Interval
31class BoxModel(pydantic.BaseModel):
32 """Test Pydantic model with a box."""
34 box: Box
37class XYYXTestCase(unittest.TestCase):
38 """Test the XY and YX classes."""
40 def test_yx(self) -> None:
41 """Test YX."""
42 yx = YX(5, 7)
43 self.assertEqual(yx, (5, 7))
44 self.assertEqual(yx.y, 5)
45 self.assertEqual(yx.x, 7)
47 def _plus_one(v: int) -> int:
48 return v + 1
50 new = yx.map(_plus_one)
51 self.assertEqual(new, (6, 8))
53 xy = yx.xy
54 self.assertEqual(xy, (7, 5))
56 def test_xy(self) -> None:
57 """Test XY."""
58 xy = XY(5, 7)
59 self.assertEqual(xy, (5, 7))
60 self.assertEqual(xy.y, 7)
61 self.assertEqual(xy.x, 5)
63 def _plus_one(v: int) -> int:
64 return v + 1
66 new = xy.map(_plus_one)
67 self.assertEqual(new, (6, 8))
69 yx = xy.yx
70 self.assertEqual(yx, (7, 5))
73class IntervalTestCase(unittest.TestCase):
74 """Test the Interval class."""
76 def test_constructor(self) -> None:
77 """Simple construction."""
78 i = Interval(start=1, stop=10)
79 self.assertEqual(i.start, 1)
80 self.assertEqual(i.stop, 10)
81 self.assertEqual(i.min, 1)
82 self.assertEqual(i.max, 9)
83 self.assertEqual(i.size, 9)
84 self.assertEqual(i.center, 5.0)
85 self.assertEqual(Interval(1, 10), i)
87 self.assertEqual(str(i), "1:10")
89 shifted = i + 10
90 self.assertEqual(shifted.start, 11)
91 self.assertEqual(shifted.stop, 20)
93 shifted = i - 10
94 self.assertEqual(shifted.start, -9)
95 self.assertEqual(shifted.stop, 0)
97 self.assertEqual(shifted, i - 10)
98 self.assertNotEqual(shifted, i)
100 sized = Interval.from_size(10)
101 self.assertEqual(sized, Interval(0, 10))
102 sized = Interval.from_size(10, 5)
103 self.assertEqual(sized, Interval(5, 15))
105 h = Interval.hull(2, -1, 3, 6)
106 self.assertEqual(h, Interval(start=-1, stop=7))
107 h2 = Interval.hull(h, 3, 40, Interval(start=-10, stop=2))
108 self.assertEqual(h2, Interval(start=-10, stop=41))
110 def test_contains(self) -> None:
111 """Test containment."""
112 i = Interval(start=1, stop=10)
113 self.assertIn(5, i)
114 self.assertNotIn(10, i)
116 i2 = Interval(start=2, stop=4)
117 self.assertTrue(i.contains(i2))
119 i3 = Interval(start=-1, stop=5)
120 self.assertFalse(i.contains(i3))
122 self.assertTrue(i3.contains(2.5))
124 containment = i3.contains(np.array([-2, -1, 0, 1, 2, 3, 4, 5, 6]))
125 self.assertEqual(list(containment), [False, True, True, True, True, True, True, True, False])
127 inter = i2.intersection(i)
128 self.assertEqual(inter, Interval(start=2, stop=4), msg=f"Intersection of {i2} with {i}")
129 self.assertIsNone(i.intersection(Interval.factory[20:30]))
131 self.assertNotEqual(i, [])
133 def test_slice(self) -> None:
134 """Interval construction with slicing."""
135 i = Interval.factory[3:20]
136 self.assertEqual(i.start, 3)
137 self.assertEqual(i.stop, 20)
138 self.assertEqual(i[::], i)
140 subset = i[5:10]
141 self.assertEqual(subset.start, 5)
142 self.assertEqual(subset.stop, 10)
144 subset = i[:10]
145 self.assertEqual(subset.start, 3)
146 self.assertEqual(subset.stop, 10)
148 subset = i[10:]
149 self.assertEqual(subset.start, 10)
150 self.assertEqual(subset.stop, 20)
152 with self.assertRaises(IndexError):
153 i[:30]
155 with self.assertRaises(IndexError):
156 i[30:]
158 with self.assertRaises(IndexError):
159 i[-1:10]
161 with self.assertRaises(ValueError):
162 i[::2]
164 with self.assertRaises(ValueError):
165 Interval.factory[1:2:2]
167 def test_usage(self) -> None:
168 """Test using intervals."""
169 i = Interval(start=1, stop=10)
170 d = i.dilated_by(5)
171 self.assertEqual(d, Interval(start=-4, stop=15))
173 s = i.slice_within(Interval(start=-1, stop=12))
174 self.assertEqual(s.start, 2)
175 self.assertEqual(s.stop, 11)
177 with self.assertRaises(IndexError):
178 i.slice_within(Interval(start=3, stop=5))
180 val = i.linspace()
181 assert_close(self, val, np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]))
182 val = i.linspace(step=2.0)
183 assert_close(self, val, np.array([1.0, 3.0, 5.0, 7.0, 9.0]))
184 val = i.linspace(n=3)
185 assert_close(self, val, np.array([1.0, 5.0, 9.0]))
186 with self.assertRaises(TypeError):
187 i.linspace(n=2, step=3.0)
189 self.assertEqual(list(i.range), [1, 2, 3, 4, 5, 6, 7, 8, 9])
190 val = i.arange
191 assert_close(self, val, np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]))
193 def test_pydantic(self) -> None:
194 """Test roundtrip through pydantic serialization."""
195 i1 = Interval(start=2, stop=5)
196 i2 = Interval(start=-5, stop=10)
197 model = IntervalModel(interval1=i1, interval2=i2)
198 j_str = model.model_dump_json()
199 jmodel = IntervalModel.model_validate_json(j_str)
200 self.assertEqual(jmodel, model)
201 self.assertEqual(jmodel.interval1, i1)
203 def test_pickle(self) -> None:
204 """Test pickle roundtrip."""
205 i = Interval(start=5, stop=10)
206 d = pickle.dumps(i)
207 copy = pickle.loads(d)
208 self.assertEqual(copy, i)
211class BoxTestCase(unittest.TestCase):
212 """Test the Box implementation."""
214 def test_constructor(self) -> None:
215 """Test Box construction."""
216 y = Interval(-5, 5)
217 x = Interval(32, 64)
218 box = Box(y, x)
220 self.assertEqual(box.start, YX(-5, 32))
221 self.assertEqual(box.shape, YX(10, 32))
222 self.assertEqual(box.x, x)
223 self.assertEqual(box.y, y)
224 self.assertNotEqual(box, [])
226 box2 = Box.factory[-5:5, 32:64]
227 self.assertEqual(box2, box)
229 sbox = Box.from_shape((10, 5))
230 self.assertEqual(sbox, Box.factory[0:10, 0:5])
231 sbox = Box.from_shape((10, 5), start=(2, 3))
232 self.assertEqual(sbox, Box.factory[2:12, 3:8])
234 sbox = Box.from_shape(YX(10, 5))
235 self.assertEqual(sbox, Box.factory[0:10, 0:5])
236 sbox = Box.from_shape((10, 5), start=YX(2, 3))
237 self.assertEqual(sbox, Box.factory[2:12, 3:8])
239 sbox = Box.from_shape(XY(5, 10))
240 self.assertEqual(sbox, Box.factory[0:10, 0:5])
241 sbox = Box.from_shape((10, 5), start=XY(3, 2))
242 self.assertEqual(sbox, Box.factory[2:12, 3:8])
244 with self.assertRaises(TypeError):
245 Box.from_shape(42)
246 with self.assertRaises(ValueError):
247 Box.from_shape([42])
248 with self.assertRaises(ValueError):
249 Box.from_shape([42, 33], start=[1, 2, 3])
251 box = Box.factory[1:2, -1:1]
252 grown = box.dilated_by(2)
253 self.assertEqual(grown, Box.factory[-1:4, -3:3])
255 def test_contains(self) -> None:
256 """Does a box fit inside another or not."""
257 box = Box.factory[0:20, 10:40]
259 self.assertTrue(box.contains(Box.factory[4:10, 20:25]))
260 self.assertFalse(box.contains(Box.factory[4:10, 35:45]))
261 self.assertTrue(box.contains(y=4, x=15))
262 self.assertFalse(box.contains(x=4, y=15))
264 contains = box.contains(
265 # Half pixel leeway.
266 x=np.array([-1, 10, 20, 30, 40, 41]),
267 y=np.array([-1, 5, 19, 20, 20, 20]),
268 )
269 self.assertEqual(list(contains), [False, True, True, True, True, False])
271 with self.assertRaises(TypeError):
272 box.contains(box, x=3, y=2)
273 with self.assertRaises(TypeError):
274 box.contains()
276 def test_intersection(self) -> None:
277 """Test box intersection."""
278 box1 = Box.factory[0:20, 30:50]
279 box2 = Box.factory[10:30, 40:42]
280 self.assertEqual(box1.intersection(box2), Box.factory[10:20, 40:42])
281 self.assertIsNone(box1.intersection(Box.factory[50:70, -10:-5]))
283 def test_slicing(self) -> None:
284 """Test slicing."""
285 box = Box.factory[:10, :20]
286 sbox = box[4:6, :3]
287 self.assertEqual(sbox, Box.factory[4:6, 0:3])
288 sbox = box[4:, 5:]
289 self.assertEqual(sbox, Box.factory[4:10, 5:20])
290 sbox = box[XY(slice(5, None), slice(4, None))]
291 self.assertEqual(sbox, Box.factory[4:10, 5:20])
293 self.assertEqual(Box.factory[4:10, -1:2], Box.factory[XY(slice(-1, 2), slice(4, 10))])
295 slices = sbox.slice_within(box)
296 self.assertEqual(slices.x.start, 5)
297 self.assertEqual(slices.y.start, 4)
298 self.assertEqual(slices.x.stop, 20)
299 self.assertEqual(slices.y.stop, 10)
301 slices = Box.factory[:5, 110:119].slice_within(Box.factory[-15:12, 90:120])
302 self.assertEqual(slices.x.start, 20)
303 self.assertEqual(slices.y.start, 15)
304 self.assertEqual(slices.x.stop, 29)
305 self.assertEqual(slices.y.stop, 20)
307 with self.assertRaises(IndexError):
308 box[-1:5, 3:]
309 with self.assertRaises(TypeError):
310 box[3:5, :5, 4:]
311 with self.assertRaises(TypeError):
312 Box.factory[3:5, :6, 4:]
314 def test_mesh(self) -> None:
315 """Test grid creation."""
316 box = Box.factory[0:2, 0:3]
318 grid = box.meshgrid()
319 assert_close(self, grid.x, np.array([[0.0, 1.0, 2.0], [0.0, 1.0, 2.0]]))
320 assert_close(self, grid.y, np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]))
322 grid = box.meshgrid(2)
323 assert_close(self, grid.x, np.array([[0.0, 2.0], [0.0, 2.0]]))
324 assert_close(self, grid.y, np.array([[0.0, 0.0], [1.0, 1.0]]))
326 grid = box.meshgrid([2, 1])
327 assert_close(self, grid.x, np.array([[0.0, 2.0]]))
328 assert_close(self, grid.y, np.array([[0.0, 0.0]]))
330 grid = box.meshgrid(XY(2, 1))
331 assert_close(self, grid.x, np.array([[0.0, 2.0]]))
332 assert_close(self, grid.y, np.array([[0.0, 0.0]]))
334 grid = box.meshgrid(YX(1, 2))
335 assert_close(self, grid.x, np.array([[0.0, 2.0]]))
336 assert_close(self, grid.y, np.array([[0.0, 0.0]]))
338 grid = box.meshgrid(step=3)
339 assert_close(self, grid.x, np.array([[0.0]]))
340 assert_close(self, grid.y, np.array([[0.0]]))
342 with self.assertRaises(TypeError):
343 box.meshgrid(2, step=3)
345 with self.assertRaises(ValueError):
346 box.meshgrid("n")
348 def test_boundary(self) -> None:
349 """Test we can found the boundary."""
350 box = Box.factory[-1:9, 7:15]
351 corners = list(box.boundary())
352 self.assertEqual(corners[0], (-1, 7))
353 self.assertEqual(corners[1], (-1, 14))
354 self.assertEqual(corners[2], (8, 14))
355 self.assertEqual(corners[3], (8, 7))
357 def test_pydantic(self) -> None:
358 """Test roundtrip through pydantic serialization."""
359 box = Box.factory[-1:1, 5:10]
360 model = BoxModel(box=box)
361 j_str = model.model_dump_json()
362 jmodel = BoxModel.model_validate_json(j_str)
363 self.assertEqual(jmodel, model)
364 self.assertEqual(jmodel.box, box)
366 def test_pickle(self) -> None:
367 """Test pickle roundtrip."""
368 box = Box.factory[-1:1, 5:10]
369 d = pickle.dumps(box)
370 copy = pickle.loads(d)
371 self.assertEqual(copy, box)
374if __name__ == "__main__":
375 unittest.main()