Coverage for tests/test_spherePoint.py: 9%

512 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-15 09:50 +0000

1# 

2# Developed for the LSST Data Management System. 

3# This product includes software developed by the LSST Project 

4# (https://www.lsst.org). 

5# See the COPYRIGHT file at the top-level directory of this distribution 

6# for details of code ownership. 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the GNU General Public License 

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

20# 

21 

22# -*- python -*- 

23""" 

24Unit tests for SpherePoint 

25 

26Run with: 

27 python testSpherePoint.py 

28or 

29 python 

30 >>> import testSpherePoint 

31 >>> testSpherePoint.run() 

32""" 

33 

34import copy 

35import math 

36import re 

37import unittest 

38 

39import numpy as np 

40from numpy.testing import assert_allclose 

41 

42import lsst.utils.tests 

43import lsst.sphgeom 

44import lsst.geom as geom 

45import lsst.pex.exceptions as pexEx 

46 

47from lsst.geom import degrees, radians, SpherePoint 

48from numpy import nan, inf 

49 

50 

51class SpherePointTestSuite(lsst.utils.tests.TestCase): 

52 

53 def setUp(self): 

54 self._dataset = SpherePointTestSuite.positions() 

55 self._poleLatitudes = [ 

56 geom.HALFPI*geom.radians, 

57 6.0*geom.hours, 

58 90.0*geom.degrees, 

59 5400.0*geom.arcminutes, 

60 324000.0*geom.arcseconds, 

61 ] 

62 

63 @property 

64 def pointSet(self): 

65 for lon, lat in self._dataset: 

66 for point in ( 

67 SpherePoint(lon, lat), 

68 SpherePoint(lon.asDegrees(), lat.asDegrees(), degrees), 

69 SpherePoint(lon.asRadians(), lat.asRadians(), radians), 

70 ): 

71 yield point 

72 

73 @staticmethod 

74 def positions(): 

75 """Provide valid coordinates for nominal-case testing. 

76 

77 Returns 

78 ------- 

79 positions : `iterable` 

80 An iterable of pairs of Angles, each representing the 

81 longitude and latitude (in that order) of a test point. 

82 """ 

83 nValidPoints = 100 

84 rng = np.random.RandomState(42) 

85 ra = rng.uniform(0.0, 360.0, nValidPoints) 

86 dec = rng.uniform(-90.0, 90.0, nValidPoints) 

87 

88 points = list(zip(ra*degrees, dec*degrees)) 

89 # Ensure corner cases are tested. 

90 points += [ 

91 (0.0*degrees, 0.0*degrees), 

92 (geom.PI*radians, -6.0*degrees), 

93 (42.0*degrees, -90.0*degrees), 

94 (172.0*degrees, geom.HALFPI*radians), 

95 (360.0*degrees, 45.0*degrees), 

96 (-278.0*degrees, -42.0*degrees), 

97 (765.0*degrees, 0.25*geom.PI*radians), 

98 (180.0*degrees, nan*radians), 

99 (inf*degrees, 45.0*degrees), 

100 (nan*degrees, -8.3*degrees), 

101 ] 

102 return points 

103 

104 def testLonLatConstructorErrors(self): 

105 """Test if the longitude, latitude constructors handle invalid input 

106 """ 

107 # Latitude should be checked for out-of-range. 

108 for lat in self._poleLatitudes: 

109 with self.assertRaises(pexEx.InvalidParameterError): 

110 SpherePoint(0.0*degrees, self.nextUp(lat)) 

111 with self.assertRaises(pexEx.InvalidParameterError): 

112 SpherePoint(0.0, self.nextUp(lat).asDegrees(), degrees) 

113 with self.assertRaises(pexEx.InvalidParameterError): 

114 SpherePoint(0.0*degrees, self.nextDown(-lat)) 

115 with self.assertRaises(pexEx.InvalidParameterError): 

116 SpherePoint(0.0, self.nextDown(-lat).asDegrees(), degrees) 

117 

118 # Longitude should not be checked for out of range. 

119 SpherePoint(360.0*degrees, 45.0*degrees) 

120 SpherePoint(360.0, 45.0, degrees) 

121 SpherePoint(-42.0*degrees, 45.0*degrees) 

122 SpherePoint(-42.0, 45.0, degrees) 

123 SpherePoint(391.0*degrees, 45.0*degrees) 

124 SpherePoint(391.0, 45.0, degrees) 

125 

126 # Infinite latitude is not allowed. 

127 with self.assertRaises(pexEx.InvalidParameterError): 

128 SpherePoint(-42.0*degrees, inf*degrees) 

129 with self.assertRaises(pexEx.InvalidParameterError): 

130 SpherePoint(-42.0, inf, degrees) 

131 with self.assertRaises(pexEx.InvalidParameterError): 

132 SpherePoint(-42.0*degrees, -inf*degrees) 

133 with self.assertRaises(pexEx.InvalidParameterError): 

134 SpherePoint(-42.0, -inf, degrees) 

135 

136 def testVector3dConstructor(self): 

137 # test poles 

138 for z in (-11.3, -1.1, 0.1, 2.5): # arbitrary non-zero values 

139 sp = SpherePoint(lsst.sphgeom.Vector3d(0.0, 0.0, z)) 

140 self.assertTrue(sp.atPole()) 

141 self.assertEqual(sp.getLongitude().asRadians(), 0.0) 

142 if z < 0: 

143 self.assertAnglesAlmostEqual(sp.getLatitude(), -90 * degrees) 

144 else: 

145 self.assertAnglesAlmostEqual(sp.getLatitude(), 90 * degrees) 

146 

147 spx = SpherePoint(lsst.sphgeom.Vector3d(11.1, 0.0, 0.0)) 

148 self.assertAnglesAlmostEqual(spx.getLongitude(), 0.0 * degrees) 

149 self.assertAnglesAlmostEqual(spx.getLatitude(), 0.0 * degrees) 

150 

151 spy = SpherePoint(lsst.sphgeom.Vector3d(0.0, 234234.5, 0.0)) 

152 self.assertAnglesAlmostEqual(spy.getLongitude(), 90.0 * degrees) 

