Coverage for tests/test_polygon.py: 10%

281 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-22 03:22 -0800

1# This file is part of afw. 

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# 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 GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import pickle 

23import unittest 

24 

25import numpy as np 

26 

27import lsst.utils.tests 

28import lsst.geom 

29import lsst.afw.geom as afwGeom 

30import lsst.afw.image # noqa: F401 required by Polygon.createImage 

31 

32display = False 

33doPause = False # If False, autoscan plots with 2 sec delay. If True, impose manual closing of plots. 

34 

35 

36def circle(radius, num, x0=0.0, y0=0.0): 

37 """Generate points on a circle 

38 

39 @param radius: radius of circle 

40 @param num: number of points 

41 @param x0,y0: Offset in x,y 

42 @return x,y coordinates as numpy array 

43 """ 

44 theta = np.linspace(0, 2*np.pi, num=num, endpoint=False) 

45 x = radius*np.cos(theta) + x0 

46 y = radius*np.sin(theta) + y0 

47 return np.array([x, y]).transpose() 

48 

49 

50class PolygonTest(lsst.utils.tests.TestCase): 

51 

52 def setUp(self): 

53 self.x0 = 0.0 

54 self.y0 = 0.0 

55 

56 def polygon(self, num, radius=1.0, x0=None, y0=None): 

57 """Generate a polygon 

58 

59 @param num: Number of points 

60 @param radius: Radius of polygon 

61 @param x0,y0: Offset of center 

62 @return polygon 

63 """ 

64 if x0 is None: 

65 x0 = self.x0 

66 if y0 is None: 

67 y0 = self.y0 

68 points = circle(radius, num, x0=x0, y0=y0) 

69 return afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in reversed(points)]) 

70 

71 def square(self, size=1.0, x0=0, y0=0): 

72 """Generate a square 

73 

74 @param size: Half-length of the sides 

75 @param x0,y0: Offset of center 

76 """ 

77 return afwGeom.Polygon([lsst.geom.Point2D(size*x + x0, size*y + y0) for 

78 x, y in ((-1, -1), (-1, 1), (1, 1), (1, -1))]) 

79 

80 def testGetters(self): 

81 """Test Polygon getters""" 

82 for num in range(3, 30): 

83 poly = self.polygon(num) 

84 self.assertEqual(poly, poly) 

85 self.assertNotEqual(poly, self.square(1.0, 2.0, 3.0)) 

86 self.assertEqual(poly.getNumEdges(), num) 

87 # One extra for the closing point 

88 self.assertEqual(len(poly.getVertices()), num + 1) 

89 self.assertEqual(len(poly.getEdges()), num) 

90 perimeter = 0.0 

91 for p1, p2 in poly.getEdges(): 

92 perimeter += np.hypot(p1.getX() - p2.getX(), 

93 p1.getY() - p2.getY()) 

94 self.assertAlmostEqual(poly.calculatePerimeter(), perimeter) 

95 

96 size = 3.0 

97 poly = self.square(size=size) 

98 self.assertEqual(poly.calculateArea(), (2*size)**2) 

99 self.assertEqual(poly.calculatePerimeter(), 2*size*4) 

100 edges = poly.getEdges() 

101 self.assertEqual(len(edges), 4) 

102 perimeter = 0.0 

103 for p1, p2 in edges: 

104 self.assertEqual(abs(p1.getX()), size) 

105 self.assertEqual(abs(p1.getY()), size) 

106 self.assertEqual(abs(p2.getX()), size) 

107 self.assertEqual(abs(p2.getY()), size) 

108 self.assertNotEqual(p1, p2) 

109 

110 def testPickle(self): 

111 for num in range(3, 30): 

112 poly = self.polygon(num) 

113 self.assertEqual(pickle.loads(pickle.dumps(poly)), poly) 

114 

115 def testFromBox(self): 

116 size = 1.0 

117 poly1 = self.square(size=size) 

