from math import acos, cos, sin, sqrt, floor, pi
from typing import NamedTuple, Sequence, List
[docs]
class Vector(NamedTuple):
""" Vector of the form:
x <i>, y <j>, z <k>
"""
x: float
y: float
z: float
def interpolate(self, other, alpha):
x = (1.0 - alpha) * self.x + alpha * other.x
y = (1.0 - alpha) * self.y + alpha * other.y
z = (1.0 - alpha) * self.z + alpha * other.z
return Vector(x, y, z)
[docs]
class ECI(Vector):
""" Vector class denoting Earth Centered Inertial (fixed to the stars) reference frame. """
x: float
y: float
z: float
[docs]
def to_geo(self, mjd: float) -> Vector:
""" Code to calculate theta copied from the ECIToGeo C++ L1 Services function. """
ut = mjd - 51544.5
ut0 = floor(ut - 0.5) + 0.5
frac = (ut - ut0)
tu = ut0 / 36525.0
gmst = (24110.54841 + (8640184.812866 + (0.093104 - 0.0000062 * tu) * tu) * tu) / 86400.0 + frac * 1.00273790935
gmst = gmst - floor(gmst)
theta = gmst * 2.0 * pi # Angle to rotate by in radians
rotation_quart = TaitBryanZYX(yaw=-theta, pitch=0.0, roll=0.0).to_quaternion()
return rotation_quart.rot([self])[0]
[docs]
class ECEF(Vector):
""" Vector class denoting Earth Centered, Earth Fixed reference frame. """
x: float
y: float
z: float
[docs]
class Geodetic(NamedTuple):
""" Vector class denoting Earth Centered, Earth Fixed reference frame in latitude, longitude, height. """
latitude: float
longitude: float
height: float
[docs]
class UnitQuaternion(NamedTuple):
""" Normalized Quaternion with multiply and vector rotate methods of the form:
a + bi + cj + dk
"""
a: float
b: float
c: float
d: float
def __neg__(self):
return UnitQuaternion(-self.a, -self.b, -self.c, -self.d)
def __add__(self, other):
a = self.a + other.a
b = self.b + other.b
c = self.c + other.c
d = self.d + other.d
return UnitQuaternion(a, b, c, d)
def __sub__(self, other):
return self + (-other)
def __mul__(self, other):
if type(other) is UnitQuaternion:
a = self.a * other.a - self.b * other.b - self.c * other.c - self.d * other.d
b = self.a * other.b + self.b * other.a + self.c * other.d - self.d * other.c
c = self.a * other.c - self.b * other.d + self.c * other.a + self.d * other.b
d = self.a * other.d + self.b * other.c - self.c * other.b + self.d * other.a
return UnitQuaternion(a, b, c, d)
else:
try:
return UnitQuaternion(other * self.a, other * self.b, other * self.c, other * self.d)
except Exception:
raise TypeError(f'{self.__class__.__name__} does not support __mul__ with {other.__class__.__name__})')
def normalize(self):
norm = sqrt(self.a ** 2.0 + self.b ** 2.0 + self.c ** 2.0 + self.d ** 2.0)
return UnitQuaternion(self.a / norm, self.b / norm, self.c / norm, self.d / norm)
def dot(self, other) -> float:
return self.a * other.a + self.b * other.b + self.c * other.c + self.d * other.d
[docs]
def rot(self, other: Sequence[Vector]) -> List[Vector]:
""" Rotate a list of vectors by a Quaternion.
q * v * q'
"""
a, b, c, d = self
# Define the 3x3 rotation matrix (A)
a00 = a ** 2.0 + b ** 2.0 - c ** 2.0 - d ** 2.0
a01 = 2.0 * b * c - 2.0 * a * d
a02 = 2.0 * b * d + 2.0 * a * c
a10 = 2.0 * b * c + 2.0 * a * d
a11 = a ** 2.0 - b ** 2.0 + c ** 2.0 - d ** 2.0
a12 = 2.0 * c * d - 2.0 * a * b
a20 = 2.0 * b * d - 2.0 * a * c
a21 = 2.0 * c * d + 2.0 * a * b
a22 = a ** 2.0 - b ** 2.0 - c ** 2.0 + d ** 2.0
rotated = []
for x, y, z in other:
xx = a00 * x + a01 * y + a02 * z
yy = a10 * x + a11 * y + a12 * z
zz = a20 * x + a21 * y + a22 * z
rotated.append(Vector(xx, yy, zz))
return rotated
[docs]
def rot_no_z(self, other: Sequence[Vector]) -> List[Vector]:
""" Rotate a list of vectors with z=0.0 by a Quaternion.
q * v * q'
"""
a, b, c, d = self
# Define the 3x2 rotation matrix (A)
a00 = a ** 2.0 + b ** 2.0 - c ** 2.0 - d ** 2.0
a01 = 2.0 * b * c - 2.0 * a * d
a10 = 2.0 * b * c + 2.0 * a * d
a11 = a ** 2.0 - b ** 2.0 + c ** 2.0 - d ** 2.0
a20 = 2.0 * b * d - 2.0 * a * c
a21 = 2.0 * c * d + 2.0 * a * b
rotated = []
for x, y, z in other:
xx = a00 * x + a01 * y
yy = a10 * x + a11 * y
zz = a20 * x + a21 * y
rotated.append(Vector(xx, yy, zz))
return rotated
[docs]
def rot_unit_x(self) -> Vector:
""" Rotate the x-unit vector <1, 0, 0> by a Quaternion.
q * v * q'
"""
a, b, c, d = self
# Define the 3x3 rotation matrix (A)
x = a ** 2.0 + b ** 2.0 - c ** 2.0 - d ** 2.0
y = 2.0 * b * c + 2.0 * a * d
z = 2.0 * b * d - 2.0 * a * c
return Vector(x, y, z)
[docs]
def slerp(self, other, alpha):
""" Quaternion slerp adapted from c++ reference on wikipedia article.
Only unit quaternions are valid rotations.
"""
dot = self.dot(other)
# If the dot product is negative, the quaternions
# have opposite handed-ness and slerp won't take
# the shorter path. Fix by reversing one quaternion.
if dot < 0.0:
other = -other
dot = -dot
dot_threshold = 0.9995
if dot > dot_threshold:
# If the inputs are too close for comfort, linearly interpolate and normalize the result.
# return self - alpha * (other - self)
return (self + (other - self) * alpha).normalize()
theta_0 = acos(dot) # theta_0 = angle between input vectors
theta = theta_0 * alpha # theta = angle between self and result
perpendicular = (other - self * dot).normalize() # { self, perpendicular } is now an orthonormal basis
return self * cos(theta) + perpendicular * sin(theta)
[docs]
class TaitBryanZYX(NamedTuple):
yaw: float # heading
pitch: float # attitude
roll: float # bank
def to_quaternion(self) -> UnitQuaternion:
cr = cos(self.roll * 0.5)
sr = sin(self.roll * 0.5)
cp = cos(self.pitch * 0.5)
sp = sin(self.pitch * 0.5)
cy = cos(self.yaw * 0.5)
sy = sin(self.yaw * 0.5)
a = cy * cr * cp + sy * sr * sp
b = cy * sr * cp - sy * cr * sp
c = cy * cr * sp + sy * sr * cp
d = sy * cr * cp - cy * sr * sp
return UnitQuaternion(a, b, c, d)