Coverage for tests/test_spherePoint.py: 9%
512 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-14 19:28 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-14 19:28 +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#
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 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))
327 def testTicket1761(self):
328 """Regression test for Ticket 1761.
330 Checks for math errors caused by unnormalized vectors.
331 """
332 refPoint = SpherePoint(lsst.sphgeom.Vector3d(0, 1, 0))
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
340 self.assertAnglesAlmostEqual(sepTrue, sep1)
341 self.assertAnglesAlmostEqual(sepTrue, sep2)
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 ]
367 for pole in poleList:
368 self.assertIsInstance(pole.atPole(), bool)
369 self.assertTrue(pole.atPole())
371 for nonPole in nonPoleList:
372 self.assertIsInstance(nonPole.atPole(), bool)
373 self.assertFalse(nonPole.atPole())
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 ]
397 for finite in finiteList:
398 self.assertIsInstance(finite.isFinite(), bool)
399 self.assertTrue(finite.isFinite())
401 for nonFinite in nonFiniteList:
402 self.assertIsInstance(nonFinite.isFinite(), bool)
403 self.assertFalse(nonFinite.isFinite())
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))
410 with self.assertRaises(IndexError):
411 point[2]
412 with self.assertRaises(IndexError):
413 point[-3]
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)
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()))
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)
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)
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)
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)
480 def checkTransitive(self, delta):
481 """Test if equality is transitive even for close points.
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.
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)
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)
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
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 ]
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)
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()))
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)
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)
595 def testBearingToValueSingular(self):
596 """White-box test: bearingTo() may be unstable if points are near opposite poles.
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()))
608 self.assertAnglesAlmostEqual(southPole.bearingTo(northPoleSame),
609 geom.HALFPI*geom.radians)
610 self.assertAnglesAlmostEqual(southPole.bearingTo(northPoleOpposite),
611 (geom.PI + geom.HALFPI)*geom.radians)
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
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()))
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)
652 # Verify to precision of quoted distance and positions.
653 self.assertAlmostEqual(
654 32.7930, spica.separation(arcturus).asDegrees(), 4)
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)
661 self.assertAnglesAlmostEqual(epsilon, spicaPlus.separation(spica))
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
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))
680 @staticmethod
681 def toVector(longitude, latitude):
682 """Converts a set of spherical coordinates to a 3-vector.
684 The conversion shall not be performed by any library, to ensure
685 that the test case does not duplicate the code being tested.
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.
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)
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)
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)
724 self.assertIsInstance(newPoint, SpherePoint)
725 self.assertAlmostEqual(
726 longitude + arcLen, newPoint.getLongitude().asDegrees())
727 self.assertAlmostEqual(
728 latitude, newPoint.getLatitude().asDegrees())
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)
736 self.assertAlmostEqual(
737 longitude, newPoint.getLongitude().asDegrees())
738 self.assertAlmostEqual(
739 latitude + arcLen, newPoint.getLatitude().asDegrees())
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())
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)
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))
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)
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()))
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()))
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()))
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)
810 self.assertAlmostEqual(0.0, newPoint.getLongitude().asDegrees())
811 self.assertAlmostEqual(80.0, newPoint.getLatitude().asDegrees())
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)
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)
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)
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)
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()))
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())
879 def testOffsetTangentPlane(self):
880 """Test offsets on a tangent plane (good for small angles)"""
882 c0 = SpherePoint(0.0, 0.0, geom.degrees)
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)
890 offset = c0.getTangentPlaneOffset(c1)
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 ]
898 for i in range(2):
899 self.assertAnglesAlmostEqual(offset[i], expectedOffset[i])
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
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)
915 # Intended use case
916 lon, lat = point
917 self.assertEqual(point.getLongitude(), lon)
918 self.assertEqual(point.getLatitude(), lat)
920 def testStrValue(self):
921 """Test if __str__ produces output consistent with its spec.
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,))
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')
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(",")))
954 spcopy = eval(pointRepr)
955 self.assertAnglesAlmostEqual(
956 point.getLongitude(), spcopy.getLongitude())
957 self.assertAnglesAlmostEqual(
958 point.getLatitude(), spcopy.getLatitude())
960 def testAverageSpherePoint(self):
961 """Test the averageSpherePoint function"""
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)
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)
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
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
996class MemoryTester(lsst.utils.tests.MemoryTestCase):
997 pass
1000def setup_module(module):
1001 lsst.utils.tests.init()
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()