118 box = lsst.geom.Box2D(lsst.geom.Point2D(-1.0, -1.0), 

119 lsst.geom.Point2D(1.0, 1.0)) 

120 poly2 = afwGeom.Polygon(box) 

121 self.assertEqual(poly1, poly2) 

122 

123 def testBBox(self): 

124 """Test Polygon.getBBox""" 

125 size = 3.0 

126 poly = self.square(size=size) 

127 box = poly.getBBox() 

128 self.assertEqual(box.getMinX(), -size) 

129 self.assertEqual(box.getMinY(), -size) 

130 self.assertEqual(box.getMaxX(), size) 

131 self.assertEqual(box.getMaxY(), size) 

132 

133 def testCenter(self): 

134 """Test Polygon.calculateCenter""" 

135 for num in range(3, 30): 

136 poly = self.polygon(num) 

137 center = poly.calculateCenter() 

138 self.assertAlmostEqual(center.getX(), self.x0) 

139 self.assertAlmostEqual(center.getY(), self.y0) 

140 

141 def testContains(self): 

142 """Test Polygon.contains""" 

143 radius = 1.0 

144 for num in range(3, 30): 

145 poly = self.polygon(num, radius=radius) 

146 self.assertTrue(poly.contains(lsst.geom.Point2D(self.x0, self.y0))) 

147 self.assertFalse(poly.contains( 

148 lsst.geom.Point2D(self.x0 + radius, self.y0 + radius))) 

149 # validate checking an array 

150 poly = self.polygon(50, radius=10) 

151 # check for inclusion 

152 yind, xind = np.mgrid[-4:5, -4:5] 

153 self.assertTrue(np.all(poly.contains(xind, yind))) 

154 # check for exclusion 

155 yind, xind = np.mgrid[30:35, 30:35] 

156 self.assertTrue(np.all(~poly.contains(xind, yind))) 

157 

158 # check various box types 

159 box = lsst.geom.Box2I(lsst.geom.Point2I(-1, -1), lsst.geom.Point2I(1, 1)) 

160 self.assertTrue(all(poly.contains(box.getCorners()))) 

161 

162 box = lsst.geom.Box2I(lsst.geom.Point2I(-100, -100), lsst.geom.Point2I(100, 100)) 

163 self.assertFalse(all(poly.contains(box.getCorners()))) 

164 

165 box = lsst.geom.Box2D(lsst.geom.Point2D(-1, -1), lsst.geom.Point2D(1, 1)) 

166 self.assertTrue(all(poly.contains(box.getCorners()))) 

167 

168 box = lsst.geom.Box2D(lsst.geom.Point2D(-100, -100), lsst.geom.Point2D(100, 100)) 

169 self.assertFalse(all(poly.contains(box.getCorners()))) 

170 

171 def testOverlaps(self): 

172 """Test Polygon.overlaps""" 

173 radius = 1.0 

174 for num in range(3, 30): 

175 poly1 = self.polygon(num, radius=radius) 

176 poly2 = self.polygon(num, radius=radius, x0=radius, y0=radius) 

177 poly3 = self.polygon(num, radius=2*radius) 

178 poly4 = self.polygon(num, radius=radius, x0=3*radius, y0=3*radius) 

179 self.assertTrue(poly1.overlaps(poly2)) 

180 self.assertTrue(poly2.overlaps(poly1)) 

181 self.assertTrue(poly1.overlaps(poly3)) 

182 self.assertTrue(poly3.overlaps(poly1)) 

183 self.assertFalse(poly1.overlaps(poly4)) 

184 self.assertFalse(poly4.overlaps(poly1)) 

185 

186 def testIntersection(self): 

187 """Test Polygon.intersection""" 

188 poly1 = self.square(2.0, -1.0, -1.0) 

189 poly2 = self.square(2.0, +1.0, +1.0) 

190 poly3 = self.square(1.0, 0.0, 0.0) 

191 poly4 = self.square(1.0, +5.0, +5.0) 

