Coverage for tests/test_spherePoint.py : 9%

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#
22# -*- python -*-
23"""
24Unit tests for SpherePoint
26Run with:
27 python testSpherePoint.py
28or
29 python
30 >>> import testSpherePoint
31 >>> testSpherePoint.run()
32"""
34import copy
35import math
36import re
37import unittest
39import numpy as np
40from numpy.testing import assert_allclose
42import lsst.utils.tests
43import lsst.sphgeom
44import lsst.geom as geom
45import lsst.pex.exceptions as pexEx
47from lsst.geom import degrees, radians, SpherePoint
48from numpy import nan, inf
51class SpherePointTestSuite(lsst.utils.tests.TestCase):
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 ]
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
73 @staticmethod
74 def positions():
75 """Provide valid coordinates for nominal-case testing.
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)
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
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)
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)
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)
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)
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)
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)
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)
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)
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))
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())
173 def testCopyConstructor(self):
174 sp = SpherePoint(-42.0*degrees, 45.0*degrees)
175 spcopy = SpherePoint(sp)
176 self.assertEqual(sp, spcopy)
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
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)
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())
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)
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)
224 def testTicket1394(self):
225 """Regression test for Ticket 1761.
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))
236 self.assertEqual(point[0].asDegrees(), 0.0)
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())
256 def testGetVectorValue(self):
257 """Test if getVector() returns the expected value.
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)
276 # Convert back to spherical.
277 newLon, newLat = SpherePoint(newVector)
278 self.assertAlmostEqual(newLon.asDegrees(), lon.asDegrees())
279 self.assertAlmostEqual(newLat.asDegrees(), lat.asDegrees())
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 ]
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())
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))
311 def testTicket1761(self):
312 """Regression test for Ticket 1761.
314 Checks for math errors caused by unnormalized vectors.
315 """
316 refPoint = SpherePoint(lsst.sphgeom.Vector3d(0, 1, 0))
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
324 self.assertAnglesAlmostEqual(sepTrue, sep1)
325 self.assertAnglesAlmostEqual(sepTrue, sep2)
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 ]
351 for pole in poleList:
352 self.assertIsInstance(pole.atPole(), bool)
353 self.assertTrue(pole.atPole())
355 for nonPole in nonPoleList:
356 self.assertIsInstance(nonPole.atPole(), bool)
357 self.assertFalse(nonPole.atPole())
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 ]
381 for finite in finiteList:
382 self.assertIsInstance(finite.isFinite(), bool)
383 self.assertTrue(finite.isFinite())
385 for nonFinite in nonFiniteList:
386 self.assertIsInstance(nonFinite.isFinite(), bool)
387 self.assertFalse(nonFinite.isFinite())
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))
394 with self.assertRaises(IndexError):
395 point[2]
396 with self.assertRaises(IndexError):
397 point[-3]
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)
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()))
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)
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)
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)
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)
464 def checkTransitive(self, delta):
465 """Test if equality is transitive even for close points.
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.
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)
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)
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
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 ]
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)
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()))
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)
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)
579 def testBearingToValueSingular(self):
580 """White-box test: bearingTo() may be unstable if points are near opposite poles.
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()))
592 self.assertAnglesAlmostEqual(southPole.bearingTo(northPoleSame),
593 geom.HALFPI*geom.radians)
594 self.assertAnglesAlmostEqual(southPole.bearingTo(northPoleOpposite),
595 (geom.PI + geom.HALFPI)*geom.radians)
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
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()))
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)
636 # Verify to precision of quoted distance and positions.
637 self.assertAlmostEqual(
638 32.7930, spica.separation(arcturus).asDegrees(), 4)
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)
645 self.assertAnglesAlmostEqual(epsilon, spicaPlus.separation(spica))
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
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))
664 @staticmethod
665 def toVector(longitude, latitude):
666 """Converts a set of spherical coordinates to a 3-vector.
668 The conversion shall not be performed by any library, to ensure
669 that the test case does not duplicate the code being tested.
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.
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)
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)
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)
708 self.assertIsInstance(newPoint, SpherePoint)
709 self.assertAlmostEqual(
710 longitude + arcLen, newPoint.getLongitude().asDegrees())
711 self.assertAlmostEqual(
712 latitude, newPoint.getLatitude().asDegrees())
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)
720 self.assertAlmostEqual(
721 longitude, newPoint.getLongitude().asDegrees())
722 self.assertAlmostEqual(
723 latitude + arcLen, newPoint.getLatitude().asDegrees())
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())
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)
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))
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)
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()))
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()))
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()))
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)
794 self.assertAlmostEqual(0.0, newPoint.getLongitude().asDegrees())
795 self.assertAlmostEqual(80.0, newPoint.getLatitude().asDegrees())
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)
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)
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)
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)
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()))
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())
863 def testOffsetTangentPlane(self):
864 """Test offsets on a tangent plane (good for small angles)"""
866 c0 = SpherePoint(0.0, 0.0, geom.degrees)
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)
874 offset = c0.getTangentPlaneOffset(c1)
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 ]
882 for i in range(2):
883 self.assertAnglesAlmostEqual(offset[i], expectedOffset[i])
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
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)
899 # Intended use case
900 lon, lat = point
901 self.assertEqual(point.getLongitude(), lon)
902 self.assertEqual(point.getLatitude(), lat)
904 def testStrValue(self):
905 """Test if __str__ produces output consistent with its spec.
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,))
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')
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(",")))
938 spcopy = eval(pointRepr)
939 self.assertAnglesAlmostEqual(
940 point.getLongitude(), spcopy.getLongitude())
941 self.assertAnglesAlmostEqual(
942 point.getLatitude(), spcopy.getLatitude())
944 def testAverageSpherePoint(self):
945 """Test the averageSpherePoint function"""
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)
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)
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
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
980class MemoryTester(lsst.utils.tests.MemoryTestCase):
981 pass
984def setup_module(module):
985 lsst.utils.tests.init()
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()