Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 testTicket1761(self): 

312 """Regression test for Ticket 1761. 

313 

314 Checks for math errors caused by unnormalized vectors. 

315 """ 

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

317 

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

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

320 sep1 = refPoint.separation(point1) 

321 sep2 = refPoint.separation(point2) 

322 sepTrue = 54.735610317245339*degrees 

323 

324 self.assertAnglesAlmostEqual(sepTrue, sep1) 

325 self.assertAnglesAlmostEqual(sepTrue, sep2) 

326 

327 def testAtPoleValue(self): 

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

329 """ 

330 poleList = \ 

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

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

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

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

335 [ 

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

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

338 ] 

339 nonPoleList = \ 

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

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

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

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

344 for lat in self._poleLatitudes] + \ 

345 [ 

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

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

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

349 ] 

350 

351 for pole in poleList: 

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

353 self.assertTrue(pole.atPole()) 

354 

355 for nonPole in nonPoleList: 

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

357 self.assertFalse(nonPole.atPole()) 

358 

359 def testIsFiniteValue(self): 

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

361 """ 

362 finiteList = [ 

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

364 SpherePoint(0.0, -90.0, degrees), 

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

366 ] 

367 nonFiniteList = [ 

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

369 SpherePoint(0.0, nan, degrees), 

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

371 SpherePoint(nan, 0.0, degrees), 

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

373 SpherePoint(inf, 0.0, degrees), 

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

375 SpherePoint(-inf, 0.0, degrees), 

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

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

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

379 ] 

380 

381 for finite in finiteList: 

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

383 self.assertTrue(finite.isFinite()) 

384 

385 for nonFinite in nonFiniteList: 

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

387 self.assertFalse(nonFinite.isFinite()) 

388 

389 def testGetItemError(self): 

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

391 """ 

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

393 

394 with self.assertRaises(IndexError): 

395 point[2] 

396 with self.assertRaises(IndexError): 

397 point[-3] 

398 

399 def testGetItemValue(self): 

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

401 """ 

402 for point in self.pointSet: 

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

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

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

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

407 

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

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

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

411 else: 

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

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

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

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

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

417 else: 

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

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

420 

421 def testEquality(self): 

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

423 """ 

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

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

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

427 for lon1, lat1 in self._dataset: 

428 point1 = SpherePoint(lon1, lat1) 

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

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

431 if point1.isFinite(): 

432 self.assertTrue(point1 == point1) 

433 self.assertFalse(point1 != point1) 

434 

435 pointCopy = copy.deepcopy(point1) 

436 self.assertIsNot(pointCopy, point1) 

437 self.assertEqual(pointCopy, point1) 

438 self.assertEqual(point1, pointCopy) 

439 self.assertFalse(pointCopy != point1) 

440 self.assertFalse(point1 != pointCopy) 

441 else: 

442 self.assertFalse(point1 == point1) 

443 self.assertTrue(point1 != point1) 

444 

445 for lon2, lat2 in self._dataset: 

446 point2 = SpherePoint(lon2, lat2) 

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

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

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

450 self.assertFalse(point2 != point1) 

451 self.assertFalse(point1 != point2) 

452 self.assertTrue(point2 == point1) 

453 self.assertTrue(point1 == point2) 

454 else: 

455 self.assertTrue(point2 != point1) 

456 self.assertTrue(point1 != point2) 

457 self.assertFalse(point2 == point1) 

458 self.assertFalse(point1 == point2) 

459 

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

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

462 self.checkTransitive(delta*radians) 

463 

464 def checkTransitive(self, delta): 

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

466 

467 This test prevents misuse of approximate floating-point 

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

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

470 values is recommended. 

471 

472 Parameters 

473 ---------- 

474 delta : `number` 

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

476 become intransitive. 

477 """ 

478 for lon, lat in self._dataset: 

479 point1 = SpherePoint(lon - delta, lat) 