192 

193 # intersectionSingle: assumes there's a single intersection (convex 

194 # polygons) 

195 self.assertEqual(poly1.intersectionSingle(poly2), poly3) 

196 self.assertEqual(poly2.intersectionSingle(poly1), poly3) 

197 self.assertRaises(afwGeom.SinglePolygonException, 

198 poly1.intersectionSingle, poly4) 

199 self.assertRaises(afwGeom.SinglePolygonException, 

200 poly4.intersectionSingle, poly1) 

201 

202 # intersection: no assumptions 

203 polyList1 = poly1.intersection(poly2) 

204 polyList2 = poly2.intersection(poly1) 

205 self.assertEqual(polyList1, polyList2) 

206 self.assertEqual(len(polyList1), 1) 

207 self.assertEqual(polyList1[0], poly3) 

208 polyList3 = poly1.intersection(poly4) 

209 polyList4 = poly4.intersection(poly1) 

210 self.assertEqual(polyList3, polyList4) 

211 self.assertEqual(len(polyList3), 0) 

212 

213 def testUnion(self): 

214 """Test Polygon.union""" 

215 poly1 = self.square(2.0, -1.0, -1.0) 

216 poly2 = self.square(2.0, +1.0, +1.0) 

217 poly3 = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in 

218 ((-3.0, -3.0), (-3.0, +1.0), (-1.0, +1.0), (-1.0, +3.0), 

219 (+3.0, +3.0), (+3.0, -1.0), (+1.0, -1.0), (+1.0, -3.0))]) 

220 poly4 = self.square(1.0, +5.0, +5.0) 

221 

222 # unionSingle: assumes there's a single union (intersecting polygons) 

223 self.assertEqual(poly1.unionSingle(poly2), poly3) 

224 self.assertEqual(poly2.unionSingle(poly1), poly3) 

225 self.assertRaises(afwGeom.SinglePolygonException, poly1.unionSingle, poly4) 

226 self.assertRaises(afwGeom.SinglePolygonException, poly4.unionSingle, poly1) 

227 

228 # union: no assumptions 

229 polyList1 = poly1.union(poly2) 

230 polyList2 = poly2.union(poly1) 

231 self.assertEqual(polyList1, polyList2) 

232 self.assertEqual(len(polyList1), 1) 

233 self.assertEqual(polyList1[0], poly3) 

234 polyList3 = poly1.union(poly4) 

235 polyList4 = poly4.union(poly1) 

236 self.assertEqual(len(polyList3), 2) 

237 self.assertEqual(len(polyList3), len(polyList4)) 

238 self.assertTrue((polyList3[0] == polyList4[0] and polyList3[1] == polyList4[1]) 

239 or (polyList3[0] == polyList4[1] and polyList3[1] == polyList4[0])) 

240 self.assertTrue((polyList3[0] == poly1 and polyList3[1] == poly4) 

241 or (polyList3[0] == poly4 and polyList3[1] == poly1)) 

242 

243 def testSymDifference(self): 

244 """Test Polygon.symDifference""" 

245 poly1 = self.square(2.0, -1.0, -1.0) 

246 poly2 = self.square(2.0, +1.0, +1.0) 

247 

248 poly3 = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in 

249 ((-3.0, -3.0), (-3.0, +1.0), (-1.0, +1.0), 

250 (-1.0, -1.0), (+1.0, -1.0), (1.0, -3.0))]) 

251 poly4 = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in 

252 ((-1.0, +1.0), (-1.0, +3.0), (+3.0, +3.0), 

253 (+3.0, -1.0), (+1.0, -1.0), (1.0, +1.0))]) 

254 

255 diff1 = poly1.symDifference(poly2) 

256 diff2 = poly2.symDifference(poly1) 

257 

258 self.assertEqual(len(diff1), 2) 

259 self.assertEqual(len(diff2), 2) 

