Coverage for python/lsst/cbp/coordUtils.py: 19%
98 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 02:34 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-30 02:34 -0800
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/>.
21"""Coordinate conversion functions"""
23__all__ = ["fieldAngleToVector", "vectorToFieldAngle", "pupilPositionToVector",
24 "computeShiftedPlanePos", "convertVectorFromBaseToPupil", "convertVectorFromPupilToBase",
25 "computeAzAltFromBasePupil", "getFlippedPos", "rotate2d"]
27import math
29import numpy as np
31from lsst.sphgeom import Vector3d
32from lsst.geom import SpherePoint, radians
34# Error data from computeAzAltFromBasePupil.
35# Error is determined by calling convertVectorFromPupilToBase
36# using the az/alt computed by computeAzAltFromBasePupil
37# and measuring the separation between that base vector and the input.
38_RecordError = False # record errors?
39_ErrorLimitArcsec = None # Only record errors larger than this.
40_ErrorList = None # A list of tuples: (errorArcsec, vectorBase, vectorPupil).
42ZeroSpherePoint = SpherePoint(0, 0, radians)
45def startRecordingErrors(errorLimit=1e-11*radians):
46 """Start recording numeric errors in computeAzAltFromBasePupil
47 and reset the error list.
49 Parameters
50 ----------
51 errorLimit : `lsst.geom.Angle`
52 Only errors larger than this limit are recorded.
53 """
54 global _RecordError, _ErrorLimitArcsec, _ErrorList
55 _RecordError = True
56 _ErrorLimitArcsec = errorLimit.asArcseconds()
57 _ErrorList = []
60def stopRecordingErrors():
61 """Stop recording numeric errors in computeAzAltFromBasePupil.
62 """
63 global _RecordError
64 _RecordError = False
67def getRecordedErrors():
68 """Get recorded numeric errors in computeAzAltFromBasePupil,
69 sorted by increasing error.
71 Returns
72 -------
73 errorList : `list` of tuples
74 Recorded error as a list of tuples containing:
75 - |error| in radians
76 - vectorBase
77 - vectorPupil
78 """
79 global _ErrorList
80 if _ErrorList is None:
81 raise RuntimeError("Errors were never recorded")
82 return sorted(_ErrorList, key=lambda elt: elt[0])
85def getFlippedPos(xyPos, flipX):
86 """Get a 2-dimensional position with the x axis properly flipped.
88 Parameters
89 ----------
90 xyPos : pair of `float`
91 Position to rotate.
92 flipX : `bool`
93 True if the x axis should be flipped (negated).
95 Returns
96 -------
97 xyResult : pair of `float`
98 ``xyPos`` with the x axis flipped or not, according to ``flipX``
99 """
100 return (-xyPos[0], xyPos[1]) if flipX else xyPos
103def fieldAngleToVector(xyrad, flipX):
104 """Convert a pupil field angle to a pupil unit vector.
106 Parameters
107 ----------
108 xyrad : `tuple` of 2 `float`
109 x,y :ref:`pupil field angle <lsst.cbp.pupil_field_angle>`
110 (radians).
112 Returns
113 -------
114 vector : `numpy.array` of 3 `float`
115 A unit vector in the :ref:`pupil frame <lsst.cbp.pupil_frame>`.
116 """
117 assert len(xyrad) == 2
118 xyradRightHanded = getFlippedPos(xyrad, flipX=flipX)
119 amount = math.hypot(*xyradRightHanded)*radians
120 bearing = math.atan2(xyradRightHanded[1], xyradRightHanded[0])*radians
121 return np.array(ZeroSpherePoint.offset(bearing=bearing, amount=amount).getVector())
124def vectorToFieldAngle(vec, flipX):
125 """Convert a vector to a pupil field angle.
127 Parameters
128 ----------
129 vec : sequence of 3 `float`
130 3-dimensional vector in the :ref:`pupil frame
131 <lsst.cbp.pupil_frame>`; the magnitude is ignored,
132 but must be large enough to compute an accurate unit vector.
133 flipX : bool
134 Set True if the x axis of the focal plane is flipped
135 with respect to the pupil.
137 Returns
138 -------
139 fieldAngle : `tuple` of 2 `float`
140 x,y :ref:`pupil field angle <lsst.cbp.pupil_field_angle>`
141 (radians).
142 """
143 sp = SpherePoint(Vector3d(*vec))
144 amountRad = ZeroSpherePoint.separation(sp).asRadians()
145 bearingRad = ZeroSpherePoint.bearingTo(sp).asRadians()
146 xyrad = (amountRad*math.cos(bearingRad),
147 amountRad*math.sin(bearingRad))
148 return getFlippedPos(xyrad, flipX=flipX)
151def pupilPositionToVector(xyPos, flipX):
152 """Convert a pupil plane position to a 3D vector.
154 Parameters
155 ----------
156 xyPos : sequence of 2 `float`
157 :ref:`pupil plane position <lsst.cbp.pupil_position>` (mm).
158 flipX : `bool`
159 True if the x axis of the position is flipped (negated).
161 Returns
162 -------
163 vector : sequence of 3 `float`
164 3-dimensional vector in the
165 :ref:`pupil frame <lsst.cbp.pupil_frame>`:
166 x = 0, y = plane position x, z = plane position y.
167 """
168 xyPosRightHanded = getFlippedPos(xyPos, flipX=flipX)
169 return np.array([0, xyPosRightHanded[0], xyPosRightHanded[1]])
172def computeShiftedPlanePos(planePos, fieldAngle, shift):
173 """Compute the plane position of a vector on a plane
174 shifted along the optical axis.
176 Parameters
177 ----------
178 planePos : pair of `float`
179 Plane position at which the vector intersects the plane (x, y).
180 fieldAngle : pair of `float`
181 Field angle of vector (x, y radians).
182 shift : `float`
183 Amount by which the new plane is shifted along the optical axis.
184 If ``shift`` and both components of ``fieldAngle``
185 are positive then both axes of the shifted plane position
186 will be larger (more positive) than ``planePos``.
188 Returns
189 -------
190 shiftedPlanePos : tuple of `float`
191 Plane position at which vector intersects the shifted plane (x, y).
193 Notes
194 -----
195 `flipX` is not an input because for this computation it does not matter
196 if the x axis is flipped: fieldAngle and planePos are either both
197 flipped or not, and that cancels out the effect.
198 """
199 # unit vector y = plane x, unit vector z = plane y
200 # (ignoring flipped x axis, which cancels out)
201 unitVector = fieldAngleToVector(fieldAngle, False)
202 dxy = [shift*unitVector[i]/unitVector[0] for i in (1, 2)]
203 return tuple(planePos[i] + dxy[i] for i in range(2))
206def convertVectorFromBaseToPupil(vectorBase, pupilAzAlt):
207 """Given a vector in base coordinates and the pupil pointing,
208 compute the vector in pupil coordinates.
210 Parameters
211 ----------
212 vectorBase : sequence of 3 `float`
213 3-dimensional vector in the :ref:`base frame
214 <lsst.cbp.base_frame>`.
215 pupilAzAlt : `lsst.geom.SpherePoint`
216 Pointing of the pupil frame as :ref:`internal azimuth, altitude
217 <lsst.cbp.internal_angles>`.
219 Returns
220 -------
221 vectorPupil : `np.array` of 3 `float`
222 3-dimensional vector in the :ref:`pupil frame
223 <lsst.cbp.pupil_frame>`.
225 Notes
226 -----
227 This could be implemented as the following Euler angle
228 rotation matrix, which is:
229 - first rotate about the z axis by azimuth
230 - then rotate about the rotated -y axis by altitude
231 - there is no third rotation
233 c1*c2 -s1 -c1*s2
234 c2*s1 c1 s1s2
235 s1 0 c2
237 where angle 1 = azimuth, angle 2 = altitude,
238 sx = sine(angle x) and cx = cosine(angle x).
240 Knowing this matrix is helpful, e.g. for math inside
241 computeAzAltFromBasePupil.
242 """
243 vectorMag = np.linalg.norm(vectorBase)
244 vectorSpBase = SpherePoint(Vector3d(*vectorBase))
246 telBase = SpherePoint(0, 0, radians)
248 # rotate vector around base z axis by -daz
249 daz = pupilAzAlt[0] - telBase[0]
250 zaxis = SpherePoint(Vector3d(0, 0, 1))
251 vectorSpRot1 = vectorSpBase.rotated(axis=zaxis, amount=-daz)
253 # rotate vector around pupil -y axis by -dalt
254 dalt = pupilAzAlt[1] - telBase[1]
255 negYAxis = SpherePoint(Vector3d(0, -1, 0))
256 vectorSpPupil = vectorSpRot1.rotated(axis=negYAxis, amount=-dalt)
257 return np.array(vectorSpPupil.getVector()) * vectorMag
260def convertVectorFromPupilToBase(vectorPupil, pupilAzAlt):
261 """Given a vector in pupil coordinates and the pupil pointing,
262 compute the vector in base coords.
264 Parameters
265 ----------
266 vectorPupil : sequence of 3 `float`
267 3-dimesional vector in the :ref:`pupil frame
268 <lsst.cbp.pupil_frame>`.
269 pupilAzAlt : `lsst.geom.SpherePoint`
270 Pointing of the pupil frame as :ref:`internal azimuth, altitude
271 <lsst.cbp.internal_angles>`.
273 Returns
274 -------
275 vectorBase : `np.array` of 3 `float`
276 3-dimensional vector in the :ref:`base frame
277 <lsst.cbp.base_frame>`.
278 """
279 vectorMag = np.linalg.norm(vectorPupil)
280 vectorSpPupil = SpherePoint(Vector3d(*vectorPupil))
282 telBase = SpherePoint(0, 0, radians)
284 # rotate vector around pupil -y axis by dalt
285 dalt = pupilAzAlt[1] - telBase[1]
286 negYAxis = SpherePoint(Vector3d(0, -1, 0))
287 vectorSpRot1 = vectorSpPupil.rotated(axis=negYAxis, amount=dalt)
289 # rotate that around base z axis by daz
290 daz = pupilAzAlt[0] - telBase[0]
291 zaxis = SpherePoint(Vector3d(0, 0, 1))
292 vectorSpBase = vectorSpRot1.rotated(axis=zaxis, amount=daz)
293 return np.array(vectorSpBase.getVector()) * vectorMag
296def computeAzAltFromBasePupil(vectorBase, vectorPupil):
297 """Compute az/alt from a vector in the base frame
298 and the same vector in the pupil frame.
300 Parameters
301 ----------
302 vectorBase : `iterable` of three `float`
303 3-dimensional vector in the :ref:`base frame
304 <lsst.cbp.base_frame>`.
305 vectorPupil : `iterable` of `float`
306 The same vector in the :ref:`pupil frame <lsst.cbp.pupil_frame>`.
307 This vector should be within 45 degrees or so of the optical axis
308 for accurate results.
310 Returns
311 -------
312 pupilAzAlt : `lsst.geom.SpherePoint`
313 Pointing of the pupil frame as :ref:`internal azimuth, altitude
314 <lsst.cbp.internal_angles>`.
316 Raises
317 ------
318 ValueError
319 If vectorPupil x <= 0
321 Notes
322 -----
323 The magnitude of each vector is ignored, except that a reasonable
324 magnitude is required in order to compute an accurate unit vector.
325 """
326 if vectorPupil[0] <= 0:
327 raise ValueError("vectorPupil x must be > 0: {}".format(vectorPupil))
329 # Compute telescope altitude using:
330 #
331 # base z = sin(alt) pupil x + cos(alt) pupil z
332 #
333 # One way to derive this is from the last row of an Euler rotation
334 # matrix listed in the comments for convertVectorFromBaseToPupil.
335 spBase = SpherePoint(Vector3d(*vectorBase))
336 spPupil = SpherePoint(Vector3d(*vectorPupil))
337 xb, yb, zb = spBase.getVector()
338 xp, yp, zp = spPupil.getVector()
339 factor = 1 / math.fsum((xp**2, zp**2))
340 addend1 = xp * zb
341 addend2 = zp * math.sqrt(math.fsum((xp**2, zp**2, -zb**2)))
342 if zp == 0:
343 sinAlt = zb/xp
344 else:
345 sinAlt = factor*(addend1 - addend2)
346 alt = math.asin(sinAlt)*radians
348 # Consider the spherical triangle connecting the telescope pointing
349 # (pupil frame x axis), the vector, and zenith (the base frame z axis).
350 # The length of all sides is known:
351 # - sideA is the side connecting the vector to the pupil frame x axis
352 # (since 0, 0 is a unit vector pointing along pupil frame x);
353 # - sideB is the side connecting telescope pointing to the zenith
354 # - sideC is the side connecting the vector to the zenith
355 #
356 # Solve for angleA, the angle between the sides at the zenith;
357 # that angle is the difference in azimuth between the telescope pointing
358 # and the azimuth of the base vector.
359 sideA = SpherePoint(0, 0, radians).separation(spPupil).asRadians()
360 sideB = math.pi/2 - alt.asRadians()
361 sideC = math.pi/2 - spBase[1].asRadians()
363 # sideA can be small or zero so use a half angle formula
364 # sides B and C will always be well away from 0 and 180 degrees
365 semiPerimeter = 0.5*math.fsum((sideA, sideB, sideC))
366 sinHalfAngleA = math.sqrt(math.sin(semiPerimeter - sideB) * math.sin(semiPerimeter - sideC)
367 / (math.sin(sideB) * math.sin(sideC)))
368 daz = 2*math.asin(sinHalfAngleA)*radians
369 if spPupil[0].wrapCtr() > 0:
370 daz = -daz
371 az = spBase[0] + daz
372 global _RecordError
373 if _RecordError: # to study sources of numerical imprecision
374 global _ErrorLimitArcsec, _ErrorList
375 sp = SpherePoint(az, alt)
376 vectorBaseRT = convertVectorFromPupilToBase(vectorPupil, sp)
377 errorArcsec = SpherePoint(Vector3d(*vectorBaseRT)).separation(
378 SpherePoint(Vector3d(*vectorBase))).asArcseconds()
379 if errorArcsec > _ErrorLimitArcsec:
380 _ErrorList.append((errorArcsec, vectorBase, vectorPupil))
381 return SpherePoint(az, alt)
384def rotate2d(pos, angle):
385 """Rotate a 2-dimensional position by a given angle.
387 Parameters
388 ----------
389 pos : pair of `float`
390 Position to rotate.
391 angle : `lsst.geom.Angle`
392 Amount of rotation.
394 Returns
395 -------
396 rotPos : pair of `float`
397 Rotated position.
399 Examples
400 --------
401 ``rotate2d((1, 2), 90*lsst.geom.degrees, False)`` returns `(-2, 1)`.
402 """
403 angRad = angle.asRadians()
404 sinAng = math.sin(angRad)
405 cosAng = math.cos(angRad)
406 return (
407 cosAng*pos[0] - sinAng*pos[1],
408 sinAng*pos[0] + cosAng*pos[1]
409 )