153 self.assertAnglesAlmostEqual(spy.getLatitude(), 0.0 * degrees) 

154 

155 spxy = SpherePoint(lsst.sphgeom.Vector3d(7.5, -7.5, 0.0)) 

156 self.assertAnglesAlmostEqual(spxy.getLongitude(), -45.0 * degrees) 

157 self.assertAnglesAlmostEqual(spxy.getLatitude(), 0.0 * degrees) 

158 

159 spxz = SpherePoint(lsst.sphgeom.Vector3d(100.0, 0.0, -100.0)) 

160 self.assertAnglesAlmostEqual(spxz.getLongitude(), 0.0 * degrees) 

161 self.assertAnglesAlmostEqual(spxz.getLatitude(), -45.0 * degrees) 

162 

163 # Only one singularity: a vector of all zeros 

164 with self.assertRaises(pexEx.InvalidParameterError): 

165 SpherePoint(lsst.sphgeom.Vector3d(0.0, 0.0, 0.0)) 

166 

167 def testDefaultConstructor(self): 

168 sp = SpherePoint() 

169 self.assertTrue(math.isnan(sp.getLongitude())) 

170 self.assertTrue(math.isnan(sp.getLatitude())) 

171 self.assertFalse(sp.isFinite()) 

172 

173 def testCopyConstructor(self): 

174 sp = SpherePoint(-42.0*degrees, 45.0*degrees) 

175 spcopy = SpherePoint(sp) 

176 self.assertEqual(sp, spcopy) 

177 

178 def testInitNArgFail(self): 

179 """Test incorrect calls to the SpherePoint constructor 

180 """ 

181 with self.assertRaises(TypeError): 

182 SpherePoint("Rotund", "Bovine") 

183 with self.assertRaises(TypeError): 

184 SpherePoint(42) 

185 with self.assertRaises(TypeError): 

186 SpherePoint("ICRS", 34.0, -56.0) 

187 with self.assertRaises(TypeError): 

188 SpherePoint(34.0, -56.0) # missing units 

189 

190 def testGetLongitudeValue(self): 

191 """Test if getLongitude() and getRa() return the expected value. 

192 """ 

193 for lon, lat in self._dataset: 

194 for point in ( 

195 SpherePoint(lon, lat), 

196 SpherePoint(lon.asDegrees(), lat.asDegrees(), degrees), 

197 SpherePoint(lon.asRadians(), lat.asRadians(), radians), 

198 ): 

199 self.assertIsInstance(point.getLongitude(), geom.Angle) 

200 # Behavior for non-finite points is undefined; depends on internal 

201 # data representation 

202 if point.isFinite(): 

203 self.assertGreaterEqual(point.getLongitude().asDegrees(), 0.0) 

204 self.assertLess(point.getLongitude().asDegrees(), 360.0) 

205 

206 # Longitude not guaranteed to match input at pole 

207 if not point.atPole(): 

208 # assertAnglesAlmostEqual handles angle wrapping internally 

209 self.assertAnglesAlmostEqual(lon, point.getLongitude()) 

210 self.assertAnglesAlmostEqual(lon, point.getRa()) 

211 

212 # Vector construction should return valid longitude even in edge cases. 

213 point = SpherePoint(lsst.sphgeom.Vector3d(0.0, 0.0, -1.0)) 

214 self.assertGreaterEqual(point.getLongitude().asDegrees(), 0.0) 

215 self.assertLess(point.getLongitude().asDegrees(), 360.0) 

216 

217 def testGetPosition(self): 

218 for sp in self.pointSet: 

219 for units in (degrees, geom.hours, radians): 

220 point = sp.getPosition(units) 

221 expectedPoint = [val.asAngularUnits(units) for val in sp] 

222 assert_allclose(point, expectedPoint, atol=1e-15) 

223 

224 def testTicket1394(self): 

225 """Regression test for Ticket 1761. 

226 

227 Checks that negative longitudes within epsilon of lon=0 lead 

228 are correctly bounded and rounded. 

229 """ 

230 # The problem was that the coordinate is less than epsilon 

231 # close to RA == 0 and bounds checking was getting a 

232 # negative RA. 

233 point = SpherePoint(lsst.sphgeom.Vector3d( 

234 0.6070619982, -1.264309928e-16, 0.7946544723)) 

235 

236 self.assertEqual(point[0].asDegrees(), 0.0) 

237 

238 def testGetLatitudeValue(self): 

239 """Test if getLatitude() and getDec() return the expected value. 

240 """ 

241 for lon, lat in self._dataset: 

242 for point in ( 

243 SpherePoint(lon, lat), 

244 SpherePoint(lon.asDegrees(), lat.asDegrees(), degrees), 

245 SpherePoint(lon.asRadians(), lat.asRadians(), radians), 

246 ): 

247 self.assertIsInstance(point.getLatitude(), geom.Angle) 

248 # Behavior for non-finite points is undefined; depends on internal 

249 # data representation 

250 if point.isFinite(): 

251 self.assertGreaterEqual(point.getLatitude().asDegrees(), -90.0) 

252 self.assertLessEqual(point.getLatitude().asDegrees(), 90.0) 

253 self.assertAnglesAlmostEqual(lat, point.getLatitude()) 

254 self.assertAnglesAlmostEqual(lat, point.getDec()) 

255 

256 def testGetVectorValue(self): 

257 """Test if getVector() returns the expected value. 

258 

259 The test includes conformance to vector-angle conventions. 

260 """ 

261 for lon, lat, vector in [ 

262 (0.0*degrees, 0.0*degrees, lsst.sphgeom.Vector3d(1.0, 0.0, 0.0)), 

263 (90.0*degrees, 0.0*degrees, lsst.sphgeom.Vector3d(0.0, 1.0, 0.0)), 

264 (0.0*degrees, 90.0*degrees, lsst.sphgeom.Vector3d(0.0, 0.0, 1.0)), 

265 ]: 

266 for point in ( 

267 SpherePoint(lon, lat), 

268 SpherePoint(lon.asDegrees(), lat.asDegrees(), degrees), 

269 SpherePoint(lon.asRadians(), lat.asRadians(), radians), 

270 ): 

