Coverage for tests / test_geom.py: 11%
289 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:34 +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 pickle
15import unittest
17import numpy as np
18import pydantic
20from lsst.images import XY, YX, Box, Interval, NoOverlapError
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 with self.assertRaises(NoOverlapError):
130 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.absolute[::], i)
139 self.assertEqual(i.local[::], i)
141 subset = i.absolute[5:10]
142 self.assertEqual(subset.start, 5)
143 self.assertEqual(subset.stop, 10)
145 subset = i.local[5:10]
146 self.assertEqual(subset.start, 8)
147 self.assertEqual(subset.stop, 13)
149 subset = i.absolute[:10]
150 self.assertEqual(subset.start, 3)
151 self.assertEqual(subset.stop, 10)
153 subset = i.local[:10]
154 self.assertEqual(subset.start, 3)
155 self.assertEqual(subset.stop, 13)
157 subset = i.absolute[10:]
158 self.assertEqual(subset.start, 10)
159 self.assertEqual(subset.stop, 20)
161 subset = i.local[10:]
162 self.assertEqual(subset.start, 13)
163 self.assertEqual(subset.stop, 20)
165 subset = i.local[3:-2]
166 self.assertEqual(subset.start, 6)
167 self.assertEqual(subset.stop, 18)
169 subset = i.local[-5:-2]
170 self.assertEqual(subset.start, 15)
171 self.assertEqual(subset.stop, 18)
173 with self.assertRaises(IndexError):
174 i.absolute[:30]
176 # It might seem surprising that this does not raise, but it's exactly
177 # what what list(range(3, 20))[:30] does:
178 subset = i.local[:30]
179 self.assertEqual(subset.start, 3)
180 self.assertEqual(subset.stop, 20)
182 with self.assertRaises(IndexError):
183 i.absolute[30:]
185 with self.assertRaises(IndexError):
186 i.local[30:]
188 with self.assertRaises(IndexError):
189 i.absolute[-1:10]
191 with self.assertRaises(IndexError):
192 i.local[-1:10]
194 with self.assertRaises(ValueError):
195 i.absolute[::2]
197 with self.assertRaises(ValueError):
198 i.local[::2]
200 with self.assertRaises(ValueError):
201 Interval.factory[1:2:2]
203 def test_usage(self) -> None:
204 """Test using intervals."""
205 i = Interval(start=1, stop=10)
206 d = i.dilated_by(5)
207 self.assertEqual(d, Interval(start=-4, stop=15))
209 s = i.slice_within(Interval(start=-1, stop=12))
210 self.assertEqual(s.start, 2)
211 self.assertEqual(s.stop, 11)
213 with self.assertRaises(IndexError):
214 i.slice_within(Interval(start=3, stop=5))
216 val = i.linspace()
217 assert_close(self, val, np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]))
218 val = i.linspace(step=2.0)
219 assert_close(self, val, np.array([1.0, 3.0, 5.0, 7.0, 9.0]))
220 val = i.linspace(n=3)
221 assert_close(self, val, np.array([1.0, 5.0, 9.0]))
222 with self.assertRaises(TypeError):
223 i.linspace(n=2, step=3.0)
225 self.assertEqual(list(i.range), [1, 2, 3, 4, 5, 6, 7, 8, 9])
226 val = i.arange
227 assert_close(self, val, np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]))
229 def test_pydantic(self) -> None:
230 """Test roundtrip through pydantic serialization."""
231 i1 = Interval(start=2, stop=5)
232 i2 = Interval(start=-5, stop=10)
233 model = IntervalModel(interval1=i1, interval2=i2)
234 j_str = model.model_dump_json()
235 jmodel = IntervalModel.model_validate_json(j_str)
236 self.assertEqual(jmodel, model)
237 self.assertEqual(jmodel.interval1, i1)
239 def test_pickle(self) -> None:
240 """Test pickle roundtrip."""
241 i = Interval(start=5, stop=10)
242 d = pickle.dumps(i)
243 copy = pickle.loads(d)
244 self.assertEqual(copy, i)
247class BoxTestCase(unittest.TestCase):
248 """Test the Box implementation."""
250 def test_constructor(self) -> None:
251 """Test Box construction."""
252 y = Interval(-5, 5)
253 x = Interval(32, 64)
254 box = Box(y, x)
256 self.assertEqual(box.start, YX(-5, 32))
257 self.assertEqual(box.shape, YX(10, 32))
258 self.assertEqual(box.x, x)
259 self.assertEqual(box.y, y)
260 self.assertNotEqual(box, [])
262 box2 = Box.factory[-5:5, 32:64]
263 self.assertEqual(box2, box)
265 sbox = Box.from_shape((10, 5))
266 self.assertEqual(sbox, Box.factory[0:10, 0:5])
267 sbox = Box.from_shape((10, 5), start=(2, 3))
268 self.assertEqual(sbox, Box.factory[2:12, 3:8])
270 sbox = Box.from_shape(YX(10, 5))
271 self.assertEqual(sbox, Box.factory[0:10, 0:5])
272 sbox = Box.from_shape((10, 5), start=YX(2, 3))
273 self.assertEqual(sbox, Box.factory[2:12, 3:8])
275 sbox = Box.from_shape(XY(5, 10))
276 self.assertEqual(sbox, Box.factory[0:10, 0:5])
277 sbox = Box.from_shape((10, 5), start=XY(3, 2))
278 self.assertEqual(sbox, Box.factory[2:12, 3:8])
280 with self.assertRaises(TypeError):
281 Box.from_shape(42)
282 with self.assertRaises(ValueError):
283 Box.from_shape([42])
284 with self.assertRaises(ValueError):
285 Box.from_shape([42, 33], start=[1, 2, 3])
287 box = Box.factory[1:2, -1:1]
288 grown = box.dilated_by(2)
289 self.assertEqual(grown, Box.factory[-1:4, -3:3])
291 def test_contains(self) -> None:
292 """Does a box fit inside another or not."""
293 box = Box.factory[0:20, 10:40]
295 self.assertTrue(box.contains(Box.factory[4:10, 20:25]))
296 self.assertFalse(box.contains(Box.factory[4:10, 35:45]))
297 self.assertTrue(box.contains(y=4, x=15))
298 self.assertFalse(box.contains(x=4, y=15))
300 contains = box.contains(
301 # Half pixel leeway.
302 x=np.array([-1, 10, 20, 30, 40, 41]),
303 y=np.array([-1, 5, 19, 20, 20, 20]),
304 )
305 self.assertEqual(list(contains), [False, True, True, True, True, False])
307 with self.assertRaises(TypeError):
308 box.contains(box, x=3, y=2)
309 with self.assertRaises(TypeError):
310 box.contains()
312 def test_intersection(self) -> None:
313 """Test box intersection."""
314 box1 = Box.factory[0:20, 30:50]
315 box2 = Box.factory[10:30, 40:42]
316 self.assertEqual(box1.intersection(box2), Box.factory[10:20, 40:42])
317 with self.assertRaises(NoOverlapError):
318 box1.intersection(Box.factory[50:70, -10:-5])
320 def test_slicing(self) -> None:
321 """Test slicing."""
322 box = Box.factory[:10, :20]
323 sbox = box.absolute[4:6, :3]
324 self.assertEqual(sbox, Box.factory[4:6, 0:3])
325 sbox = box.local[4:6, :3]
326 self.assertEqual(sbox, Box.factory[4:6, 0:3])
327 sbox = box.absolute[4:, 5:]
328 self.assertEqual(sbox, Box.factory[4:10, 5:20])
329 sbox = box.local[4:, 5:]
330 self.assertEqual(sbox, Box.factory[4:10, 5:20])
331 sbox = box.absolute[XY(slice(5, None), slice(4, None))]
332 self.assertEqual(sbox, Box.factory[4:10, 5:20])
333 sbox = box.local[XY(slice(5, None), slice(4, None))]
334 self.assertEqual(sbox, Box.factory[4:10, 5:20])
336 self.assertEqual(Box.factory[4:10, -1:2], Box.factory[XY(slice(-1, 2), slice(4, 10))])
338 slices = sbox.slice_within(box)
339 self.assertEqual(slices.x.start, 5)
340 self.assertEqual(slices.y.start, 4)
341 self.assertEqual(slices.x.stop, 20)
342 self.assertEqual(slices.y.stop, 10)
344 slices = Box.factory[:5, 110:119].slice_within(Box.factory[-15:12, 90:120])
345 self.assertEqual(slices.x.start, 20)
346 self.assertEqual(slices.y.start, 15)
347 self.assertEqual(slices.x.stop, 29)
348 self.assertEqual(slices.y.stop, 20)
350 with self.assertRaises(IndexError):
351 box.absolute[-1:5, 3:]
352 with self.assertRaises(IndexError):
353 box.local[-1:5, 3:]
354 with self.assertRaises(TypeError):
355 box.absolute[3:5, :5, 4:]
356 with self.assertRaises(TypeError):
357 box.local[3:5, :5, 4:]
358 with self.assertRaises(TypeError):
359 Box.factory[3:5, :6, 4:]
361 def test_mesh(self) -> None:
362 """Test grid creation."""
363 box = Box.factory[0:2, 0:3]
365 grid = box.meshgrid()
366 assert_close(self, grid.x, np.array([[0.0, 1.0, 2.0], [0.0, 1.0, 2.0]]))
367 assert_close(self, grid.y, np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]))
369 grid = box.meshgrid(2)
370 assert_close(self, grid.x, np.array([[0.0, 2.0], [0.0, 2.0]]))
371 assert_close(self, grid.y, np.array([[0.0, 0.0], [1.0, 1.0]]))
373 grid = box.meshgrid([2, 1])
374 assert_close(self, grid.x, np.array([[0.0, 2.0]]))
375 assert_close(self, grid.y, np.array([[0.0, 0.0]]))
377 grid = box.meshgrid(XY(2, 1))
378 assert_close(self, grid.x, np.array([[0.0, 2.0]]))
379 assert_close(self, grid.y, np.array([[0.0, 0.0]]))
381 grid = box.meshgrid(YX(1, 2))
382 assert_close(self, grid.x, np.array([[0.0, 2.0]]))
383 assert_close(self, grid.y, np.array([[0.0, 0.0]]))
385 grid = box.meshgrid(step=3)
386 assert_close(self, grid.x, np.array([[0.0]]))
387 assert_close(self, grid.y, np.array([[0.0]]))
389 with self.assertRaises(TypeError):
390 box.meshgrid(2, step=3)
392 with self.assertRaises(ValueError):
393 box.meshgrid("n")
395 def test_boundary(self) -> None:
396 """Test we can found the boundary."""
397 box = Box.factory[-1:9, 7:15]
398 corners = list(box.boundary())
399 self.assertEqual(corners[0], (-1, 7))
400 self.assertEqual(corners[1], (-1, 14))
401 self.assertEqual(corners[2], (8, 14))
402 self.assertEqual(corners[3], (8, 7))
404 def test_pydantic(self) -> None:
405 """Test roundtrip through pydantic serialization."""
406 box = Box.factory[-1:1, 5:10]
407 model = BoxModel(box=box)
408 j_str = model.model_dump_json()
409 jmodel = BoxModel.model_validate_json(j_str)
410 self.assertEqual(jmodel, model)
411 self.assertEqual(jmodel.box, box)
413 def test_pickle(self) -> None:
414 """Test pickle roundtrip."""
415 box = Box.factory[-1:1, 5:10]
416 d = pickle.dumps(box)
417 copy = pickle.loads(d)
418 self.assertEqual(copy, box)
421if __name__ == "__main__":
422 unittest.main()