Coverage for tests/test_coordUtils.py: 13%
148 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 10:49 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 10:49 +0000
1# This file is part of cbp.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import itertools
23import math
24import unittest
26import numpy as np
28from lsst.sphgeom import Vector3d
29from lsst.geom import SpherePoint, degrees, radians
30from lsst.cbp import coordUtils
31import lsst.utils.tests
33RAD_PER_DEG = math.pi / 180
35# set True to record and report numeric error in convertVectorFromPupilToBase
36ReportRecordedErrors = True
39class CoordUtilsTestCase(lsst.utils.tests.TestCase):
41 def setUp(self):
42 if ReportRecordedErrors:
43 coordUtils.startRecordingErrors()
45 def tearDown(self):
46 if ReportRecordedErrors:
47 errList = coordUtils.getRecordedErrors()
48 coordUtils.stopRecordingErrors()
49 if len(errList) > 0:
50 print("\nWarning: recorded {} numerical errors in computeAzAltFromBasePupil;"
51 " the worst 5 are:".format(len(errList)))
52 for err, vectorBase, vectorPupil in errList[-5:]:
53 print("error={:0.5f} arcsec, vectorBase={}, vectorPupil={}".format(
54 err, vectorBase, vectorPupil))
56 def testGetFlippedPos(self):
57 floatList = (0, -5.1, 4.3)
58 for x, y, flipX in itertools.product(floatList, floatList, (False, True)):
59 with self.subTest(x=x, y=y, flipX=flipX):
60 if flipX:
61 desiredResult = (-x, y)
62 else:
63 desiredResult = (x, y)
64 result = coordUtils.getFlippedPos(xyPos=(x, y), flipX=flipX)
65 self.assertEqual(result, desiredResult)
67 def testFieldAngleToVector(self):
68 sp00 = SpherePoint(0, 0, degrees)
69 degList = (-90, -89.9, -20, 0, 10, 89.9, 90)
70 for xdeg, ydeg, flipX in itertools.product(degList, degList, (False, True)):
71 with self.subTest(xdeg=xdeg, ydeg=ydeg, flipX=flipX):
72 xrad = xdeg * RAD_PER_DEG
73 signx = -1 if flipX else 1
74 testOrientation = xdeg != 0 or ydeg != 0
75 yrad = ydeg * RAD_PER_DEG
76 fieldAngle = (xrad, yrad)
77 vector = coordUtils.fieldAngleToVector(fieldAngle, flipX)
78 self.assertAlmostEqual(np.linalg.norm(vector), 1)
79 if testOrientation:
80 # Orientation should match.
81 orientationFromFieldAngle = math.atan2(yrad, signx*xrad)*radians
82 # Field angle x = vector y, field angle y = vector z.
83 orientationFromVector = math.atan2(vector[2], vector[1])*radians
84 self.assertAnglesAlmostEqual(orientationFromVector, orientationFromFieldAngle)
86 # Now test as spherical geometry.
87 sp = SpherePoint(Vector3d(*vector))
88 separation = sp00.separation(sp)
89 predictedSeparation = math.hypot(xrad, yrad)*radians
90 self.assertAnglesAlmostEqual(predictedSeparation, separation)
91 if testOrientation:
92 bearing = sp00.bearingTo(sp)
93 self.assertAnglesAlmostEqual(orientationFromFieldAngle, bearing)
95 # Test round trip through vectorToFieldAngle.
96 fieldAngleFromVector = coordUtils.vectorToFieldAngle(vector, flipX)
97 np.testing.assert_allclose(fieldAngleFromVector, fieldAngle, atol=1e-15)
99 def testVectorToFieldAngle(self):
100 # Note: more sophisticated cases are tested by testFieldAngleToVector.
101 for flipX in (False, True):
102 signx = -1 if flipX else 1
103 for magMultiplier in (0.001, 1, 1000):
104 for vector, predictedFieldAngleDeg in (
105 ((1, 0, 0), (0, 0)),
106 ((0, 1, 0), (signx*90, 0)),
107 ((0, 0, 1), (0, 90)),
108 ):
109 predictedFieldAngle = [val*RAD_PER_DEG for val in predictedFieldAngleDeg]
110 scaledVector = np.array(vector) * magMultiplier
111 fieldAngle = coordUtils.vectorToFieldAngle(scaledVector, flipX)
112 np.testing.assert_allclose(predictedFieldAngle, fieldAngle, atol=1e-15)
114 def testComputeShiftedPlanePosZeroFieldAngle(self):
115 """Test computeShiftedPlanePos with zero field angle
117 This should result in no change
118 """
119 zeroFieldAngle = (0, 0)
120 for planePos in (
121 (-1000, -2000),
122 (0, 0),
123 (5000, 4000),
124 ):
125 for shift in (-500, 0, 500):
126 shiftedPlanePos = coordUtils.computeShiftedPlanePos(planePos, zeroFieldAngle, shift)
127 self.assertPairsAlmostEqual(planePos, shiftedPlanePos)
129 def testComputeShiftedPlanePosZeroShift(self):
130 """Test computeShiftedPlanePos with zero shift
132 This should result in no change
133 """
134 zeroShift = 0
135 for planePos in (
136 (-1000, -2000),
137 (0, 0),
138 (5000, 4000),
139 ):
140 for fieldAngle in (
141 (0.5, 0.5),
142 (0, 1),
143 (-0.5, 0.3),
144 ):
145 shiftedPlanePos = coordUtils.computeShiftedPlanePos(planePos, fieldAngle, zeroShift)
146 self.assertPairsAlmostEqual(planePos, shiftedPlanePos)
148 def testComputeShiftedPlanePos(self):
149 """Test computeShiftedPlanePos for the general case
150 """
151 # In the general case the increase in x and y equals the shift
152 # times y/x, z/x of a vector equivalent to the field angle.
153 for ratios in (
154 (0.0, 0.0),
155 (0.5, 0.5),
156 (-0.23, 0.75),
157 (0.3, 0.1),
158 ):
159 vector = (1, ratios[0], ratios[1])
160 fieldAngle = coordUtils.vectorToFieldAngle(vector, False)
161 for planePos in (
162 (-1000, -2000),
163 (0, 0),
164 (5000, 4000),
165 ):
166 for shift3 in (-550, 0, 375):
167 predictedShiftedPlanePos = [planePos[i] + shift3*ratios[i] for i in range(2)]
168 shiftedPlanePos = coordUtils.computeShiftedPlanePos(planePos, fieldAngle, shift3)
169 self.assertPairsAlmostEqual(shiftedPlanePos, predictedShiftedPlanePos)
171 def testConvertVectorFromPupilToBase(self):
172 """Test convertVectorFromPupilToBase and convertVectorFromBaseToPupil
173 """
174 cos30 = math.cos(30 * math.pi / 180)
175 sin30 = math.sin(30 * math.pi / 180)
176 for magMultiplier in (0.001, 1, 1000):
177 for azAltDeg, vectorPupil, predictedVectorBase in (
178 # At az=0, alt=0: base = pupil.
179 ((0, 0), (1, 0, 0), (1, 0, 0)),
180 ((0, 0), (0, 1, 0), (0, 1, 0)),
181 ((0, 0), (0, 0, -1), (0, 0, -1)),
182 ((0, 0), (1, -1, 1), (1, -1, 1)),
183 # At az=90, alt=0:
184 # base x = pupil -y,
185 # base y = pupil x,
186 # base z = pupil z.
187 ((90, 0), (1, 0, 0), (0, 1, 0)),
188 ((90, 0), (0, 1, 0), (-1, 0, 0)),
189 ((90, 0), (0, 0, -1), (0, 0, -1)),
190 ((90, 0), (1, -1, 1), (1, 1, 1)),
191 # At az=0, alt=90:
192 # base x = - pupil z,
193 # base y = pupil y,
194 # base z = pupil x.
195 ((0, 90), (1, 0, 0), (0, 0, 1)),
196 ((0, 90), (0, 1, 0), (0, 1, 0)),
197 ((0, 90), (0, 0, -1), (1, 0, 0)),
198 ((0, 90), (1, -1, 1), (-1, -1, 1)),
199 # At az=90, alt=90: base x = -pupil y, base y = - pupil z,
200 # base z = pupil x.
201 ((90, 90), (1, 0, 0), (0, 0, 1)),
202 ((90, 90), (0, 1, 0), (-1, 0, 0)),
203 ((90, 90), (0, 0, -1), (0, 1, 0)),
204 ((90, 90), (1, -1, 1), (1, -1, 1)),
205 # At az=0, alt=45:
206 # base x = cos(30) * pupil x - sin(30) * pupil z
207 # base y = pupil y
208 # base z = sin(30) * pupil x + cos(30) * pupil z.
209 ((0, 30), (1, 0, 0), (cos30, 0, sin30)),
210 ((0, 30), (0, 1, 0), (0, 1, 0)),
211 ((0, 30), (0, 0, -1), (sin30, 0, -cos30)),
212 ((0, 30), (1, -1, 1), (cos30 - sin30, -1, sin30 + cos30)),
213 # At az=30, alt=0:
214 # base x = cos(30) * pupil x - sin(30) * pupil y
215 # base y = sin(30) * pupil x + cos(30) * pupil y
216 # base z = pupil z
217 ((30, 0), (1, 0, 0), (cos30, sin30, 0)),
218 ((30, 0), (0, 1, 0), (-sin30, cos30, 0)),
219 ((30, 0), (0, 0, -1), (0, 0, -1)),
220 ((30, 0), (1, -1, 1), (cos30 + sin30, sin30 - cos30, 1)),
221 ):
222 vectorPupil = np.array(vectorPupil) * magMultiplier
223 predictedVectorBase = np.array(predictedVectorBase) * magMultiplier
224 pupilAzAlt = SpherePoint(*azAltDeg, degrees)
225 vectorBase = coordUtils.convertVectorFromPupilToBase(vectorPupil=vectorPupil,
226 pupilAzAlt=pupilAzAlt)
227 atol = max(magMultiplier, 1) * 1e-15
228 msg = "azAltDeg={}, vectorPupil={}".format(azAltDeg, vectorPupil)
229 np.testing.assert_allclose(vectorBase, predictedVectorBase, atol=atol,
230 err_msg=msg, verbose=True)
232 vectorPupilRoundTrip = coordUtils.convertVectorFromBaseToPupil(vectorBase=vectorBase,
233 pupilAzAlt=pupilAzAlt)
234 np.testing.assert_allclose(vectorPupil, vectorPupilRoundTrip, atol=atol,
235 err_msg=msg, verbose=True)
237 def testComputeAzAltFromPupilBaseWithBaseEqualsPupil(self):
238 """Test computeAzAltFromBasePupil with baseVector=pupilVector,
239 so the telescope will to internal az, alt=0
240 """
241 zeroSp = SpherePoint(0, 0, radians)
242 for vector, pupilMagFactor, baseMagFactor in itertools.product(
243 ((1, 0, 0), (0.1, -1, 0), (0.1, -0.5, 0.5), (0.5, 0, 0.5), (1, 0.7, -0.8)),
244 (1, 1000),
245 (1, 1000),
246 ):
247 with self.subTest(vector=vector, pupilMagFactor=pupilMagFactor, baseMagFactor=baseMagFactor):
248 vectorPupil = np.array(vector, dtype=float) * pupilMagFactor
249 vectorBase = np.array(vector, dtype=float) * baseMagFactor
250 obs = coordUtils.computeAzAltFromBasePupil(vectorPupil=vectorPupil,
251 vectorBase=vectorBase)
252 sep = zeroSp.separation(obs).asRadians()
253 self.assertLess(sep, 1e-14)
255 def testComputeAzAltFromPupilBaseWithVectorPupil100(self):
256 """Test computeAzAltFromBasePupil with vectorPupil = (1, 0, 0),
257 so internal az/alt points along vectorBase
258 """
259 vectorPupil = (1, 0, 0)
260 for vectorBase, pupilMagFactor, baseMagFactor in itertools.product(
261 ((1, 0, 0), (0, -1, 0), (0, -0.5, 0.5), (0.5, 0, 0.5), (1, 0.7, -0.8)),
262 (1, 1000),
263 (1, 1000),
264 ):
265 with self.subTest(vectorBase=vectorBase, pupilMagFactor=pupilMagFactor,
266 baseMagFactor=baseMagFactor):
267 predictedPupilAzalt = SpherePoint(Vector3d(*vectorBase))
268 vectorPupilScaled = np.array(vectorPupil, dtype=float) * pupilMagFactor
269 vectorBaseScaled = np.array(vectorBase, dtype=float) * baseMagFactor
270 pupilAzAlt = coordUtils.computeAzAltFromBasePupil(vectorPupil=vectorPupilScaled,
271 vectorBase=vectorBaseScaled)
272 sep = pupilAzAlt.separation(predictedPupilAzalt)
273 if sep.asRadians() > 1e-14:
274 print("Warning: sep={:0.5f} asec for vectorPupilScaled={}, vectorBaseScaled={}".format(
275 sep.asArcseconds(), vectorPupilScaled, vectorBaseScaled))
276 # The worst error I see is 0.0026"
277 # for vectorBase=(1, 0.7, -0.8).
278 # That is worrisome, but acceptable.
279 self.assertLess(sep.asArcseconds(), 0.01)
281 def testComputeAzAltFromPupilBase(self):
282 """Test computeAzAltFromBasePupil with general values
283 """
284 # transform the pupil vector back to the base vector
285 # using the computed internal az/alt position
286 for vectorPupil, vectorBase, pupilMagFactor, baseMagFactor in itertools.product(
287 ((1, 0, 0), (2, 1, 0), (2, 0, 1), (2, 0.7, -0.8)),
288 ((1, 0, 0), (0, 1, 0), (1, -0.7, 0.8)),
289 (1, 1000),
290 (1, 1000),
291 ):
292 with self.subTest(vectorPupil=vectorPupil, vectorBase=vectorBase, pupilMagFactor=pupilMagFactor,
293 baseMagFactor=baseMagFactor):
294 vectorPupilScaled = np.array(vectorPupil, dtype=float) * pupilMagFactor
295 pupilMag = np.linalg.norm(vectorPupilScaled)
296 vectorBaseScaled = np.array(vectorBase, dtype=float) * baseMagFactor
297 pupilAzAlt = coordUtils.computeAzAltFromBasePupil(vectorPupil=vectorPupilScaled,
298 vectorBase=vectorBaseScaled)
299 # Check the round trip; note that the magnitude
300 # of the returned vector will equal
301 # the magnitude of the input vector.
302 vectorBaseRoundTrip = coordUtils.convertVectorFromPupilToBase(
303 vectorPupil=vectorPupilScaled,
304 pupilAzAlt=pupilAzAlt)
305 vectorBaseRoundTripMag = np.linalg.norm(vectorBaseRoundTrip)
306 self.assertAlmostEqual(vectorBaseRoundTripMag, pupilMag, delta=1e-15*pupilMag)
307 spBase = SpherePoint(Vector3d(*vectorBase))
308 spBaseRoundTrip = SpherePoint(Vector3d(*vectorBaseRoundTrip))
309 sep = spBase.separation(spBaseRoundTrip)
310 self.assertLess(sep.asRadians(), 2e-15)
312 def testRotate2d(self):
313 for pos, angleDeg, expectedPos in (
314 ((1, 2), 0, (1, 2)),
315 ((1, 0), -30, (math.cos(RAD_PER_DEG*30), -0.5)),
316 ((0, 1), -30, (0.5, math.cos(RAD_PER_DEG*30))),
317 ((1, 2), 90, (-2, 1)),
318 ):
319 with self.subTest(pos=pos, angleDeg=angleDeg, expectedPos=expectedPos):
320 angle = angleDeg*degrees
321 rotatedPos = coordUtils.rotate2d(pos, angle)
322 self.assertPairsAlmostEqual(rotatedPos, expectedPos)
325class MemoryTester(lsst.utils.tests.MemoryTestCase):
326 pass
329def setup_module(module):
330 lsst.utils.tests.init()
333if __name__ == "__main__": 333 ↛ 334line 333 didn't jump to line 334, because the condition on line 333 was never true
334 lsst.utils.tests.init()
335 unittest.main()