271 newVector = point.getVector() 

272 self.assertIsInstance(newVector, lsst.sphgeom.UnitVector3d) 

273 for oldElement, newElement in zip(vector, newVector): 

274 self.assertAlmostEqual(oldElement, newElement) 

275 

276 # Convert back to spherical. 

277 newLon, newLat = SpherePoint(newVector) 

278 self.assertAlmostEqual(newLon.asDegrees(), lon.asDegrees()) 

279 self.assertAlmostEqual(newLat.asDegrees(), lat.asDegrees()) 

280 

281 # Try some un-normalized ones, too. 

282 pointList = [ 

283 ((0.0, 0.0), lsst.sphgeom.Vector3d(1.3, 0.0, 0.0)), 

284 ((90.0, 0.0), lsst.sphgeom.Vector3d(0.0, 1.2, 0.0)), 

285 ((0.0, 90.0), lsst.sphgeom.Vector3d(0.0, 0.0, 2.3)), 

286 ((0.0, 0.0), lsst.sphgeom.Vector3d(0.5, 0.0, 0.0)), 

287 ((90.0, 0.0), lsst.sphgeom.Vector3d(0.0, 0.7, 0.0)), 

288 ((0.0, 90.0), lsst.sphgeom.Vector3d(0.0, 0.0, 0.9)), 

289 ] 

290 

291 for lonLat, vector in pointList: 

292 # Only convert from vector to spherical. 

293 point = SpherePoint(vector) 

294 newLon, newLat = point 

295 self.assertAlmostEqual(lonLat[0], newLon.asDegrees()) 

296 self.assertAlmostEqual(lonLat[1], newLat.asDegrees()) 

297 vector = lsst.sphgeom.Vector3d(point.getVector()) 

298 self.assertAlmostEqual(1.0, vector.getSquaredNorm()) 

299 

300 # Ill-defined points should be all NaN after normalization 

301 cleanValues = [0.5, -0.3, 0.2] 

302 badValues = [nan, inf, -inf] 

303 for i in range(3): 

304 for badValue in badValues: 

305 values = cleanValues[:] 

306 values[i] = badValue 

307 nonFiniteVector = lsst.sphgeom.Vector3d(*values) 

308 for element in SpherePoint(nonFiniteVector).getVector(): 

309 self.assertTrue(math.isnan(element)) 

310 

311 def testToUnitXZY(self): 

312 """Test that the numpy-vectorized transformation from (lat, lon) to 

313 (x, y, z) matches SpherePoint.getVector(). 

314 """ 

315 for units in (degrees, radians): 

316 scale = float(180.0*degrees)/float(1.0*units) 

317 lon = scale*np.random.rand(5, 3) 

318 lat = scale*(np.random.rand(5, 3) - 0.5) 

319 x, y, z = SpherePoint.toUnitXYZ(longitude=lon, latitude=lat, units=units) 

320 for i in range(lon.shape[0]): 

321 for j in range(lon.shape[1]): 

322 s = SpherePoint(lon[i, j], lat[i, j], units) 

323 u1 = s.getVector() 

324 u2 = lsst.sphgeom.UnitVector3d(x=x[i, j], y=y[i, j], z=z[i, j]) 

325 self.assertFloatsAlmostEqual(np.array(u1, dtype=float), np.array(u2, dtype=float)) 

326 

327 def testTicket1761(self): 

328 """Regression test for Ticket 1761. 

329 

330 Checks for math errors caused by unnormalized vectors. 

331 """ 

332 refPoint = SpherePoint(lsst.sphgeom.Vector3d(0, 1, 0)) 

333 

334 point1 = SpherePoint(lsst.sphgeom.Vector3d(0.1, 0.1, 0.1)) 

335 point2 = SpherePoint(lsst.sphgeom.Vector3d(0.6, 0.6, 0.6)) 

336 sep1 = refPoint.separation(point1) 

337 sep2 = refPoint.separation(point2) 

338 sepTrue = 54.735610317245339*degrees 

339 

340 self.assertAnglesAlmostEqual(sepTrue, sep1) 

341 self.assertAnglesAlmostEqual(sepTrue, sep2) 

342 

343 def testAtPoleValue(self): 

344 """Test if atPole() returns the expected value. 

345 """ 

346 poleList = \ 

347 [SpherePoint(42.0*degrees, lat) for lat in self._poleLatitudes] + \ 

348 [SpherePoint(42.0, lat.asDegrees(), degrees) for lat in self._poleLatitudes] + \ 

349 [SpherePoint(42.0*degrees, -lat) for lat in self._poleLatitudes] + \ 

350 [SpherePoint(42.0, -lat.asDegrees(), degrees) for lat in self._poleLatitudes] + \ 

351 [ 

352 SpherePoint(lsst.sphgeom.Vector3d(0.0, 0.0, 1.0)), 

353 SpherePoint(lsst.sphgeom.Vector3d(0.0, 0.0, -1.0)), 

354 ] 

355 nonPoleList = \ 

356 [SpherePoint(42.0*degrees, self.nextDown(lat)) for lat in self._poleLatitudes] + \ 

357 [SpherePoint(42.0, self.nextDown(lat).asDegrees(), degrees) for lat in self._poleLatitudes] + \ 

358 [SpherePoint(42.0*degrees, self.nextUp(-lat)) for lat in self._poleLatitudes] + \ 

359 [SpherePoint(42.0, self.nextUp(-lat).asDegrees(), degrees) 

360 for lat in self._poleLatitudes] + \ 

361 [ 

362 SpherePoint(lsst.sphgeom.Vector3d(9.9e-7, 0.0, 1.0)), 

363 SpherePoint(lsst.sphgeom.Vector3d(9.9e-7, 0.0, -1.0)), 

364 SpherePoint(0.0*degrees, nan*degrees), 

365 ] 

366 

367 for pole in poleList: 

368 self.assertIsInstance(pole.atPole(), bool) 

369 self.assertTrue(pole.atPole()) 

370 

371 for nonPole in nonPoleList: 

372 self.assertIsInstance(nonPole.atPole(), bool) 

373 self.assertFalse(nonPole.atPole()) 

