Coverage for tests / test_geom.py: 11%

287 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-15 00:04 +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 pickle 

15import unittest 

16 

17import numpy as np 

18import pydantic 

19 

20from lsst.images import XY, YX, Box, Interval 

21from lsst.images.tests import assert_close 

22 

23 

24class IntervalModel(pydantic.BaseModel): 

25 """Test Pydantic model with an Interval.""" 

26 

27 interval1: Interval 

28 interval2: Interval 

29 

30 

31class BoxModel(pydantic.BaseModel): 

32 """Test Pydantic model with a box.""" 

33 

34 box: Box 

35 

36 

37class XYYXTestCase(unittest.TestCase): 

38 """Test the XY and YX classes.""" 

39 

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) 

46 

47 def _plus_one(v: int) -> int: 

48 return v + 1 

49 

50 new = yx.map(_plus_one) 

51 self.assertEqual(new, (6, 8)) 

52 

53 xy = yx.xy 

54 self.assertEqual(xy, (7, 5)) 

55 

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) 

62 

63 def _plus_one(v: int) -> int: 

64 return v + 1 

65 

66 new = xy.map(_plus_one) 

67 self.assertEqual(new, (6, 8)) 

68 

69 yx = xy.yx 

70 self.assertEqual(yx, (7, 5)) 

71 

72 

73class IntervalTestCase(unittest.TestCase): 

74 """Test the Interval class.""" 

75 

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) 

86 

87 self.assertEqual(str(i), "1:10") 

88 

89 shifted = i + 10 

90 self.assertEqual(shifted.start, 11) 

91 self.assertEqual(shifted.stop, 20) 

92 

93 shifted = i - 10 

94 self.assertEqual(shifted.start, -9) 

95 self.assertEqual(shifted.stop, 0) 

96 

97 self.assertEqual(shifted, i - 10) 

98 self.assertNotEqual(shifted, i) 

99 

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)) 

104 

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)) 

109 

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) 

115 

116 i2 = Interval(start=2, stop=4) 

117 self.assertTrue(i.contains(i2)) 

118 

119 i3 = Interval(start=-1, stop=5) 

120 self.assertFalse(i.contains(i3)) 

121 

122 self.assertTrue(i3.contains(2.5)) 

123 

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]) 

126 

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])) 

130 

131 self.assertNotEqual(i, []) 

132 

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) 

140 

141 subset = i.absolute[5:10] 

142 self.assertEqual(subset.start, 5) 

143 self.assertEqual(subset.stop, 10) 

144 

145 subset = i.local[5:10] 

146 self.assertEqual(subset.start, 8) 

147 self.assertEqual(subset.stop, 13) 

148 

149 subset = i.absolute[:10] 

150 self.assertEqual(subset.start, 3) 

151 self.assertEqual(subset.stop, 10) 

152 

153 subset = i.local[:10] 

154 self.assertEqual(subset.start, 3) 

155 self.assertEqual(subset.stop, 13) 

156 

157 subset = i.absolute[10:] 

158 self.assertEqual(subset.start, 10) 

159 self.assertEqual(subset.stop, 20) 

160 

161 subset = i.local[10:] 

162 self.assertEqual(subset.start, 13) 

163 self.assertEqual(subset.stop, 20) 

164 

165 subset = i.local[3:-2] 

166 self.assertEqual(subset.start, 6) 

167 self.assertEqual(subset.stop, 18) 

168 

169 subset = i.local[-5:-2] 

170 self.assertEqual(subset.start, 15) 

171 self.assertEqual(subset.stop, 18) 

172 

173 with self.assertRaises(IndexError): 

174 i.absolute[:30] 

175 

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) 

181 

182 with self.assertRaises(IndexError): 

183 i.absolute[30:] 

184 

185 with self.assertRaises(IndexError): 

186 i.local[30:] 

187 

188 with self.assertRaises(IndexError): 

189 i.absolute[-1:10] 

190 

191 with self.assertRaises(IndexError): 

192 i.local[-1:10] 

193 

194 with self.assertRaises(ValueError): 

195 i.absolute[::2] 

196 

197 with self.assertRaises(ValueError): 

198 i.local[::2] 

199 

200 with self.assertRaises(ValueError): 

201 Interval.factory[1:2:2] 

202 

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)) 

208 

209 s = i.slice_within(Interval(start=-1, stop=12)) 

210 self.assertEqual(s.start, 2) 

211 self.assertEqual(s.stop, 11) 

212 

213 with self.assertRaises(IndexError): 

214 i.slice_within(Interval(start=3, stop=5)) 

215 

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) 

224 

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])) 

228 

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) 

238 

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) 

245 

246 

247class BoxTestCase(unittest.TestCase): 

248 """Test the Box implementation.""" 

249 

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) 

255 

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, []) 

261 

262 box2 = Box.factory[-5:5, 32:64] 

263 self.assertEqual(box2, box) 

264 

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]) 

269 

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]) 

274 

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]) 

279 

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]) 

286 

287 box = Box.factory[1:2, -1:1] 

288 grown = box.dilated_by(2) 

289 self.assertEqual(grown, Box.factory[-1:4, -3:3]) 

290 

291 def test_contains(self) -> None: 

292 """Does a box fit inside another or not.""" 

293 box = Box.factory[0:20, 10:40] 

294 

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)) 

299 

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]) 

306 