260 self.assertTrue((diff1[0] == diff2[0] and diff1[1] == diff2[1]) 

261 or (diff1[1] == diff2[0] and diff1[0] == diff2[1])) 

262 self.assertTrue((diff1[0] == poly3 and diff1[1] == poly4) 

263 or (diff1[1] == poly3 and diff1[0] == poly4)) 

264 

265 def testConvexHull(self): 

266 """Test Polygon.convexHull""" 

267 poly1 = self.square(2.0, -1.0, -1.0) 

268 poly2 = self.square(2.0, +1.0, +1.0) 

269 poly = poly1.unionSingle(poly2) 

270 expected = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in 

271 ((-3.0, -3.0), (-3.0, +1.0), (-1.0, +3.0), 

272 (+3.0, +3.0), (+3.0, -1.0), (+1.0, -3.0))]) 

273 self.assertEqual(poly.convexHull(), expected) 

274 

275 def testImage(self): 

276 """Test Polygon.createImage""" 

277 if display: 

278 import lsst.afw.display as afwDisplay 

279 

280 for i, num in enumerate(range(3, 30)): 

281 # We need to test at different centers, because the 

282 # boost::intersection algorithm depends sensitively on 

283 # input floating-point values, and you will get different 

284 # aliasing depending on the central pixel value when 

285 # generating the polygon from numpy values. 

286 for cent in [-75, -50, -25, 0, 25, 50, 75]: 

287 poly = self.polygon(num, 25, cent, cent) 

288 box = lsst.geom.Box2I(lsst.geom.Point2I(cent - 60, cent - 60), 

289 lsst.geom.Extent2I(115, 115)) 

290 image = poly.createImage(box) 

291 if display: 

292 disp = afwDisplay.Display(frame=i + 1) 

293 disp.mtv(image, title=f"Polygon nside={num}") 

294 for p1, p2 in poly.getEdges(): 

295 disp.line((p1, p2)) 

296 # Some computations of the image area have such large aliasing 

297 # in boost::intersection that the precision required here is 0.025. 

298 self.assertFloatsAlmostEqual( 

299 image.getArray().sum(), poly.calculateArea(), rtol=0.025) 

300 

301 def testTransform(self): 

302 """Test constructor for Polygon involving transforms""" 

303 box = lsst.geom.Box2D(lsst.geom.Point2D(0.0, 0.0), 

304 lsst.geom.Point2D(123.4, 567.8)) 

305 poly1 = afwGeom.Polygon(box) 

306 scale = 1.5 

307 shift = lsst.geom.Extent2D(3.0, 4.0) 

308 affineTransform = lsst.geom.AffineTransform.makeTranslation(shift) * \ 

309 lsst.geom.AffineTransform.makeScaling(scale) 

310 transform22 = afwGeom.makeTransform(affineTransform) 

311 transformedVertices = transform22.applyForward(box.getCorners()) 

312 expect = afwGeom.Polygon(transformedVertices) 

313 poly1 = afwGeom.Polygon(box, affineTransform) 

314 poly2 = afwGeom.Polygon(box, transform22) 

315 self.assertEqual(poly1, expect) 

316 self.assertEqual(poly2, expect) 

317 

318 def testIteration(self): 

319 """Test iteration over polygon""" 

320 for num in range(3, 30): 

321 poly = self.polygon(num) 

322 self.assertEqual(len(poly), num) 

323 points1 = [p for p in poly] 

324 points2 = poly.getVertices() 

325 self.assertEqual(len(points1), num + 1) 

326 self.assertEqual(len(points2), num + 1) 

327 self.assertEqual(points2[0], points2[-1]) # Closed representation 

328 for p1, p2 in zip(points1, points2): 

329 self.assertEqual(p1, p2) 

330 for i, p1 in enumerate(points1): 

331 self.assertEqual(poly[i], p1) 

332 

333 def testSubSample(self): 

334 """Test Polygon.subSample""" 

335 if display: 

336 import matplotlib.pyplot as plt 