374 

375 def testIsFiniteValue(self): 

376 """Test if isFinite() returns the expected value. 

377 """ 

378 finiteList = [ 

379 SpherePoint(0.0*degrees, -90.0*degrees), 

380 SpherePoint(0.0, -90.0, degrees), 

381 SpherePoint(lsst.sphgeom.Vector3d(0.1, 0.2, 0.3)), 

382 ] 

383 nonFiniteList = [ 

384 SpherePoint(0.0*degrees, nan*degrees), 

385 SpherePoint(0.0, nan, degrees), 

386 SpherePoint(nan*degrees, 0.0*degrees), 

387 SpherePoint(nan, 0.0, degrees), 

388 SpherePoint(inf*degrees, 0.0*degrees), 

389 SpherePoint(inf, 0.0, degrees), 

390 SpherePoint(-inf*degrees, 0.0*degrees), 

391 SpherePoint(-inf, 0.0, degrees), 

392 SpherePoint(lsst.sphgeom.Vector3d(nan, 0.2, 0.3)), 

393 SpherePoint(lsst.sphgeom.Vector3d(0.1, inf, 0.3)), 

394 SpherePoint(lsst.sphgeom.Vector3d(0.1, 0.2, -inf)), 

395 ] 

396 

397 for finite in finiteList: 

398 self.assertIsInstance(finite.isFinite(), bool) 

399 self.assertTrue(finite.isFinite()) 

400 

401 for nonFinite in nonFiniteList: 

402 self.assertIsInstance(nonFinite.isFinite(), bool) 

403 self.assertFalse(nonFinite.isFinite()) 

404 

405 def testGetItemError(self): 

406 """Test if indexing correctly handles invalid input. 

407 """ 

408 point = SpherePoint(lsst.sphgeom.Vector3d(1.0, 1.0, 1.0)) 

409 

410 with self.assertRaises(IndexError): 

411 point[2] 

412 with self.assertRaises(IndexError): 

413 point[-3] 

414 

415 def testGetItemValue(self): 

416 """Test if indexing returns the expected value. 

417 """ 

418 for point in self.pointSet: 

419 self.assertIsInstance(point[-2], geom.Angle) 

420 self.assertIsInstance(point[-1], geom.Angle) 

421 self.assertIsInstance(point[0], geom.Angle) 

422 self.assertIsInstance(point[1], geom.Angle) 

423 

424 if not math.isnan(point.getLongitude().asRadians()): 

425 self.assertEqual(point.getLongitude(), point[-2]) 

426 self.assertEqual(point.getLongitude(), point[0]) 

427 else: 

428 self.assertTrue(math.isnan(point[-2].asRadians())) 

429 self.assertTrue(math.isnan(point[0].asRadians())) 

430 if not math.isnan(point.getLatitude().asRadians()): 

431 self.assertEqual(point.getLatitude(), point[-1]) 

432 self.assertEqual(point.getLatitude(), point[1]) 

433 else: 

434 self.assertTrue(math.isnan(point[-1].asRadians())) 

435 self.assertTrue(math.isnan(point[1].asRadians())) 

436 

437 def testEquality(self): 

438 """Test if tests for equality treat SpherePoints as values. 

439 """ 

440 # (In)equality is determined by value, not identity. 

441 # See DM-2347, DM-2465. These asserts are testing the 

442 # functionality of `==` and `!=` and should not be changed. 

443 for lon1, lat1 in self._dataset: 

444 point1 = SpherePoint(lon1, lat1) 

445 self.assertIsInstance(point1 == point1, bool) 

446 self.assertIsInstance(point1 != point1, bool) 

447 if point1.isFinite(): 

448 self.assertTrue(point1 == point1) 

449 self.assertFalse(point1 != point1) 

450 

451 pointCopy = copy.deepcopy(point1) 

452 self.assertIsNot(pointCopy, point1) 

453 self.assertEqual(pointCopy, point1) 

454 self.assertEqual(point1, pointCopy) 

455 self.assertFalse(pointCopy != point1) 

456 self.assertFalse(point1 != pointCopy) 

457 else: 

458 self.assertFalse(point1 == point1) 

459 self.assertTrue(point1 != point1) 

460 

461 for lon2, lat2 in self._dataset: 

462 point2 = SpherePoint(lon2, lat2) 

463 if lon1 == lon2 and lat1 == lat2 and point1.isFinite() and point2.isFinite(): 

464 # note: the isFinite checks are needed because if longitude is infinite 

465 # then the resulting SpherePoint has nan as its longitude, due to wrapping 

466 self.assertFalse(point2 != point1) 

467 self.assertFalse(point1 != point2) 

468 self.assertTrue(point2 == point1) 

469 self.assertTrue(point1 == point2) 

470 else: 

471 self.assertTrue(point2 != point1) 

472 self.assertTrue(point1 != point2) 

473 self.assertFalse(point2 == point1) 

474 self.assertFalse(point1 == point2) 

475 

476 # Test for transitivity (may be assumed by algorithms). 

477 for delta in [10.0**(0.1*x) for x in range(-150, -49, 5)]: 

478 self.checkTransitive(delta*radians) 

479 

480 def checkTransitive(self, delta): 

481 """Test if equality is transitive even for close points. 

482 

483 This test prevents misuse of approximate floating-point 

484 equality -- if `__eq__` is implemented using AFP, then this 

485 test will fail for some value of `delta`. Testing multiple 

486 values is recommended. 

487 

488 Parameters 

489 ---------- 

490 delta : `number` 

491 The separation, in degrees, at which point equality may 

492 become intransitive. 

493 """ 

494 for lon, lat in self._dataset: 

495 point1 = SpherePoint(lon - delta, lat) 

496 point2 = SpherePoint(lon, lat) 

497 point3 = SpherePoint(lon + delta, lat) 

498 

499 self.assertTrue(point1 != point2 

500 or point2 != point3 

501 or point1 == point3) 

502 self.assertTrue(point3 != point1 

503 or point1 != point2 

504 or point3 == point2) 

505 self.assertTrue(point2 == point3 

506 or point3 != point1 

507 or point2 == point1) 

508 

