Coverage for tests / test_spanSets.py: 13%
314 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 16:53 -0700
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 16:53 -0700
1#
2# LSST Data Management System
3#
4# Copyright 2008-2016 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
24import unittest
25import numpy as np
27import lsst.utils.tests
28import lsst.geom
29import lsst.afw.geom as afwGeom
30import lsst.afw.geom.ellipses as afwGeomEllipses
31import lsst.afw.image as afwImage
34class SpanSetTestCase(lsst.utils.tests.TestCase):
35 '''
36 This is a python level unit test of the SpanSets class. It is mean to work in conjuction
37 with the c++ unit test. The C++ test has much more coverage, and tests some features which
38 are not exposed to python. This test serves mainly as a way to test that the python bindings
39 correctly work. Many of these tests are smaller versions of the C++ tests.
40 '''
42 def testNullSpanSet(self):
43 nullSS = afwGeom.SpanSet()
44 self.assertEqual(nullSS.getArea(), 0)
45 self.assertEqual(len(nullSS), 0)
46 self.assertEqual(nullSS.getBBox().getDimensions().getX(), 0)
47 self.assertEqual(nullSS.getBBox().getDimensions().getY(), 0)
49 def testBBoxSpanSet(self):
50 boxSS = afwGeom.SpanSet(lsst.geom.Box2I(lsst.geom.Point2I(2, 2),
51 lsst.geom.Point2I(6, 6)))
52 self.assertEqual(boxSS.getArea(), 25)
53 bBox = boxSS.getBBox()
54 self.assertEqual(bBox.getMinX(), 2)
55 self.assertEqual(bBox.getMinY(), 2)
57 def testIteratorConstructor(self):
58 spans = [afwGeom.Span(0, 2, 4), afwGeom.Span(1, 2, 4),
59 afwGeom.Span(2, 2, 4)]
60 spanSetFromList = afwGeom.SpanSet(spans)
61 spanSetFromArray = afwGeom.SpanSet(np.array(spans))
63 self.assertEqual(spanSetFromList.getBBox().getMinX(), 2)
64 self.assertEqual(spanSetFromList.getBBox().getMaxX(), 4)
65 self.assertEqual(spanSetFromList.getBBox().getMinY(), 0)
67 self.assertEqual(spanSetFromArray.getBBox().getMinX(), 2)
68 self.assertEqual(spanSetFromArray.getBBox().getMaxX(), 4)
69 self.assertEqual(spanSetFromArray.getBBox().getMinY(), 0)
71 def testIsContiguous(self):
72 spanSetConList = [afwGeom.Span(0, 2, 5), afwGeom.Span(1, 5, 8)]
73 spanSetCon = afwGeom.SpanSet(spanSetConList)
74 self.assertTrue(spanSetCon.isContiguous())
76 spanSetNotConList = [afwGeom.Span(0, 2, 5), afwGeom.Span(1, 20, 25)]
77 spanSetNotCon = afwGeom.SpanSet(spanSetNotConList)
78 self.assertFalse(spanSetNotCon.isContiguous())
80 def testSplit(self):
81 spanSetOne = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(2, 2)
82 spanSetTwo = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(8, 8)
84 spanSetList = []
85 for spn in spanSetOne:
86 spanSetList.append(spn)
87 for spn in spanSetTwo:
88 spanSetList.append(spn)
89 spanSetTogether = afwGeom.SpanSet(spanSetList)
91 spanSetSplit = spanSetTogether.split()
92 self.assertEqual(len(spanSetSplit), 2)
94 for a, b in zip(spanSetOne, spanSetSplit[0]):
95 self.assertEqual(a, b)
97 for a, b in zip(spanSetTwo, spanSetSplit[1]):
98 self.assertEqual(a, b)
100 def testTransform(self):
101 transform = lsst.geom.LinearTransform(np.array([[2.0, 0.0], [0.0, 2.0]]))
102 spanSetPreScale = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.CIRCLE)
103 spanSetPostScale = spanSetPreScale.transformedBy(transform)
105 self.assertEqual(spanSetPostScale.getBBox().getMinX(), -4)
106 self.assertEqual(spanSetPostScale.getBBox().getMinY(), -4)
108 def testOverlaps(self):
109 spanSetNoShift = afwGeom.SpanSet.fromShape(4, afwGeom.Stencil.CIRCLE)
110 spanSetShift = spanSetNoShift.shiftedBy(2, 2)
112 self.assertTrue(spanSetNoShift.overlaps(spanSetShift))
114 def testContains(self):
115 spanSetLarge = afwGeom.SpanSet.fromShape(4, afwGeom.Stencil.CIRCLE)
116 spanSetSmall = afwGeom.SpanSet.fromShape(1, afwGeom.Stencil.CIRCLE)
118 self.assertTrue(spanSetLarge.contains(spanSetSmall))
119 self.assertFalse(spanSetSmall.contains(lsst.geom.Point2I(100, 100)))
121 def testComputeCentroid(self):
122 spanSetShape = afwGeom.SpanSet.fromShape(4, afwGeom.Stencil.CIRCLE).shiftedBy(2, 2)
123 center = spanSetShape.computeCentroid()
125 self.assertEqual(center.getX(), 2)
126 self.assertEqual(center.getY(), 2)
128 def testComputeShape(self):
129 spanSetShape = afwGeom.SpanSet.fromShape(1, afwGeom.Stencil.CIRCLE)
130 quad = spanSetShape.computeShape()
132 self.assertEqual(quad.getIxx(), 0.4)
133 self.assertEqual(quad.getIyy(), 0.4)
134 self.assertEqual(quad.getIxy(), 0)
136 def testdilated(self):
137 spanSetPredilated = afwGeom.SpanSet.fromShape(1, afwGeom.Stencil.CIRCLE)
138 spanSetPostdilated = spanSetPredilated.dilated(1)
140 bBox = spanSetPostdilated.getBBox()
141 self.assertEqual(bBox.getMinX(), -2)
142 self.assertEqual(bBox.getMinY(), -2)
144 def testErode(self):
145 spanSetPreErode = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.CIRCLE)
146 spanSetPostErode = spanSetPreErode.eroded(1)
148 bBox = spanSetPostErode.getBBox()
149 self.assertEqual(bBox.getMinX(), -1)
150 self.assertEqual(bBox.getMinY(), -1)
152 def testFlatten(self):
153 # Give an initial value to an input array
154 inputArray = np.ones((6, 6)) * 9
155 inputArray[1, 1] = 1
156 inputArray[1, 2] = 2
157 inputArray[2, 1] = 3
158 inputArray[2, 2] = 4
160 inputSpanSet = afwGeom.SpanSet([afwGeom.Span(0, 0, 1),
161 afwGeom.Span(1, 0, 1)])
162 flatArr = inputSpanSet.flatten(inputArray, lsst.geom.Point2I(-1, -1))
164 self.assertEqual(flatArr.size, inputSpanSet.getArea())
166 # Test flatttening a 3D array
167 spanSetArea = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX)
168 spanSetArea = spanSetArea.shiftedBy(2, 2)
170 testArray = np.arange(5*5*3).reshape(5, 5, 3)
171 flattened2DArray = spanSetArea.flatten(testArray)
173 truthArray = np.arange(5*5*3).reshape(5*5, 3)
174 self.assertFloatsAlmostEqual(flattened2DArray, truthArray)
176 def testUnflatten(self):
177 inputArray = np.ones(6) * 4
178 inputSpanSet = afwGeom.SpanSet([afwGeom.Span(9, 2, 3),
179 afwGeom.Span(10, 3, 4),
180 afwGeom.Span(11, 2, 3)])
181 outputArray = inputSpanSet.unflatten(inputArray)
183 arrayShape = outputArray.shape
184 bBox = inputSpanSet.getBBox()
185 self.assertEqual(arrayShape[0], bBox.getHeight())
186 self.assertEqual(arrayShape[1], bBox.getWidth())
188 # Test unflattening a 2D array
189 spanSetArea = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX)
190 spanSetArea = spanSetArea.shiftedBy(2, 2)
192 testArray = np.arange(5*5*3).reshape(5*5, 3)
193 unflattened3DArray = spanSetArea.unflatten(testArray)
195 truthArray = np.arange(5*5*3).reshape(5, 5, 3)
196 self.assertFloatsAlmostEqual(unflattened3DArray, truthArray)
198 def populateMask(self):
199 msk = afwImage.Mask(10, 10, 1)
200 spanSetMask = afwGeom.SpanSet.fromShape(3, afwGeom.Stencil.CIRCLE).shiftedBy(5, 5)
201 spanSetMask.setMask(msk, 2)
202 return msk, spanSetMask
204 def testSetMask(self):
205 mask, spanSetMask = self.populateMask()
206 mskArray = mask.getArray()
207 for i in range(mskArray.shape[0]):
208 for j in range(mskArray.shape[1]):
209 if lsst.geom.Point2I(i, j) in spanSetMask:
210 self.assertEqual(mskArray[i, j], 3)
211 else:
212 self.assertEqual(mskArray[i, j], 1)
214 def testClearMask(self):
215 mask, spanSetMask = self.populateMask()
216 spanSetMask.clearMask(mask, 2)
217 mskArray = mask.getArray()
218 for i in range(mskArray.shape[0]):
219 for j in range(mskArray.shape[1]):
220 self.assertEqual(mskArray[i, j], 1)
222 def makeOverlapSpanSets(self):
223 firstSpanSet = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(2, 4)
224 secondSpanSet = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(2, 2)
225 return firstSpanSet, secondSpanSet
227 def makeMaskAndSpanSetForOperationTest(self):
228 firstMaskPart = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(3, 2)
229 secondMaskPart = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(3, 8)
230 spanSetMaskOperation = afwGeom.SpanSet.fromShape(2, afwGeom.Stencil.BOX).shiftedBy(3, 5)
232 mask = afwImage.Mask(20, 20)
233 firstMaskPart.setMask(mask, 3)
234 secondMaskPart.setMask(mask, 3)
235 spanSetMaskOperation.setMask(mask, 4)
237 return mask, spanSetMaskOperation
239 def testIntersection(self):
240 firstSpanSet, secondSpanSet = self.makeOverlapSpanSets()
242 overlap = firstSpanSet.intersect(secondSpanSet)
243 for i, span in enumerate(overlap):
244 self.assertEqual(span.getY(), i+2)
245 self.assertEqual(span.getMinX(), 0)
246 self.assertEqual(span.getMaxX(), 4)
248 mask, spanSetMaskOperation = self.makeMaskAndSpanSetForOperationTest()
249 spanSetIntersectMask = spanSetMaskOperation.intersect(mask, 2)
251 expectedYRange = [3, 4, 6, 7]
252 for expected, val in zip(expectedYRange, spanSetIntersectMask):
253 self.assertEqual(expected, val.getY())
255 def testIntersectionWithGap(self):
256 # This test was created in DM-47945 to fix a bug where the
257 # intersection of two spansets where multiple spans have the same
258 # y-value would fail.
259 spans1 = afwGeom.SpanSet([afwGeom.Span(1, 0, 3), afwGeom.Span(1, 5, 7)])
260 spans2 = afwGeom.SpanSet([afwGeom.Span(1, 2, 6)])
261 intersection = spans1.intersect(spans2)
262 trueIntersection = afwGeom.SpanSet([afwGeom.Span(1, 2, 3), afwGeom.Span(1, 5, 6)])
264 self.assertEqual(len(intersection), 2)
266 for a, b in zip(intersection, trueIntersection):
267 self.assertEqual(a, b)
269 def testIntersectNot(self):
270 firstSpanSet, secondSpanSet = self.makeOverlapSpanSets()
272 overlap = firstSpanSet.intersectNot(secondSpanSet)
273 for yVal, span in enumerate(overlap):
274 self.assertEqual(span.getY(), yVal+5)
275 self.assertEqual(span.getMinX(), 0)
276 self.assertEqual(span.getMaxX(), 4)
278 mask, spanSetMaskOperation = self.makeMaskAndSpanSetForOperationTest()
280 spanSetIntersectNotMask = spanSetMaskOperation.intersectNot(mask, 2)
282 self.assertEqual(len(spanSetIntersectNotMask), 1)
283 self.assertEqual(next(iter(spanSetIntersectNotMask)).getY(), 5)
285 # More complicated intersection with disconnected SpanSets
286 spanList1 = [afwGeom.Span(0, 0, 10),
287 afwGeom.Span(1, 0, 10),
288 afwGeom.Span(2, 0, 10)]
290 spanList2 = [afwGeom.Span(1, 2, 4), afwGeom.Span(1, 7, 8)]
292 resultList = [afwGeom.Span(0, 0, 10),
293 afwGeom.Span(1, 0, 1),
294 afwGeom.Span(1, 5, 6),
295 afwGeom.Span(1, 9, 10),
296 afwGeom.Span(2, 0, 10)]
298 spanSet1 = afwGeom.SpanSet(spanList1)
299 spanSet2 = afwGeom.SpanSet(spanList2)
300 expectedSpanSet = afwGeom.SpanSet(resultList)
302 outputSpanSet = spanSet1.intersectNot(spanSet2)
304 self.assertEqual(outputSpanSet, expectedSpanSet)
306 numIntersectNotTrials = 100
307 spanRow = 5
308 # Set a seed for random functions
309 np.random.seed(400)
310 for N in range(numIntersectNotTrials):
311 # Create two random SpanSets, both with holes in them
312 listOfRandomSpanSets = []
313 for i in range(2):
314 # Make two rectangles to be turned into a SpanSet
315 rand1 = np.random.randint(0, 26, 2)
316 rand2 = np.random.randint(rand1.max(), 51, 2)
317 tempList = [afwGeom.Span(spanRow, rand1.min(), rand1.max()),
318 afwGeom.Span(spanRow, rand2.min(), rand2.max())]
319 listOfRandomSpanSets.append(afwGeom.SpanSet(tempList))
321 # IntersectNot the SpanSets, randomly choosing which one is the one
322 # to be the negated SpanSet
323 randChoice = np.random.randint(0, 2)
324 negatedRandChoice = int(not randChoice)
325 sourceSpanSet = listOfRandomSpanSets[randChoice]
326 targetSpanSet = listOfRandomSpanSets[negatedRandChoice]
327 resultSpanSet = sourceSpanSet.intersectNot(targetSpanSet)
328 for span in resultSpanSet:
329 for point in span:
330 self.assertTrue(sourceSpanSet.contains(point))
331 self.assertFalse(targetSpanSet.contains(point))
333 for x in range(51):
334 point = lsst.geom.Point2I(x, spanRow)
335 if sourceSpanSet.contains(point) and not\
336 targetSpanSet.contains(point):
337 self.assertTrue(resultSpanSet.contains(point))
339 def testIntersectNotWithGap(self):
340 # This test was created in DM-47945 to fix a bug where the
341 # intersection of two spansets where multiple spans have the same
342 # y-value would fail.
343 spans1 = afwGeom.SpanSet([afwGeom.Span(1, 0, 3), afwGeom.Span(1, 5, 7)])
344 spans2 = afwGeom.SpanSet([afwGeom.Span(1, 2, 6)])
345 intersectNot = spans1.intersectNot(spans2)
346 trueIntersectNot = afwGeom.SpanSet([afwGeom.Span(1, 0, 1), afwGeom.Span(1, 7, 7)])
348 self.assertEqual(len(intersectNot), 2)
350 for a, b in zip(intersectNot, trueIntersectNot):
351 self.assertEqual(a, b)
353 def testUnion(self):
354 firstSpanSet, secondSpanSet = self.makeOverlapSpanSets()
356 overlap = firstSpanSet.union(secondSpanSet)
358 for yVal, span in enumerate(overlap):
359 self.assertEqual(span.getY(), yVal)
360 self.assertEqual(span.getMinX(), 0)
361 self.assertEqual(span.getMaxX(), 4)
363 mask, spanSetMaskOperation = self.makeMaskAndSpanSetForOperationTest()
365 spanSetUnion = spanSetMaskOperation.union(mask, 2)
367 for yVal, span in enumerate(spanSetUnion):
368 self.assertEqual(span.getY(), yVal)
370 def testMaskToSpanSet(self):
371 mask, _ = self.makeMaskAndSpanSetForOperationTest()
372 spanSetFromMask = afwGeom.SpanSet.fromMask(mask)
374 for yCoord, span in enumerate(spanSetFromMask):
375 self.assertEqual(span, afwGeom.Span(yCoord, 1, 5))
377 def testFromMask(self):
378 xy0 = lsst.geom.Point2I(12345, 67890) # xy0 for image
379 dims = lsst.geom.Extent2I(123, 45) # Dimensions of image
380 box = lsst.geom.Box2I(xy0, dims) # Bounding box of image
381 value = 32
382 other = 16
383 assert value & other == 0 # Setting 'other' unsets 'value'
384 point = lsst.geom.Point2I(3 + xy0.getX(), 3 + xy0.getY()) # Point in the image
386 mask = afwImage.Mask(box)
387 mask.set(value)
389 # We can create a SpanSet from bit planes
390 spans = afwGeom.SpanSet.fromMask(mask, value)
391 self.assertEqual(spans.getArea(), box.getArea())
392 self.assertEqual(spans.getBBox(), box)
393 self.assertTrue(point in spans)
395 # Pixels not matching the desired bit plane are ignored as they should be
396 mask[point] = other # unset one pixel
397 spans = afwGeom.SpanSet.fromMask(mask, value)
398 self.assertEqual(spans.getArea(), box.getArea() - 1)
399 self.assertEqual(spans.getBBox(), box)
400 self.assertFalse(point in spans)
402 def testEquality(self):
403 firstSpanSet, secondSpanSet = self.makeOverlapSpanSets()
404 secondSpanSetShift = secondSpanSet.shiftedBy(0, 2)
406 self.assertFalse(firstSpanSet == secondSpanSet)
407 self.assertTrue(firstSpanSet != secondSpanSet)
408 self.assertTrue(firstSpanSet == secondSpanSetShift)
409 self.assertFalse(firstSpanSet != secondSpanSetShift)
411 def testSpanSetFromEllipse(self):
412 axes = afwGeomEllipses.Axes(6, 6, 0)
413 ellipse = afwGeom.Ellipse(axes, lsst.geom.Point2D(5, 6))
414 spanSet = afwGeom.SpanSet.fromShape(ellipse)
415 for ss, es in zip(spanSet, afwGeomEllipses.PixelRegion(ellipse)):
416 self.assertEqual(ss, es)
418 def testfromShapeOffset(self):
419 shift = lsst.geom.Point2I(2, 2)
420 spanSetShifted = afwGeom.SpanSet.fromShape(2, offset=shift)
421 bbox = spanSetShifted.getBBox()
422 self.assertEqual(bbox.getMinX(), 0)
423 self.assertEqual(bbox.getMinY(), 0)
425 def testFindEdgePixels(self):
426 spanSet = afwGeom.SpanSet.fromShape(6, afwGeom.Stencil.CIRCLE)
427 spanSetEdge = spanSet.findEdgePixels()
429 truthSpans = [afwGeom.Span(-6, 0, 0),
430 afwGeom.Span(-5, -3, -1),
431 afwGeom.Span(-5, 1, 3),
432 afwGeom.Span(-4, -4, -4),
433 afwGeom.Span(-4, 4, 4),
434 afwGeom.Span(-3, -5, -5),
435 afwGeom.Span(-3, 5, 5),
436 afwGeom.Span(-2, -5, -5),
437 afwGeom.Span(-2, 5, 5),
438 afwGeom.Span(-1, -5, -5),
439 afwGeom.Span(-1, 5, 5),
440 afwGeom.Span(0, -6, -6),
441 afwGeom.Span(0, 6, 6),
442 afwGeom.Span(1, -5, -5),
443 afwGeom.Span(1, 5, 5),
444 afwGeom.Span(2, -5, -5),
445 afwGeom.Span(2, 5, 5),
446 afwGeom.Span(3, -5, -5),
447 afwGeom.Span(3, 5, 5),
448 afwGeom.Span(4, -4, -4),
449 afwGeom.Span(4, 4, 4),
450 afwGeom.Span(5, -3, -1),
451 afwGeom.Span(5, 1, 3),
452 afwGeom.Span(6, 0, 0)]
453 truthSpanSet = afwGeom.SpanSet(truthSpans)
454 self.assertEqual(spanSetEdge, truthSpanSet)
456 def testIndices(self):
457 dataArray = np.zeros((5, 5))
458 spanSet = afwGeom.SpanSet.fromShape(2,
459 afwGeom.Stencil.BOX,
460 offset=(2, 2))
461 yind, xind = spanSet.indices()
462 dataArray[yind, xind] = 9
463 self.assertTrue((dataArray == 9).all())
465 def testSpanIteration(self):
466 span = afwGeom.Span(4, 3, 8)
467 points = list(span)
468 self.assertEqual(len(span), len(points))
469 self.assertEqual(points, [lsst.geom.Point2I(x, 4) for x in range(3, 9)])
471 def testAsArray(self):
472 spans = afwGeom.SpanSet(lsst.geom.Box2I(lsst.geom.Point2I(2, 3), lsst.geom.Point2I(6, 7)))
473 truth = np.ones((5, 5), dtype=bool)
474 arr = spans.asArray()
475 np.testing.assert_array_equal(arr, truth)
477 shape = (10, 10)
478 truth = np.zeros(shape, dtype=bool)
479 truth[:5, :5] = 1
480 arr = spans.asArray(shape)
481 np.testing.assert_array_equal(arr, truth)
483 truth = np.zeros(shape, dtype=bool)
484 truth[3:8, 2:7] = 1
485 arr = spans.asArray(shape, (0, 0))
486 np.testing.assert_array_equal(arr, truth)
489class TestMemory(lsst.utils.tests.MemoryTestCase):
490 pass
493def setup_module(module):
494 lsst.utils.tests.init()
497if __name__ == "__main__": 497 ↛ 498line 497 didn't jump to line 498 because the condition on line 497 was never true
498 lsst.utils.tests.init()
499 unittest.main()