480 point2 = SpherePoint(lon, lat) 

481 point3 = SpherePoint(lon + delta, lat) 

482 

483 self.assertTrue(point1 != point2 

484 or point2 != point3 

485 or point1 == point3) 

486 self.assertTrue(point3 != point1 

487 or point1 != point2 

488 or point3 == point2) 

489 self.assertTrue(point2 == point3 

490 or point3 != point1 

491 or point2 == point1) 

492 

493 def testBearingToValueOnEquator(self): 

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

495 """ 

496 lon0 = 90.0 

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

498 arcLen = 10.0 

499 

500 trials = [ 

501 # Along celestial equator 

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

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

504 # Along a meridian 

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

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

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

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

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

510 # 

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

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

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

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

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

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

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

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

519 # Argument at a pole should work 

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

521 # Support for non-finite values 

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

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

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

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

526 ] 

527 

528 for trial in trials: 

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

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

531 bearing = origin.bearingTo(end) 

532 

533 self.assertIsInstance(bearing, geom.Angle) 

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

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

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

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

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

539 self.assertAlmostEqual( 

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

541 else: 

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

543 

544 def testBearingToValueSameLongitude(self): 

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

546 """ 

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

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

549 sp0 = SpherePoint(longDeg, lat0Deg, degrees) 

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

551 if lat0Deg == lat1Deg: 

552 continue 

553 sp1 = SpherePoint(longDeg, lat1Deg, degrees) 

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

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

556 continue 

557 bearing = sp0.bearingTo(sp1) 

558 if lat1Deg > lat0Deg: 

559 self.assertAnglesAlmostEqual(bearing, 90 * degrees) 

560 else: 

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

562 

563 def testBearingToFromPole(self): 

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

565 """ 

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

567 for atSouthPole in (False, True): 

568 lat0Deg = -90 if atSouthPole else 90 

569 sp0 = SpherePoint(long0Deg, lat0Deg, degrees) 

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

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

572 sp1 = SpherePoint(long1Deg, lat1Deg, degrees) 

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

574 if atSouthPole: 

575 desiredBearing *= -1 

576 measuredBearing = sp0.bearingTo(sp1) 

577 self.assertAnglesAlmostEqual(desiredBearing, measuredBearing) 

578 

579 def testBearingToValueSingular(self): 

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

581 

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

583 implementation. It may become irrelevant if the implementation 

584 changes. 

585 """ 

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

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

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

589 northPoleOpposite = SpherePoint( 

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

591 

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

593 geom.HALFPI*geom.radians) 

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

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

596 

597 def testSeparationValueGeneric(self): 

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

599 """ 

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

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

602 # internally. 

603 for lon1, lat1 in self._dataset: 

604 point1 = SpherePoint(lon1, lat1) 

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

606 for lon2, lat2 in self._dataset: 

607 point2 = SpherePoint(lon2, lat2) 

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

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

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

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

612 else: 

613 expected = 0.0 

614 

615 sep = point1.separation(point2) 

616 self.assertIsInstance(sep, geom.Angle) 

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

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

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

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

621 self.assertAnglesAlmostEqual( 

622 sep, point2.separation(point1)) 

623 else: 

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

625 self.assertTrue(math.isnan( 

626 point2.separation(point1).asRadians())) 

627 

628 def testSeparationValueAbsolute(self): 

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

630 """ 

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

632 # don't know exact reference) 

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

634 arcturus = SpherePoint(213.9154, 19.1825, degrees) 

635 

636 # Verify to precision of quoted distance and positions. 