509 def testBearingToValueOnEquator(self): 

510 """Test if bearingTo() returns the expected value from a point on the equator 

511 """ 

512 lon0 = 90.0 

513 lat0 = 0.0 # These tests only work from the equator. 

514 arcLen = 10.0 

515 

516 trials = [ 

517 # Along celestial equator 

518 dict(lon=lon0, lat=lat0, bearing=0.0, 

519 lonEnd=lon0+arcLen, latEnd=lat0), 

520 # Along a meridian 

521 dict(lon=lon0, lat=lat0, bearing=90.0, 

522 lonEnd=lon0, latEnd=lat0+arcLen), 

523 # 180 degree arc (should go to antipodal point) 

524 dict(lon=lon0, lat=lat0, bearing=45.0, 

525 lonEnd=lon0+180.0, latEnd=-lat0), 

526 # 

527 dict(lon=lon0, lat=lat0, bearing=45.0, 

528 lonEnd=lon0+90.0, latEnd=lat0 + 45.0), 

529 dict(lon=lon0, lat=lat0, bearing=225.0, 

530 lonEnd=lon0-90.0, latEnd=lat0 - 45.0), 

531 dict(lon=lon0, lat=np.nextafter(-90.0, inf), 

532 bearing=90.0, lonEnd=lon0, latEnd=0.0), 

533 dict(lon=lon0, lat=np.nextafter(-90.0, inf), 

534 bearing=0.0, lonEnd=lon0 + 90.0, latEnd=0.0), 

535 # Argument at a pole should work 

536 dict(lon=lon0, lat=lat0, bearing=270.0, lonEnd=lon0, latEnd=-90.0), 

537 # Support for non-finite values 

538 dict(lon=lon0, lat=nan, bearing=nan, lonEnd=lon0, latEnd=45.0), 

539 dict(lon=lon0, lat=lat0, bearing=nan, lonEnd=nan, latEnd=90.0), 

540 dict(lon=inf, lat=lat0, bearing=nan, lonEnd=lon0, latEnd=42.0), 

541 dict(lon=lon0, lat=lat0, bearing=nan, lonEnd=-inf, latEnd=42.0), 

542 ] 

543 

544 for trial in trials: 

545 origin = SpherePoint(trial['lon']*degrees, trial['lat']*degrees) 

546 end = SpherePoint(trial['lonEnd']*degrees, trial['latEnd']*degrees) 

547 bearing = origin.bearingTo(end) 

548 

549 self.assertIsInstance(bearing, geom.Angle) 

550 if origin.isFinite() and end.isFinite(): 

551 self.assertGreaterEqual(bearing.asDegrees(), 0.0) 

552 self.assertLess(bearing.asDegrees(), 360.0) 

553 if origin.separation(end).asDegrees() != 180.0: 

554 if not math.isnan(trial['bearing']): 

555 self.assertAlmostEqual( 

556 trial['bearing'], bearing.asDegrees(), 12) 

557 else: 

558 self.assertTrue(math.isnan(bearing.asRadians())) 

559 

560 def testBearingToValueSameLongitude(self): 

561 """Test that bearingTo() returns +/- 90 for two points on the same longitude 

562 """ 

563 for longDeg in (0, 55, 270): 

564 for lat0Deg in (-90, -5, 0, 44, 90): 

565 sp0 = SpherePoint(longDeg, lat0Deg, degrees) 

566 for lat1Deg in (-90, -41, 1, 41, 90): 

567 if lat0Deg == lat1Deg: 

568 continue 

569 sp1 = SpherePoint(longDeg, lat1Deg, degrees) 

570 if sp0.atPole() and sp1.atPole(): 

571 # the points are at opposite poles; any bearing may be returned 

572 continue 

573 bearing = sp0.bearingTo(sp1) 

574 if lat1Deg > lat0Deg: 

575 self.assertAnglesAlmostEqual(bearing, 90 * degrees) 

576 else: 

577 self.assertAnglesAlmostEqual(bearing, -90 * degrees) 

578 

579 def testBearingToFromPole(self): 

580 """Test if bearingTo() returns the expected value from a point at a pole 

581 """ 

582 for long0Deg in (0, 55, 270): 

583 for atSouthPole in (False, True): 

584 lat0Deg = -90 if atSouthPole else 90 

585 sp0 = SpherePoint(long0Deg, lat0Deg, degrees) 

586 for long1Deg in (0, 55, 270): 

587 for lat1Deg in (-89, 0, 89): 

588 sp1 = SpherePoint(long1Deg, lat1Deg, degrees) 

589 desiredBearing = ((long1Deg - long0Deg) - 90) * degrees 

590 if atSouthPole: 

591 desiredBearing *= -1 

592 measuredBearing = sp0.bearingTo(sp1) 

593 self.assertAnglesAlmostEqual(desiredBearing, measuredBearing) 

594 

595 def testBearingToValueSingular(self): 

596 """White-box test: bearingTo() may be unstable if points are near opposite poles. 

597 

598 This test is motivated by an error analysis of the `bearingTo` 

599 implementation. It may become irrelevant if the implementation 

600 changes. 

601 """ 

602 southPole = SpherePoint(0.0*degrees, self.nextUp(-90.0*degrees)) 

603 northPoleSame = SpherePoint(0.0*degrees, self.nextDown(90.0*degrees)) 

604 # Don't let it be on exactly the opposite side. 

605 northPoleOpposite = SpherePoint( 

606 180.0*degrees, self.nextDown(northPoleSame.getLatitude())) 

607 

608 self.assertAnglesAlmostEqual(southPole.bearingTo(northPoleSame), 

609 geom.HALFPI*geom.radians) 

610 self.assertAnglesAlmostEqual(southPole.bearingTo(northPoleOpposite), 

611 (geom.PI + geom.HALFPI)*geom.radians) 

612 

613 def testSeparationValueGeneric(self): 

614 """Test if separation() returns the correct value. 

615 """ 

616 # This should cover arcs over the meridian, across the pole, etc. 

617 # Do not use sphgeom as an oracle, in case SpherePoint uses it 

618 # internally. 

619 for lon1, lat1 in self._dataset: 

