use crate::errors::{InvalidRotationSnafu, PhysicsError};
use crate::math::rotation::EPSILON;
use crate::structure::dataset::DataSetT;
use crate::{math::Vector3, math::Vector4, NaifId};
use core::fmt;
use core::ops::Mul;
use der::{Decode, Encode, Reader, Writer};
use nalgebra::Matrix4x3;
use serde::{Deserialize, Serialize};
use snafu::ensure;
#[cfg(feature = "python")]
use pyo3::prelude::*;
use super::EPSILON_RAD;
pub type Quaternion = EulerParameter;
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyclass)]
#[cfg_attr(
feature = "python",
pyo3(name = "Quaternion", module = "anise.rotation")
)]
pub struct EulerParameter {
pub w: f64,
pub x: f64,
pub y: f64,
pub z: f64,
pub from: NaifId,
pub to: NaifId,
}
impl EulerParameter {
pub const fn identity(from: NaifId, to: NaifId) -> Self {
Self {
w: 1.0,
x: 0.0,
y: 0.0,
z: 0.0,
from,
to,
}
}
pub fn new(w: f64, x: f64, y: f64, z: f64, from: NaifId, to: NaifId) -> Self {
Self {
w,
x,
y,
z,
from,
to,
}
.normalize()
}
pub fn about_x(angle_rad: f64, from: NaifId, to: NaifId) -> Self {
let (s_theta, c_theta) = (angle_rad / 2.0).sin_cos();
Self {
w: c_theta,
x: s_theta,
y: 0.0,
z: 0.0,
from,
to,
}
.normalize()
}
pub fn about_y(angle_rad: f64, from: NaifId, to: NaifId) -> Self {
let (s_theta, c_theta) = (angle_rad / 2.0).sin_cos();
Self {
w: c_theta,
x: 0.0,
y: s_theta,
z: 0.0,
from,
to,
}
.normalize()
}
pub fn about_z(angle_rad: f64, from: NaifId, to: NaifId) -> Self {
let (s_theta, c_theta) = (angle_rad / 2.0).sin_cos();
Self {
w: c_theta,
x: 0.0,
y: 0.0,
z: s_theta,
from,
to,
}
.normalize()
}
pub fn derivative(&self, omega_rad_s: Vector3) -> Self {
let q = 0.5 * self.b_matrix() * omega_rad_s;
Self {
w: q[0],
x: q[1],
y: q[2],
z: q[3],
from: self.from,
to: self.to,
}
}
pub fn b_matrix(&self) -> Matrix4x3<f64> {
Matrix4x3::new(
-self.x, -self.y, -self.z, self.w, -self.z, self.y, self.z, self.w, -self.x, -self.y,
self.x, self.w,
)
}
#[deprecated(since = "0.7.0", note = "use uvec_angle_rad")]
pub fn uvec_angle(&self) -> (Vector3, f64) {
self.uvec_angle_rad()
}
pub fn uvec_angle_rad(&self) -> (Vector3, f64) {
let half_angle_rad = self.w.acos();
if half_angle_rad.abs() < EPSILON {
(Vector3::zeros(), 2.0 * half_angle_rad)
} else {
let prv = Vector3::new(self.x, self.y, self.z) / half_angle_rad.sin();
(prv / prv.norm(), 2.0 * half_angle_rad)
}
}
pub fn prv(&self) -> Vector3 {
let (uvec, angle) = self.uvec_angle_rad();
angle * uvec
}
pub fn as_vector(&self) -> Vector4 {
Vector4::new(self.w, self.x, self.y, self.z)
}
}
#[cfg_attr(feature = "python", pymethods)]
impl EulerParameter {
pub fn conjugate(&self) -> Self {
Self {
w: self.w,
x: -self.x,
y: -self.y,
z: -self.z,
from: self.to,
to: self.from,
}
}
pub fn is_zero(&self) -> bool {
self.w.abs() < EPSILON || (1.0 - self.w.abs()) < EPSILON
}
pub fn scalar_norm(&self) -> f64 {
(self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
pub fn normalize(&self) -> Self {
let norm = self.scalar_norm();
let mut me = *self;
me.w /= norm;
me.x /= norm;
me.y /= norm;
me.z /= norm;
me
}
pub fn short(&self) -> Self {
if self.w < 0.0 {
Self {
w: -self.w,
x: -self.x,
y: -self.y,
z: -self.z,
from: self.from,
to: self.to,
}
} else {
*self
}
}
}
impl Mul for Quaternion {
type Output = Result<Self, PhysicsError>;
fn mul(self, rhs: Quaternion) -> Self::Output {
ensure!(
self.from == rhs.to,
InvalidRotationSnafu {
action: "multiply quaternions",
from1: self.from,
to1: self.to,
from2: rhs.from,
to2: rhs.to
}
);
let p = rhs; let q = self;
let s = p.w * q.w - p.x * q.x - p.y * q.y - p.z * q.z;
let i = p.w * q.x + p.x * q.w + p.y * q.z - p.z * q.y;
let j = p.w * q.y - p.x * q.z + p.y * q.w + p.z * q.x;
let k = p.w * q.z + p.x * q.y - p.y * q.x + p.z * q.w;
Ok(Quaternion {
w: s,
x: i,
y: j,
z: k,
from: rhs.from, to: self.to, })
}
}
impl Mul for &Quaternion {
type Output = Result<Quaternion, PhysicsError>;
fn mul(self, other: &Quaternion) -> Result<Quaternion, PhysicsError> {
*self * *other
}
}
impl Mul<Vector3> for Quaternion {
type Output = Vector3;
fn mul(self, rhs: Vector3) -> Self::Output {
let r = Vector3::new(self.x, self.y, self.z);
let t = 2.0 * r.cross(&rhs);
rhs - (self.w * t) + r.cross(&t)
}
}
impl PartialEq for Quaternion {
fn eq(&self, other: &Self) -> bool {
if self.to == other.to && self.from == other.from {
if (self.w - other.w).abs() < 1e-12 && (self.w - 1.0).abs() < 1e-12 {
true
} else {
let (self_uvec, self_angle) = self.uvec_angle_rad();
let (other_uvec, other_angle) = other.uvec_angle_rad();
(self_angle - other_angle).abs() < EPSILON_RAD
&& (self_uvec - other_uvec).norm() <= 1e-12
}
} else {
false
}
}
}
impl fmt::Display for EulerParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"Euler Parameter {} -> {} = [w = {:1.6}, {:1.6}, {:1.6}, {:1.6}]",
self.from, self.to, self.w, self.x, self.y, self.z
)
}
}
impl Default for EulerParameter {
fn default() -> Self {
Self::identity(0, 0)
}
}
impl<'a> Decode<'a> for EulerParameter {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let from = decoder.decode()?;
let to = decoder.decode()?;
let w = decoder.decode()?;
let x = decoder.decode()?;
let y = decoder.decode()?;
let z = decoder.decode()?;
Ok(Self {
w,
x,
y,
z,
from,
to,
})
}
}
impl Encode for EulerParameter {
fn encoded_len(&self) -> der::Result<der::Length> {
self.from.encoded_len()?
+ self.to.encoded_len()?
+ self.w.encoded_len()?
+ self.x.encoded_len()?
+ self.y.encoded_len()?
+ self.z.encoded_len()?
}
fn encode(&self, encoder: &mut impl Writer) -> der::Result<()> {
self.from.encode(encoder)?;
self.to.encode(encoder)?;
self.w.encode(encoder)?;
self.x.encode(encoder)?;
self.y.encode(encoder)?;
self.z.encode(encoder)
}
}
impl DataSetT for EulerParameter {
const NAME: &'static str = "euler parameter";
}
#[cfg(test)]
mod ut_quaternion {
use crate::math::{
rotation::{generate_angles, vec3_eq, DCM},
Vector4,
};
use super::{EulerParameter, Quaternion, Vector3, EPSILON};
use core::f64::consts::{FRAC_PI_2, PI};
#[test]
fn test_quat_frames() {
for angle in generate_angles() {
for (i, q) in [
Quaternion::about_x(angle, 0, 1),
Quaternion::about_y(angle, 0, 1),
Quaternion::about_z(angle, 0, 1),
]
.iter()
.enumerate()
{
assert!((q * q).is_err());
assert!((q * &q.conjugate()).is_ok());
assert_eq!(
(q * &q.conjugate()).unwrap(),
Quaternion::identity(1, 1),
"axis {i} and {angle}"
);
let prv = q.prv();
assert!((prv[i] - angle).abs() < EPSILON, "{} with {angle}", prv[i]);
assert!(
(prv.norm() - angle.abs()).abs() < EPSILON,
"{prv} with {angle}"
);
}
}
}
#[test]
fn test_quat_start_end_frames() {
for angle in generate_angles() {
let q1 = Quaternion::about_x(angle, 0, 1);
let (uvec_q1, _angle_rad) = q1.uvec_angle_rad();
let q2 = Quaternion::about_x(angle, 1, 2);
let q1_to_q2 = (q2 * q1).unwrap();
assert_eq!(q1_to_q2.from, 0, "{angle}");
assert_eq!(q1_to_q2.to, 2, "{angle}");
let (uvec, angle_rad) = q1_to_q2.uvec_angle_rad();
if uvec.norm() > EPSILON {
if !(-PI..=PI).contains(&angle) {
assert_eq!(uvec, -uvec_q1, "{angle}");
} else {
assert_eq!(uvec, uvec_q1, "{angle}");
let cmp_angle = (2.0 * angle).abs();
assert!(
(angle_rad - cmp_angle).abs() < 1e-12,
"got: {angle_rad}\twant: {cmp_angle} (orig: {angle})"
);
}
}
let q2_to_q1 = (q1.conjugate() * q2.conjugate()).unwrap();
assert_eq!(q2_to_q1.from, 2, "{angle}");
assert_eq!(q2_to_q1.to, 0, "{angle}");
let (uvec, _angle_rad) = q2_to_q1.uvec_angle_rad();
if uvec.norm() > EPSILON {
if (-PI..=PI).contains(&angle) {
assert_eq!(uvec, -uvec_q1, "{angle}");
} else {
assert_eq!(uvec, uvec_q1, "{angle}");
}
}
}
}
#[test]
fn test_zero() {
let z = EulerParameter::identity(0, 1);
assert!(z.is_zero());
let c = DCM::identity(0, 1);
let q = Quaternion::from(c);
assert_eq!(c, q.into());
}
#[test]
fn test_derivative_zero_angular_velocity() {
let euler_params = EulerParameter::identity(0, 1);
let w = Vector3::new(0.0, 0.0, 0.0);
let derivative = euler_params.derivative(w);
assert!(derivative.is_zero());
}
#[test]
fn test_dcm_recip() {
for angle in generate_angles() {
let c_x = DCM::r1(angle, 0, 1);
let q_x = Quaternion::about_x(angle, 0, 1);
println!("{q_x} for {:.2} deg", angle.to_degrees());
vec3_eq(
DCM::from(q_x) * Vector3::x(),
c_x * Vector3::x(),
format!("X on {}", angle.to_degrees()),
);
vec3_eq(
q_x * Vector3::x(),
c_x * Vector3::x(),
format!("X on {}", angle.to_degrees()),
);
vec3_eq(
q_x * Vector3::y(),
c_x * Vector3::y(),
format!("Y on {}", angle.to_degrees()),
);
vec3_eq(
DCM::from(q_x) * Vector3::z(),
c_x * Vector3::z(),
format!("Z on {}", angle.to_degrees()),
);
}
}
#[test]
fn test_single_axis_rotations() {
let q_x = Quaternion::about_x(FRAC_PI_2, 0, 1);
assert!(
(q_x.as_vector() - Vector4::new(0.5_f64.sqrt(), 0.5_f64.sqrt(), 0.0, 0.0)).norm()
< EPSILON
);
assert_eq!(q_x * Vector3::x(), Vector3::x());
let d = DCM::from(q_x);
assert_eq!(d * Vector3::y(), q_x * Vector3::y());
assert!((d * Vector3::y() - -Vector3::z()).norm() < 1e-12);
assert!((q_x * Vector3::y() - -Vector3::z()).norm() < 1e-12);
let q_y = Quaternion::about_y(FRAC_PI_2, 0, 1);
assert_eq!(q_y * Vector3::y(), Vector3::y());
let d = DCM::from(q_y);
assert_eq!(d * Vector3::z(), q_y * Vector3::z());
assert!((d * Vector3::x() - Vector3::z()).norm() < 1e-12);
assert!((q_y * Vector3::x() - Vector3::z()).norm() < 1e-12);
let q_z = Quaternion::about_z(FRAC_PI_2, 0, 1);
assert_eq!(q_z * Vector3::z(), Vector3::z());
let d = DCM::from(q_z);
assert_eq!(d * Vector3::x(), q_z * Vector3::x());
}
use der::{Decode, Encode};
#[test]
fn ep_encdec_min_repr() {
let repr = EulerParameter {
from: -123,
to: 345,
w: 0.1,
x: 0.2,
y: 0.2,
z: 0.2,
}
.normalize();
let mut buf = vec![];
repr.encode_to_vec(&mut buf).unwrap();
let repr_dec = EulerParameter::from_der(&buf).unwrap();
assert_eq!(repr, repr_dec);
}
#[test]
fn test_derivative_values() {
let q = EulerParameter::identity(0, 1);
let w = Vector3::new(1.0, 0.0, 0.0);
let dq = q.derivative(w);
assert!((dq.w - 0.0).abs() < EPSILON);
assert!((dq.x - 0.5).abs() < EPSILON);
assert!((dq.y - 0.0).abs() < EPSILON);
assert!((dq.z - 0.0).abs() < EPSILON);
let q = Quaternion::about_z(PI, 0, 1);
let w = Vector3::new(0.0, 0.0, 1.0);
let dq = q.derivative(w);
assert!((dq.w + 0.5).abs() < EPSILON);
assert!((dq.x - 0.0).abs() < EPSILON);
assert!((dq.y - 0.0).abs() < EPSILON);
assert!((dq.z - 0.0).abs() < EPSILON);
}
#[test]
fn test_short_rotation() {
let q = Quaternion::about_x(3.0 * FRAC_PI_2, 0, 1);
assert!(q.w < 0.0);
let q_short = q.short();
assert!(q_short.w > 0.0);
let v = Vector3::new(0.0, 1.0, 0.0);
let v1 = q * v;
let v2 = q_short * v;
assert!((v1 - v2).norm() < EPSILON);
}
#[test]
fn test_normalization_explicit() {
let q = EulerParameter {
w: 10.0,
x: 0.0,
y: 0.0,
z: 0.0,
from: 0,
to: 1,
};
let q_norm = q.normalize();
assert!((q_norm.w - 1.0).abs() < EPSILON);
assert!((q_norm.scalar_norm() - 1.0).abs() < EPSILON);
let q = EulerParameter {
w: 1.0,
x: 1.0,
y: 1.0,
z: 1.0,
from: 0,
to: 1,
}; let q_norm = q.normalize();
assert!((q_norm.scalar_norm() - 1.0).abs() < EPSILON);
assert!((q_norm.w - 0.5).abs() < EPSILON);
}
#[test]
fn test_vector_rotation_arbitrary() {
let q = Quaternion::about_z(FRAC_PI_2, 0, 1);
let v = Vector3::x();
let rotated = q * v;
assert!((rotated + Vector3::y()).norm() < EPSILON);
let v = Vector3::y();
let rotated = q * v;
assert!((rotated - Vector3::x()).norm() < EPSILON);
}
}