337 for num in range(3, 30): 

338 poly = self.polygon(num) 

339 sub = poly.subSample(2) 

340 

341 if display: 

342 axes = poly.plot(c='b') 

343 axes.set_aspect("equal") 

344 axes.set_title(f"Polygon nside={num}") 

345 sub.plot(axes, c='r') 

346 if not doPause: 

347 try: 

348 plt.pause(2) 

349 plt.close() 

350 except Exception: 

351 print(f"{str(self)}: plt.pause() failed. Please close plots when done.") 

352 plt.show() 

353 else: 

354 print(f"{str(self)}: Please close plots when done.") 

355 plt.show() 

356 

357 self.assertEqual(len(sub), 2*num) 

358 self.assertAlmostEqual(sub.calculateArea(), poly.calculateArea()) 

359 self.assertAlmostEqual( 

360 sub.calculatePerimeter(), poly.calculatePerimeter()) 

361 polyCenter = poly.calculateCenter() 

362 subCenter = sub.calculateCenter() 

363 self.assertAlmostEqual(polyCenter[0], subCenter[0]) 

364 self.assertAlmostEqual(polyCenter[1], subCenter[1]) 

365 for i in range(num): 

366 self.assertEqual(sub[2*i], poly[i]) 

367 

368 sub = poly.subSample(0.1) 

369 self.assertAlmostEqual(sub.calculateArea(), poly.calculateArea()) 

370 self.assertAlmostEqual( 

371 sub.calculatePerimeter(), poly.calculatePerimeter()) 

372 polyCenter = poly.calculateCenter() 

373 subCenter = sub.calculateCenter() 

374 self.assertAlmostEqual(polyCenter[0], subCenter[0]) 

375 self.assertAlmostEqual(polyCenter[1], subCenter[1]) 

376 

377 def testTransform2(self): 

378 if display: 

379 import matplotlib.pyplot as plt 

380 scale = 2.0 

381 shift = lsst.geom.Extent2D(3.0, 4.0) 

382 affineTransform = lsst.geom.AffineTransform.makeTranslation(shift) * \ 

383 lsst.geom.AffineTransform.makeScaling(scale) 

384 transform22 = afwGeom.makeTransform(affineTransform) 

385 for num in range(3, 30): 

386 small = self.polygon(num, 1.0, 0.0, 0.0) 

387 large1 = small.transform(affineTransform) 

388 large2 = small.transform(transform22) 

389 expect = self.polygon(num, scale, shift[0], shift[1]) 

390 self.assertEqual(large1, expect) 

391 self.assertEqual(large2, expect) 

392 

393 if display: 

394 axes = small.plot(c='k') 

395 axes.set_aspect("equal") 

396 axes.set_title(f"AffineTransform: Polygon nside={num}") 

397 large1.plot(axes, c='b') 

398 if not doPause: 

399 try: 

400 plt.pause(2) 

401 plt.close() 

402 except Exception: 

403 print(f"{str(self)}: plt.pause() failed. Please close plots when done.") 

404 plt.show() 

405 else: 

406 print(f"{str(self)}: Please close plots when done.") 

407 plt.show() 

408 

409 def testReadWrite(self): 

410 """Test that polygons can be read and written to fits files""" 

411 for num in range(3, 30): 

412 poly = self.polygon(num) 

413 with lsst.utils.tests.getTempFilePath(".fits") as filename: 

414 poly.writeFits(filename) 

415 poly2 = afwGeom.Polygon.readFits(filename) 

416 self.assertEqual(poly, poly2) 

417 

418 

419class TestMemory(lsst.utils.tests.MemoryTestCase): 

420 pass 

421 

422 

423def setup_module(module): 

424 lsst.utils.tests.init() 

425 

426 

427if __name__ == "__main__": 427 ↛ 428line 427 didn't jump to line 428, because the condition on line 427 was never true

428 lsst.utils.tests.init() 

429 unittest.main()