620 point1 = SpherePoint(lon1, lat1) 

621 x1, y1, z1 = SpherePointTestSuite.toVector(lon1, lat1) 

622 for lon2, lat2 in self._dataset: 

623 point2 = SpherePoint(lon2, lat2) 

624 if lon1 != lon2 or lat1 != lat2: 

625 # Numerically unstable at small angles, but that's ok. 

626 x2, y2, z2 = SpherePointTestSuite.toVector(lon2, lat2) 

627 expected = math.acos(x1*x2 + y1*y2 + z1*z2) 

628 else: 

629 expected = 0.0 

630 

631 sep = point1.separation(point2) 

632 self.assertIsInstance(sep, geom.Angle) 

633 if point1.isFinite() and point2.isFinite(): 

634 self.assertGreaterEqual(sep.asDegrees(), 0.0) 

635 self.assertLessEqual(sep.asDegrees(), 180.0) 

636 self.assertAlmostEqual(expected, sep.asRadians()) 

637 self.assertAnglesAlmostEqual( 

638 sep, point2.separation(point1)) 

639 else: 

640 self.assertTrue(math.isnan(sep.asRadians())) 

641 self.assertTrue(math.isnan( 

642 point2.separation(point1).asRadians())) 

643 

644 def testSeparationValueAbsolute(self): 

645 """Test if separation() returns specific values. 

646 """ 

647 # Test from "Meeus, p. 110" (test originally written for coord::Coord; 

648 # don't know exact reference) 

649 spica = SpherePoint(201.2983, -11.1614, degrees) 

650 arcturus = SpherePoint(213.9154, 19.1825, degrees) 

651 

652 # Verify to precision of quoted distance and positions. 

653 self.assertAlmostEqual( 

654 32.7930, spica.separation(arcturus).asDegrees(), 4) 

655 

656 # Verify small angles: along a constant ra, add an arcsec to spica dec. 

657 epsilon = 1.0*geom.arcseconds 

658 spicaPlus = SpherePoint(spica.getLongitude(), 

659 spica.getLatitude() + epsilon) 

660 

661 self.assertAnglesAlmostEqual(epsilon, spicaPlus.separation(spica)) 

662 

663 def testSeparationPoles(self): 

664 """White-box test: all representations of a pole should have the same distance to another point. 

665 """ 

666 southPole1 = SpherePoint(-30.0, -90.0, degrees) 

667 southPole2 = SpherePoint(183.0, -90.0, degrees) 

668 regularPoint = SpherePoint(42.0, 45.0, degrees) 

669 expectedSep = (45.0 + 90.0)*degrees 

670 

671 self.assertAnglesAlmostEqual( 

672 expectedSep, southPole1.separation(regularPoint)) 

673 self.assertAnglesAlmostEqual( 

674 expectedSep, regularPoint.separation(southPole1)) 

675 self.assertAnglesAlmostEqual( 

676 expectedSep, southPole2.separation(regularPoint)) 

677 self.assertAnglesAlmostEqual( 

678 expectedSep, regularPoint.separation(southPole2)) 

679 

680 @staticmethod 

681 def toVector(longitude, latitude): 

682 """Converts a set of spherical coordinates to a 3-vector. 

683 

684 The conversion shall not be performed by any library, to ensure 

685 that the test case does not duplicate the code being tested. 

686 

687 Parameters 

688 ---------- 

689 longitude : `Angle` 

690 The longitude (right ascension, azimuth, etc.) of the 

691 position. 

692 latitude : `Angle` 

693 The latitude (declination, elevation, etc.) of the 

694 position. 

695 

696 Returns 

697 ------- 

698 x, y, z : `number` 

699 Components of the unit vector representation of 

700 `(longitude, latitude)` 

701 """ 

702 alpha = longitude.asRadians() 

703 delta = latitude.asRadians() 

704 if math.isnan(alpha) or math.isinf(alpha) or math.isnan(delta) or math.isinf(delta): 

705 return (nan, nan, nan) 

706 

707 x = math.cos(alpha)*math.cos(delta) 

708 y = math.sin(alpha)*math.cos(delta) 

709 z = math.sin(delta) 

710 return (x, y, z) 

711 

712 def testRotatedValue(self): 

713 """Test if rotated() returns the expected value. 

714 """ 

715 # Try rotating about the equatorial pole (ie. along a parallel). 

716 longitude = 90.0 

717 latitudes = [0.0, 30.0, 60.0] 

718 arcLen = 10.0 

719 pole = SpherePoint(0.0*degrees, 90.0*degrees) 

720 for latitude in latitudes: 

721 point = SpherePoint(longitude*degrees, latitude*degrees) 

722 newPoint = point.rotated(pole, arcLen*degrees) 

723 

724 self.assertIsInstance(newPoint, SpherePoint) 

725 self.assertAlmostEqual( 

726 longitude + arcLen, newPoint.getLongitude().asDegrees()) 

727 self.assertAlmostEqual( 

728 latitude, newPoint.getLatitude().asDegrees()) 

729 

730 # Try with pole = vernal equinox and rotate up the 90 degree meridian. 

731 pole = SpherePoint(0.0*degrees, 0.0*degrees) 

732 for latitude in latitudes: 

733 point = SpherePoint(longitude*degrees, latitude*degrees) 

734 newPoint = point.rotated(pole, arcLen*degrees) 

735 

736 self.assertAlmostEqual( 

737 longitude, newPoint.getLongitude().asDegrees()) 

738 self.assertAlmostEqual( 

739 latitude + arcLen, newPoint.getLatitude().asDegrees()) 

740 

741 # Test accuracy close to coordinate pole 

742 point = SpherePoint(90.0*degrees, np.nextafter(90.0, -inf)*degrees) 

743 newPoint = point.rotated(pole, 90.0*degrees) 

744 self.assertAlmostEqual(270.0, newPoint.getLongitude().asDegrees()) 

745 self.assertAlmostEqual(90.0 - np.nextafter(90.0, -inf), 

746 newPoint.getLatitude().asDegrees()) 

747 

748 # Generic pole; can't predict position, but test for rotation 

749 # invariant. 