307 with self.assertRaises(TypeError): 

308 box.contains(box, x=3, y=2) 

309 with self.assertRaises(TypeError): 

310 box.contains() 

311 

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 self.assertIsNone(box1.intersection(Box.factory[50:70, -10:-5])) 

318 

319 def test_slicing(self) -> None: 

320 """Test slicing.""" 

321 box = Box.factory[:10, :20] 

322 sbox = box.absolute[4:6, :3] 

323 self.assertEqual(sbox, Box.factory[4:6, 0:3]) 

324 sbox = box.local[4:6, :3] 

325 self.assertEqual(sbox, Box.factory[4:6, 0:3]) 

326 sbox = box.absolute[4:, 5:] 

327 self.assertEqual(sbox, Box.factory[4:10, 5:20]) 

328 sbox = box.local[4:, 5:] 

329 self.assertEqual(sbox, Box.factory[4:10, 5:20]) 

330 sbox = box.absolute[XY(slice(5, None), slice(4, None))] 

331 self.assertEqual(sbox, Box.factory[4:10, 5:20]) 

332 sbox = box.local[XY(slice(5, None), slice(4, None))] 

333 self.assertEqual(sbox, Box.factory[4:10, 5:20]) 

334 

335 self.assertEqual(Box.factory[4:10, -1:2], Box.factory[XY(slice(-1, 2), slice(4, 10))]) 

336 

337 slices = sbox.slice_within(box) 

338 self.assertEqual(slices.x.start, 5) 

339 self.assertEqual(slices.y.start, 4) 

340 self.assertEqual(slices.x.stop, 20) 

341 self.assertEqual(slices.y.stop, 10) 

342 

343 slices = Box.factory[:5, 110:119].slice_within(Box.factory[-15:12, 90:120]) 

344 self.assertEqual(slices.x.start, 20) 

345 self.assertEqual(slices.y.start, 15) 

346 self.assertEqual(slices.x.stop, 29) 

347 self.assertEqual(slices.y.stop, 20) 

348 

349 with self.assertRaises(IndexError): 

350 box.absolute[-1:5, 3:] 

351 with self.assertRaises(IndexError): 

352 box.local[-1:5, 3:] 

353 with self.assertRaises(TypeError): 

354 box.absolute[3:5, :5, 4:] 

355 with self.assertRaises(TypeError): 

356 box.local[3:5, :5, 4:] 

357 with self.assertRaises(TypeError): 

358 Box.factory[3:5, :6, 4:] 

359 

360 def test_mesh(self) -> None: 

361 """Test grid creation.""" 

362 box = Box.factory[0:2, 0:3] 

363 

364 grid = box.meshgrid() 

365 assert_close(self, grid.x, np.array([[0.0, 1.0, 2.0], [0.0, 1.0, 2.0]])) 

366 assert_close(self, grid.y, np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])) 

367 

368 grid = box.meshgrid(2) 

369 assert_close(self, grid.x, np.array([[0.0, 2.0], [0.0, 2.0]])) 

370 assert_close(self, grid.y, np.array([[0.0, 0.0], [1.0, 1.0]])) 

371 

372 grid = box.meshgrid([2, 1]) 

373 assert_close(self, grid.x, np.array([[0.0, 2.0]])) 

374 assert_close(self, grid.y, np.array([[0.0, 0.0]])) 

375 

376 grid = box.meshgrid(XY(2, 1)) 

377 assert_close(self, grid.x, np.array([[0.0, 2.0]])) 

378 assert_close(self, grid.y, np.array([[0.0, 0.0]])) 

379 

380 grid = box.meshgrid(YX(1, 2)) 

381 assert_close(self, grid.x, np.array([[0.0, 2.0]])) 

382 assert_close(self, grid.y, np.array([[0.0, 0.0]])) 

383 

384 grid = box.meshgrid(step=3) 

385 assert_close(self, grid.x, np.array([[0.0]])) 

386 assert_close(self, grid.y, np.array([[0.0]])) 

387 

388 with self.assertRaises(TypeError): 

389 box.meshgrid(2, step=3) 

390 

391 with self.assertRaises(ValueError): 

392 box.meshgrid("n") 

393 

394 def test_boundary(self) -> None: 

395 """Test we can found the boundary.""" 

396 box = Box.factory[-1:9, 7:15] 

397 corners = list(box.boundary()) 

398 self.assertEqual(corners[0], (-1, 7)) 

399 self.assertEqual(corners[1], (-1, 14)) 

400 self.assertEqual(corners[2], (8, 14)) 

401 self.assertEqual(corners[3], (8, 7)) 

402 

403 def test_pydantic(self) -> None: 

404 """Test roundtrip through pydantic serialization.""" 

405 box = Box.factory[-1:1, 5:10] 

406 model = BoxModel(box=box) 

407 j_str = model.model_dump_json() 

408 jmodel = BoxModel.model_validate_json(j_str) 

409 self.assertEqual(jmodel, model) 

410 self.assertEqual(jmodel.box, box) 

411 

412 def test_pickle(self) -> None: 

413 """Test pickle roundtrip.""" 

414 box = Box.factory[-1:1, 5:10] 

415 d = pickle.dumps(box) 

416 copy = pickle.loads(d) 

417 self.assertEqual(copy, box) 

418 

419 

420if __name__ == "__main__": 

421 unittest.main()