637 self.assertAlmostEqual( 

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

639 

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

641 epsilon = 1.0*geom.arcseconds 

642 spicaPlus = SpherePoint(spica.getLongitude(), 

643 spica.getLatitude() + epsilon) 

644 

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

646 

647 def testSeparationPoles(self): 

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

649 """ 

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

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

652 regularPoint = SpherePoint(42.0, 45.0, degrees) 

653 expectedSep = (45.0 + 90.0)*degrees 

654 

655 self.assertAnglesAlmostEqual( 

656 expectedSep, southPole1.separation(regularPoint)) 

657 self.assertAnglesAlmostEqual( 

658 expectedSep, regularPoint.separation(southPole1)) 

659 self.assertAnglesAlmostEqual( 

660 expectedSep, southPole2.separation(regularPoint)) 

661 self.assertAnglesAlmostEqual( 

662 expectedSep, regularPoint.separation(southPole2)) 

663 

664 @staticmethod 

665 def toVector(longitude, latitude): 

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

667 

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

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

670 

671 Parameters 

672 ---------- 

673 longitude : `Angle` 

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

675 position. 

676 latitude : `Angle` 

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

678 position. 

679 

680 Returns 

681 ------- 

682 x, y, z : `number` 

683 Components of the unit vector representation of 

684 `(longitude, latitude)` 

685 """ 

686 alpha = longitude.asRadians() 

687 delta = latitude.asRadians() 

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

689 return (nan, nan, nan) 

690 

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

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

693 z = math.sin(delta) 

694 return (x, y, z) 

695 

696 def testRotatedValue(self): 

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

698 """ 

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

700 longitude = 90.0 

701 latitudes = [0.0, 30.0, 60.0] 

702 arcLen = 10.0 

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

704 for latitude in latitudes: 

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

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

707 

708 self.assertIsInstance(newPoint, SpherePoint) 

709 self.assertAlmostEqual( 

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

711 self.assertAlmostEqual( 

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

713 

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

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

716 for latitude in latitudes: 

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

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

719 

720 self.assertAlmostEqual( 

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

722 self.assertAlmostEqual( 

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

724 

725 # Test accuracy close to coordinate pole 

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

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

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

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

730 newPoint.getLatitude().asDegrees()) 

731 

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

733 # invariant. 

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

735 for lon, lat in self._dataset: 

736 point = SpherePoint(lon, lat) 

737 dist = point.separation(pole) 

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

739 

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

741 newPoint.getLongitude().asDegrees()) 

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

743 newPoint.getLatitude().asDegrees()) 

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

745 

746 # Non-finite values give undefined rotations 

747 for latitude in latitudes: 

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

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

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

751 

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

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

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

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

756 

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

758 for point in [ 

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

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

761 ]: 

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

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

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

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

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

767 

768 # Rotation around non-finite poles undefined 

769 for latitude in latitudes: 

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

771 for pole in [ 

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

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

774 ]: 

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

776 self.assertTrue(math.isnan( 

777 nanPoint.getLongitude().asRadians())) 

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

779 self.assertTrue(math.isnan( 

780 infPoint.getLongitude().asRadians())) 

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

782 

783 def testRotatedAlias(self): 

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

785 """ 

786 longitudes = [0.0, 90.0, 242.0] 

787 latitude = 90.0 

788 arcLen = 10.0 

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

790 for longitude in longitudes: 

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

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

793 

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

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

796 

797 def testOffsetValue(self): 

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

799 """ 

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

801 for lon1, lat1 in self._dataset: 

802 point1 = SpherePoint(lon1, lat1) 

803 for lon2, lat2 in self._dataset: 

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

805 continue 

806 point2 = SpherePoint(lon2, lat2) 

807 bearing = point1.bearingTo(point2) 

808 distance = point1.separation(point2) 

809 

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

811 newPoint = point1.offset(bearing, distance) 

812 self.assertIsInstance(newPoint, SpherePoint) 

813 self.assertSpherePointsAlmostEqual(point2, newPoint) 

814 if newPoint.atPole(): 

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

816 

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

818 # should produce the requested separation and bearing 

819 measuredDistance = point1.separation(newPoint) 

820 self.assertAnglesAlmostEqual(measuredDistance, distance) 

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

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

823 # so the bearing has a well defined value 

824 measuredBearing = point1.bearingTo(newPoint) 

825 self.assertAnglesAlmostEqual(measuredBearing, bearing) 

826 

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

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

829 self.assertIsInstance(newPoint2, SpherePoint) 

830 # check angular separation (longitude is checked below) 

831 self.assertSpherePointsAlmostEqual(newPoint, newPoint2) 

832 

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

834 if not point2.atPole(): 

835 self.assertAnglesAlmostEqual( 

836 point2.getLongitude(), newPoint.getLongitude()) 

837 self.assertAnglesAlmostEqual( 

838 point2.getLongitude(), newPoint2.getLongitude()) 

839 self.assertAnglesAlmostEqual( 

840 point2.getLatitude(), newPoint.getLatitude()) 

841 self.assertAnglesAlmostEqual( 

842 point2.getLatitude(), newPoint2.getLatitude()) 

843 else: 

844 self.assertTrue(math.isnan( 

845 newPoint.getLongitude().asRadians())) 

846 self.assertTrue(math.isnan( 

847 newPoint2.getLongitude().asRadians())) 

848 self.assertTrue(math.isnan( 

849 newPoint.getLatitude().asRadians())) 

850 self.assertTrue(math.isnan( 

851 newPoint2.getLatitude().asRadians())) 

852 

853 # Test precision near the poles 

854 lon = 123.0*degrees 

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

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

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

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

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

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

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

862 

863 def testOffsetTangentPlane(self): 

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

865 

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

867 

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

869 dRa = dRaDeg*geom.degrees 

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

871 dDec = dDecDeg*geom.degrees 

872 c1 = SpherePoint(dRa, dDec) 

873 

874 offset = c0.getTangentPlaneOffset(c1) 

875 

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

877 expectedOffset = [ 

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

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

880 ] 

881 

882 for i in range(2): 

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

884 

885 def testIterResult(self): 

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

887 """ 

888 for point in self.pointSet: 

889 if not point.isFinite(): 

890 continue 

891 

892 # Test mechanics directly 

893 it = iter(point) 

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

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

896 with self.assertRaises(StopIteration): 

897 next(it) 

898 

899 # Intended use case 

900 lon, lat = point 

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

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

903 

904 def testStrValue(self): 

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

906 

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

908 is (deliberately) incompletely specified. 

909 """ 

910 for point in self.pointSet: 

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

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

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

914 

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

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

917 self.assertAlmostEqual( 

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

919 else: 

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

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

922 self.assertAlmostEqual( 

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

924 # Latitude must be signed 

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

926 else: 

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

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

929 

930 def testReprValue(self): 

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

932 """ 

933 for point in self.pointSet: 

934 pointRepr = repr(point) 

935 self.assertIn("degrees", pointRepr) 

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

937 

938 spcopy = eval(pointRepr) 

939 self.assertAnglesAlmostEqual( 

940 point.getLongitude(), spcopy.getLongitude()) 

941 self.assertAnglesAlmostEqual( 

942 point.getLatitude(), spcopy.getLatitude()) 

943 

944 def testAverageSpherePoint(self): 

945 """Test the averageSpherePoint function""" 

946 

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

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

949 """ 

950 coords = [] 

951 deltaAngle = 360*degrees / numPts 

952 for ii in range(numPts): 

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

954 coords.append(new) 

955 result = geom.averageSpherePoint(coords) 

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

957 

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

959 for center, start in ( 

960 # RA=0=360 border 

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

962 # North pole 

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

964 # South pole 

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

966 ): 

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

968 

969 def nextUp(self, angle): 

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

971 """ 

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

973 

974 def nextDown(self, angle): 

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

976 """ 

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

978 

979 

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

981 pass 

982 

983 

984def setup_module(module): 

985 lsst.utils.tests.init() 

986 

987 

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

989 lsst.utils.tests.init() 

990 unittest.main()