750 pole = SpherePoint(283.5*degrees, -23.6*degrees) 

751 for lon, lat in self._dataset: 

752 point = SpherePoint(lon, lat) 

753 dist = point.separation(pole) 

754 newPoint = point.rotated(pole, -32.4*geom.radians) 

755 

756 self.assertNotAlmostEqual(point.getLongitude().asDegrees(), 

757 newPoint.getLongitude().asDegrees()) 

758 self.assertNotAlmostEqual(point.getLatitude().asDegrees(), 

759 newPoint.getLatitude().asDegrees()) 

760 self.assertAnglesAlmostEqual(dist, newPoint.separation(pole)) 

761 

762 # Non-finite values give undefined rotations 

763 for latitude in latitudes: 

764 point = SpherePoint(longitude*degrees, latitude*degrees) 

765 nanPoint = point.rotated(pole, nan*degrees) 

766 infPoint = point.rotated(pole, inf*degrees) 

767 

768 self.assertTrue(math.isnan(nanPoint.getLongitude().asRadians())) 

769 self.assertTrue(math.isnan(nanPoint.getLatitude().asRadians())) 

770 self.assertTrue(math.isnan(infPoint.getLongitude().asRadians())) 

771 self.assertTrue(math.isnan(infPoint.getLatitude().asRadians())) 

772 

773 # Non-finite points rotate into non-finite points 

774 for point in [ 

775 SpherePoint(-inf*degrees, 1.0*radians), 

776 SpherePoint(32.0*degrees, nan*radians), 

777 ]: 

778 newPoint = point.rotated(pole, arcLen*degrees) 

779 self.assertTrue(math.isnan(nanPoint.getLongitude().asRadians())) 

780 self.assertTrue(math.isnan(nanPoint.getLatitude().asRadians())) 

781 self.assertTrue(math.isnan(infPoint.getLongitude().asRadians())) 

782 self.assertTrue(math.isnan(infPoint.getLatitude().asRadians())) 

783 

784 # Rotation around non-finite poles undefined 

785 for latitude in latitudes: 

786 point = SpherePoint(longitude*degrees, latitude*degrees) 

787 for pole in [ 

788 SpherePoint(-inf*degrees, 1.0*radians), 

789 SpherePoint(32.0*degrees, nan*radians), 

790 ]: 

791 newPoint = point.rotated(pole, arcLen*degrees) 

792 self.assertTrue(math.isnan( 

793 nanPoint.getLongitude().asRadians())) 

794 self.assertTrue(math.isnan(nanPoint.getLatitude().asRadians())) 

795 self.assertTrue(math.isnan( 

796 infPoint.getLongitude().asRadians())) 

797 self.assertTrue(math.isnan(infPoint.getLatitude().asRadians())) 

798 

799 def testRotatedAlias(self): 

800 """White-box test: all representations of a pole should rotate into the same point. 

801 """ 

802 longitudes = [0.0, 90.0, 242.0] 

803 latitude = 90.0 

804 arcLen = 10.0 

805 pole = SpherePoint(90.0*degrees, 0.0*degrees) 

806 for longitude in longitudes: 

807 point = SpherePoint(longitude*degrees, latitude*degrees) 

808 newPoint = point.rotated(pole, arcLen*degrees) 

809 

810 self.assertAlmostEqual(0.0, newPoint.getLongitude().asDegrees()) 

811 self.assertAlmostEqual(80.0, newPoint.getLatitude().asDegrees()) 

812 

813 def testOffsetValue(self): 

814 """Test if offset() returns the expected value. 

815 """ 

816 # This should cover arcs over the meridian, across the pole, etc. 

817 for lon1, lat1 in self._dataset: 

818 point1 = SpherePoint(lon1, lat1) 

819 for lon2, lat2 in self._dataset: 

820 if lon1 == lon2 and lat1 == lat2: 

821 continue 

822 point2 = SpherePoint(lon2, lat2) 

823 bearing = point1.bearingTo(point2) 

824 distance = point1.separation(point2) 

825 

826 # offsetting point1 by bearing and distance should produce the same result as point2 

827 newPoint = point1.offset(bearing, distance) 

828 self.assertIsInstance(newPoint, SpherePoint) 

829 self.assertSpherePointsAlmostEqual(point2, newPoint) 

830 if newPoint.atPole(): 

831 self.assertAnglesAlmostEqual(newPoint.getLongitude(), 0*degrees) 

832 

833 # measuring the separation and bearing from point1 to the new point 

834 # should produce the requested separation and bearing 

835 measuredDistance = point1.separation(newPoint) 

836 self.assertAnglesAlmostEqual(measuredDistance, distance) 

837 if abs(measuredDistance.asDegrees() - 180) > 1e-5: 

838 # The two points are not opposite each other on the sphere, 

839 # so the bearing has a well defined value 

840 measuredBearing = point1.bearingTo(newPoint) 

841 self.assertAnglesAlmostEqual(measuredBearing, bearing) 

842 

843 # offset by a negative amount in the opposite direction should produce the same result 

844 newPoint2 = point1.offset(bearing + 180 * degrees, -distance) 

845 self.assertIsInstance(newPoint2, SpherePoint) 

846 # check angular separation (longitude is checked below) 

847 self.assertSpherePointsAlmostEqual(newPoint, newPoint2) 

848 

849 if point1.isFinite() and point2.isFinite(): 

850 if not point2.atPole(): 

851 self.assertAnglesAlmostEqual( 

852 point2.getLongitude(), newPoint.getLongitude()) 

853 self.assertAnglesAlmostEqual( 

854 point2.getLongitude(), newPoint2.getLongitude()) 

855 self.assertAnglesAlmostEqual( 

856 point2.getLatitude(), newPoint.getLatitude()) 

857 self.assertAnglesAlmostEqual( 

858 point2.getLatitude(), newPoint2.getLatitude()) 

859 else: 

860 self.assertTrue(math.isnan( 

861 newPoint.getLongitude().asRadians())) 

862 self.assertTrue(math.isnan( 

863 newPoint2.getLongitude().asRadians())) 

864 self.assertTrue(math.isnan( 

865 newPoint.getLatitude().asRadians())) 

