use snafu::ensure;
use crate::{
errors::{DivisionByZeroSnafu, InvalidRotationSnafu, MathError, PhysicsError},
math::{Matrix3, Vector3},
NaifId,
};
use core::ops::Mul;
use super::{Quaternion, Rotation};
#[derive(Copy, Clone, Debug)]
pub struct MRP {
pub s0: f64,
pub s1: f64,
pub s2: f64,
pub from: NaifId,
pub to: NaifId,
}
impl Rotation for MRP {}
impl MRP {
pub fn new(s0: f64, s1: f64, s2: f64, from: NaifId, to: NaifId) -> Self {
Self {
s0,
s1,
s2,
from,
to,
}
.normalize()
}
pub(crate) fn scalar_norm(&self) -> f64 {
(self.s0 * self.s0 + self.s1 * self.s1 + self.s2 * self.s2).sqrt()
}
pub(crate) fn norm_squared(&self) -> f64 {
self.s0 * self.s0 + self.s1 * self.s1 + self.s2 * self.s2
}
pub fn shadow(&self) -> Result<Self, MathError> {
ensure!(
!self.is_singular(),
DivisionByZeroSnafu {
action: "cannot compute shadow MRP of a singular MRP"
}
);
let s_squared = self.s0 * self.s0 + self.s1 * self.s1 + self.s2 * self.s2;
Ok(MRP {
s0: -self.s0 / s_squared,
s1: -self.s1 / s_squared,
s2: -self.s2 / s_squared,
from: self.from,
to: self.to,
})
}
pub fn is_singular(&self) -> bool {
self.scalar_norm() < f64::EPSILON
}
pub fn normalize(&self) -> Self {
if self.scalar_norm() > 1.0 {
self.shadow().unwrap()
} else {
*self
}
}
pub fn uvec_angle(&self) -> (Vector3, f64) {
Quaternion::from(*self).uvec_angle_rad()
}
pub(crate) fn as_vector(&self) -> Vector3 {
Vector3::new(self.s0, self.s1, self.s2)
}
pub fn b_matrix(&self) -> Matrix3 {
let mut b = Matrix3::zeros();
let s2 = self.scalar_norm();
let q = self.as_vector();
b[(0, 0)] = 1.0 - s2 + 2.0 * q[0] * q[0];
b[(0, 1)] = 2.0 * (q[0] * q[1] - q[2]);
b[(0, 2)] = 2.0 * (q[0] * q[2] + q[1]);
b[(1, 0)] = 2.0 * (q[1] * q[0] + q[2]);
b[(1, 1)] = 1.0 - s2 + 2.0 * q[1] * q[1];
b[(1, 2)] = 2.0 * (q[1] * q[2] - q[0]);
b[(2, 0)] = 2.0 * (q[2] * q[0] - q[1]);
b[(2, 1)] = 2.0 * (q[2] * q[1] + q[0]);
b[(2, 2)] = 1.0 - s2 + 2.0 * q[2] * q[2];
b
}
pub fn diff_eq(&self, w: Vector3) -> MRP {
let s = 0.25 * self.b_matrix() * w;
MRP {
s0: s[0],
s1: s[1],
s2: s[2],
from: self.from,
to: self.to,
}
}
pub fn relative_to(&self, rhs: &Self) -> Result<Self, PhysicsError> {
ensure!(
self.from == rhs.from,
InvalidRotationSnafu {
action: "compute relative MRP",
from1: self.from,
to1: self.to,
from2: rhs.from,
to2: rhs.to
}
);
let s_prime = self;
let s_dprime = rhs;
let denom = 1.0
+ s_prime.norm_squared() * s_dprime.norm_squared()
+ 2.0 * s_prime.as_vector().dot(&s_dprime.as_vector());
let num1 = (1.0 - s_prime.norm_squared()) * s_dprime.as_vector();
let num2 = -(1.0 - s_dprime.norm_squared()) * s_prime.as_vector();
let num3 = 2.0 * s_dprime.as_vector().cross(&s_prime.as_vector());
let sigma = (num1 + num2 + num3) / denom;
Ok(Self::new(sigma[0], sigma[1], sigma[2], rhs.from, self.to))
}
}
impl PartialEq for MRP {
fn eq(&self, other: &Self) -> bool {
self.from == other.from
&& self.to == other.to
&& self.is_singular() == other.is_singular()
&& ((self.as_vector() - other.as_vector()).norm() < 1e-12
|| (self.as_vector() - other.shadow().unwrap().as_vector()).norm() < 1e-12)
}
}
impl Mul for MRP {
type Output = Result<MRP, PhysicsError>;
fn mul(self, rhs: Self) -> Self::Output {
ensure!(
self.to == rhs.from,
InvalidRotationSnafu {
action: "compose MRPs",
from1: self.from,
to1: self.to,
from2: rhs.from,
to2: rhs.to
}
);
let s_prime = self;
let s_dprime = rhs;
let denom = 1.0 + s_prime.norm_squared() * s_dprime.norm_squared()
- 2.0 * s_prime.as_vector().dot(&s_dprime.as_vector());
let num1 = (1.0 - s_prime.norm_squared()) * s_dprime.as_vector();
let num2 = (1.0 - s_dprime.norm_squared()) * s_prime.as_vector();
let num3 = -2.0 * s_dprime.as_vector().cross(&s_prime.as_vector());
let sigma = (num1 + num2 + num3) / denom;
Ok(Self::new(sigma[0], sigma[1], sigma[2], self.from, rhs.to))
}
}
impl TryFrom<Quaternion> for MRP {
type Error = MathError;
fn try_from(q: Quaternion) -> Result<Self, Self::Error> {
ensure!(
(1.0 + q.w).abs() >= f64::EPSILON,
DivisionByZeroSnafu {
action: "quaternion represents a 360 deg rotation, which is a singular MRP"
}
);
let s = Self {
from: q.from,
to: q.to,
s0: q.x / (1.0 + q.w),
s1: q.y / (1.0 + q.w),
s2: q.z / (1.0 + q.w),
}
.normalize();
ensure!(
!s.is_singular(),
DivisionByZeroSnafu {
action: "MRP from quaternion is singular"
}
);
Ok(s)
}
}
impl From<MRP> for Quaternion {
fn from(s: MRP) -> Self {
let qm = s.scalar_norm();
let ps = 1.0 + qm * qm;
Quaternion {
w: (1.0 - qm * qm) / ps,
x: 2.0 * s.s0 / ps,
y: 2.0 * s.s1 / ps,
z: 2.0 * s.s2 / ps,
from: s.from,
to: s.to,
}
.normalize()
}
}
#[cfg(test)]
mod ut_mrp {
use crate::math::rotation::generate_angles;
use super::{Quaternion, MRP};
use core::f64::consts::{FRAC_PI_2, PI, TAU};
#[test]
fn test_singular() {
let q = Quaternion::about_x(TAU, 0, 1);
assert!(MRP::try_from(q).is_err());
let q = Quaternion::about_x(-TAU, 0, 1);
assert!(MRP::try_from(q).is_err());
let q = Quaternion::about_y(TAU, 0, 1);
assert!(MRP::try_from(q).is_err());
let q = Quaternion::about_y(-TAU, 0, 1);
assert!(MRP::try_from(q).is_err());
let q = Quaternion::about_z(TAU, 0, 1);
assert!(MRP::try_from(q).is_err());
let q = Quaternion::about_z(-TAU, 0, 1);
assert!(MRP::try_from(q).is_err());
let s = MRP {
s0: 0.0,
s1: 0.0,
s2: 0.0,
from: 0,
to: 1,
};
assert!(s.is_singular());
assert!(s.shadow().is_err());
let (uvec, angle) = s.uvec_angle();
assert_eq!(uvec.norm(), 0.0);
assert_eq!(angle, 0.0);
}
#[test]
fn test_shadow_set_recip() {
for angle in generate_angles() {
let q = Quaternion::about_z(angle, 0, 1);
if let Ok(m) = MRP::try_from(q) {
let shadow_m = m.shadow().unwrap();
assert_eq!(shadow_m.shadow().unwrap(), m);
}
let q = Quaternion::about_y(angle, 0, 1);
if let Ok(m) = MRP::try_from(q) {
let shadow_m = m.shadow().unwrap();
assert_eq!(shadow_m.shadow().unwrap(), m);
}
let q = Quaternion::about_x(angle, 0, 1);
if let Ok(m) = MRP::try_from(q) {
let shadow_m = m.shadow().unwrap();
assert_eq!(shadow_m.shadow().unwrap(), m);
}
}
}
#[test]
fn test_quat_recip() {
for angle in generate_angles() {
let q = Quaternion::about_x(angle, 0, 1).short();
if let Ok(m) = MRP::try_from(q) {
let q_back = Quaternion::from(m);
assert_eq!(q_back, q, "X fail with {angle}");
}
let q = Quaternion::about_y(angle, 0, 1).short();
if let Ok(m) = MRP::try_from(q) {
let q_back = Quaternion::from(m);
assert_eq!(q_back, q, "Y fail with {angle}");
}
let q = Quaternion::about_z(angle, 0, 1).short();
if let Ok(m) = MRP::try_from(q) {
let q_back = Quaternion::from(m);
assert_eq!(q_back, q, "Z fail with {angle}");
}
}
}
#[test]
fn test_composition() {
let m_x0: MRP = Quaternion::about_x(FRAC_PI_2, 0, 1).try_into().unwrap();
let m_x1: MRP = Quaternion::about_x(FRAC_PI_2, 1, 2).try_into().unwrap();
let m_x: MRP = Quaternion::about_x(PI, 0, 2).try_into().unwrap();
assert_eq!((m_x0 * m_x1).unwrap(), m_x);
let mx_rel_x0 = m_x.relative_to(&m_x0).unwrap();
let rel = Quaternion::about_x(-FRAC_PI_2, 0, 2);
assert_eq!(rel, mx_rel_x0.into());
let rel_mrp: MRP = rel.try_into().unwrap();
assert_eq!(rel_mrp, mx_rel_x0);
}
}