866 self.assertTrue(math.isnan( 

867 newPoint2.getLatitude().asRadians())) 

868 

869 # Test precision near the poles 

870 lon = 123.0*degrees 

871 almostPole = SpherePoint(lon, self.nextDown(90.0*degrees)) 

872 goSouth = almostPole.offset(-90.0*degrees, 90.0*degrees) 

873 self.assertAnglesAlmostEqual(lon, goSouth.getLongitude()) 

874 self.assertAnglesAlmostEqual(0.0*degrees, goSouth.getLatitude()) 

875 goEast = almostPole.offset(0.0*degrees, 90.0*degrees) 

876 self.assertAnglesAlmostEqual(lon + 90.0*degrees, goEast.getLongitude()) 

877 self.assertAnglesAlmostEqual(0.0*degrees, goEast.getLatitude()) 

878 

879 def testOffsetTangentPlane(self): 

880 """Test offsets on a tangent plane (good for small angles)""" 

881 

882 c0 = SpherePoint(0.0, 0.0, geom.degrees) 

883 

884 for dRaDeg in (0.0123, 0.0, -0.0321): 

885 dRa = dRaDeg*geom.degrees 

886 for dDecDeg in (0.0543, 0.0, -0.0987): 

887 dDec = dDecDeg*geom.degrees 

888 c1 = SpherePoint(dRa, dDec) 

889 

890 offset = c0.getTangentPlaneOffset(c1) 

891 

892 # This more-or-less works for small angles because c0 is 0,0 

893 expectedOffset = [ 

894 math.tan(dRa.asRadians())*geom.radians, 

895 math.tan(dDec.asRadians())*geom.radians, 

896 ] 

897 

898 for i in range(2): 

899 self.assertAnglesAlmostEqual(offset[i], expectedOffset[i]) 

900 

901 def testIterResult(self): 

902 """Test if iteration returns the expected values. 

903 """ 

904 for point in self.pointSet: 

905 if not point.isFinite(): 

906 continue 

907 

908 # Test mechanics directly 

909 it = iter(point) 

910 self.assertEqual(point.getLongitude(), next(it)) 

911 self.assertEqual(point.getLatitude(), next(it)) 

912 with self.assertRaises(StopIteration): 

913 next(it) 

914 

915 # Intended use case 

916 lon, lat = point 

917 self.assertEqual(point.getLongitude(), lon) 

918 self.assertEqual(point.getLatitude(), lat) 

919 

920 def testStrValue(self): 

921 """Test if __str__ produces output consistent with its spec. 

922 

923 This is necessarily a loose test, as the behavior of __str__ 

924 is (deliberately) incompletely specified. 

925 """ 

926 for point in self.pointSet: 

927 numbers = re.findall(r'(?:\+|-)?(?:[\d.]+|nan|inf)', str(point)) 

928 self.assertEqual(2, len(numbers), 

929 "String '%s' should have exactly two coordinates." % (point,)) 

930 

931 # Low precision to allow for only a few digits in string. 

932 if not math.isnan(point.getLongitude().asRadians()): 

933 self.assertAlmostEqual( 

934 point.getLongitude().asDegrees(), float(numbers[0]), delta=1e-6) 

935 else: 

936 self.assertRegex(numbers[0], r'-?nan') 

937 if not math.isnan(point.getLatitude().asRadians()): 

938 self.assertAlmostEqual( 

939 point.getLatitude().asDegrees(), float(numbers[1]), delta=1e-6) 

940 # Latitude must be signed 

941 self.assertIn(numbers[1][0], ("+", "-")) 

942 else: 

943 # Some C++ compilers will output NaN with a sign, others won't 

944 self.assertRegex(numbers[1], r'(?:\+|-)?nan') 

945 

946 def testReprValue(self): 

947 """Test if __repr__ is a machine-readable representation. 

948 """ 

949 for point in self.pointSet: 

950 pointRepr = repr(point) 

951 self.assertIn("degrees", pointRepr) 

952 self.assertEqual(2, len(pointRepr.split(","))) 

953 

954 spcopy = eval(pointRepr) 

955 self.assertAnglesAlmostEqual( 

956 point.getLongitude(), spcopy.getLongitude()) 

957 self.assertAnglesAlmostEqual( 

958 point.getLatitude(), spcopy.getLatitude()) 

959 

960 def testAverageSpherePoint(self): 

961 """Test the averageSpherePoint function""" 

962 

963 def checkCircle(center, start, numPts, maxSep=1.0e-9*geom.arcseconds): 

964 """Generate points in a circle; test that average is in the center 

965 """ 

966 coords = [] 

967 deltaAngle = 360*degrees / numPts 

968 for ii in range(numPts): 

969 new = start.rotated(center, ii*deltaAngle) 

970 coords.append(new) 

971 result = geom.averageSpherePoint(coords) 

972 self.assertSpherePointsAlmostEqual(center, result, maxSep=maxSep) 

973 

974 for numPts in (2, 3, 120): 

975 for center, start in ( 

976 # RA=0=360 border 

977 (SpherePoint(0, 0, geom.degrees), SpherePoint(5, 0, geom.degrees)), 

978 # North pole 

979 (SpherePoint(0, 90, geom.degrees), SpherePoint(0, 85, geom.degrees)), 

980 # South pole 

981 (SpherePoint(0, -90, geom.degrees), SpherePoint(0, -85, geom.degrees)), 

982 ): 

983 checkCircle(center=center, start=start, numPts=numPts) 

984 

985 def nextUp(self, angle): 

986 """Returns the smallest angle that is larger than the argument. 

987 """ 

988 return np.nextafter(angle.asRadians(), inf)*radians 

989 

990 def nextDown(self, angle): 

991 """Returns the largest angle that is smaller than the argument. 

992 """ 

993 return np.nextafter(angle.asRadians(), -inf)*radians 

994 

995 

996class MemoryTester(lsst.utils.tests.MemoryTestCase): 

997 pass 

998 

999 

1000def setup_module(module): 

1001 lsst.utils.tests.init() 

1002 

1003 

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

1005 lsst.utils.tests.init() 

1006 unittest.main()