use core::marker::PhantomData;
use astrodyn_math::JeodQuat;
use astrodyn_quantities::aliases::{AngularVelocity, Position, Velocity};
use astrodyn_quantities::frame::Frame;
use astrodyn_quantities::quat::{LeftTransform, NormalizedQuat, ScalarFirst};
use glam::{DMat3, DVec3};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefFrameTrans {
pub position: DVec3, pub velocity: DVec3, }
impl Default for RefFrameTrans {
fn default() -> Self {
Self {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefFrameRot {
pub q_parent_this: JeodQuat,
pub t_parent_this: DMat3,
pub ang_vel_this: DVec3,
}
impl Default for RefFrameRot {
fn default() -> Self {
Self {
q_parent_this: JeodQuat::identity(),
t_parent_this: DMat3::IDENTITY,
ang_vel_this: DVec3::ZERO,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct RefFrameState {
pub trans: RefFrameTrans,
pub rot: RefFrameRot,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RefFrameInfo {
pub name: String,
pub kind: RefFrameKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefFrameKind {
Inertial,
PlanetFixed,
Body,
}
impl RefFrameState {
pub fn negate(source: &RefFrameState) -> RefFrameState {
let mut q_new = source.rot.q_parent_this.conjugate();
q_new.normalize();
let t_new = q_new.left_quat_to_transformation();
let ang_vel_new = -(t_new * source.rot.ang_vel_this);
let pos_new = -(source.rot.t_parent_this * source.trans.position);
let t_vel = source.rot.t_parent_this * source.trans.velocity;
let vel_new = -source.rot.ang_vel_this.cross(pos_new) - t_vel;
RefFrameState {
trans: RefFrameTrans {
position: pos_new,
velocity: vel_new,
},
rot: RefFrameRot {
q_parent_this: q_new,
t_parent_this: t_new,
ang_vel_this: ang_vel_new,
},
}
}
pub fn incr_right(&self, s_bc: &RefFrameState) -> RefFrameState {
let mut q_ac = s_bc.rot.q_parent_this.multiply(&self.rot.q_parent_this);
q_ac.normalize();
let t_ac = q_ac.left_quat_to_transformation();
let ang_vel_ac = s_bc.rot.t_parent_this * self.rot.ang_vel_this + s_bc.rot.ang_vel_this;
let pos_ac = self.trans.position + self.rot.t_parent_this.transpose() * s_bc.trans.position;
let omega_cross_pos = self.rot.ang_vel_this.cross(s_bc.trans.position);
let vel_ac = self.trans.velocity
+ self.rot.t_parent_this.transpose() * (s_bc.trans.velocity + omega_cross_pos);
RefFrameState {
trans: RefFrameTrans {
position: pos_ac,
velocity: vel_ac,
},
rot: RefFrameRot {
q_parent_this: q_ac,
t_parent_this: t_ac,
ang_vel_this: ang_vel_ac,
},
}
}
pub fn incr_left(&mut self, s_ab: &RefFrameState) {
let result = s_ab.incr_right(self);
*self = result;
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefFrameTransTyped<P: Frame> {
pub position: Position<P>,
pub velocity: Velocity<P>,
}
impl<P: Frame> Default for RefFrameTransTyped<P> {
#[inline]
fn default() -> Self {
Self {
position: Position::<P>::zero(),
velocity: Velocity::<P>::zero(),
}
}
}
impl<P: Frame> RefFrameTransTyped<P> {
#[inline]
pub fn to_untyped(&self) -> RefFrameTrans {
RefFrameTrans {
position: self.position.raw_si(),
velocity: self.velocity.raw_si(),
}
}
#[inline]
pub fn from_untyped_unchecked(t: &RefFrameTrans) -> Self {
Self {
position: Position::<P>::from_raw_si(t.position),
velocity: Velocity::<P>::from_raw_si(t.velocity),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefFrameRotTyped<P: Frame, C: Frame> {
q_parent_this: NormalizedQuat<ScalarFirst, LeftTransform>,
t_parent_this: DMat3,
ang_vel_this: AngularVelocity<C>,
_p: PhantomData<P>,
}
impl<P: Frame, C: Frame> RefFrameRotTyped<P, C> {
#[inline]
pub fn new(
q_parent_this: NormalizedQuat<ScalarFirst, LeftTransform>,
ang_vel_this: AngularVelocity<C>,
) -> Self {
let t_parent_this = q_parent_this.left_quat_to_transformation();
Self {
q_parent_this,
t_parent_this,
ang_vel_this,
_p: PhantomData,
}
}
#[inline]
pub fn q_parent_this(&self) -> NormalizedQuat<ScalarFirst, LeftTransform> {
self.q_parent_this
}
#[inline]
pub fn t_parent_this(&self) -> DMat3 {
self.t_parent_this
}
#[inline]
pub fn ang_vel_this(&self) -> AngularVelocity<C> {
self.ang_vel_this
}
#[inline]
pub fn to_untyped(&self) -> RefFrameRot {
RefFrameRot {
q_parent_this: self.q_parent_this.inner(),
t_parent_this: self.t_parent_this,
ang_vel_this: self.ang_vel_this.raw_si(),
}
}
#[inline]
pub fn from_untyped_unchecked(r: &RefFrameRot) -> Self {
let q = NormalizedQuat::new(r.q_parent_this).unwrap_or_else(|err| {
panic!("RefFrameRotTyped::from_untyped_unchecked: quaternion is not unit-norm: {err}")
});
Self::new(q, AngularVelocity::<C>::from_raw_si(r.ang_vel_this))
}
}
impl<F: Frame> Default for RefFrameRotTyped<F, F> {
#[inline]
fn default() -> Self {
let q =
NormalizedQuat::new(JeodQuat::identity()).expect("identity quaternion is unit-norm");
Self {
q_parent_this: q,
t_parent_this: DMat3::IDENTITY,
ang_vel_this: AngularVelocity::<F>::zero(),
_p: PhantomData,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefFrameStateTyped<P: Frame, C: Frame> {
pub trans: RefFrameTransTyped<P>,
pub rot: RefFrameRotTyped<P, C>,
}
impl<F: Frame> Default for RefFrameStateTyped<F, F> {
#[inline]
fn default() -> Self {
Self {
trans: RefFrameTransTyped::<F>::default(),
rot: RefFrameRotTyped::<F, F>::default(),
}
}
}
impl<P: Frame, C: Frame> RefFrameStateTyped<P, C> {
#[inline]
pub fn new(trans: RefFrameTransTyped<P>, rot: RefFrameRotTyped<P, C>) -> Self {
Self { trans, rot }
}
pub fn to_untyped(&self) -> RefFrameState {
RefFrameState {
trans: RefFrameTrans {
position: self.trans.position.raw_si(),
velocity: self.trans.velocity.raw_si(),
},
rot: RefFrameRot {
q_parent_this: self.rot.q_parent_this.inner(),
t_parent_this: self.rot.t_parent_this,
ang_vel_this: self.rot.ang_vel_this.raw_si(),
},
}
}
pub fn from_untyped_unchecked(s: &RefFrameState) -> Self {
let q_norm = NormalizedQuat::new(s.rot.q_parent_this)
.unwrap_or_else(|err| panic!("RefFrameState quaternion is not unit-norm: {err}"));
Self {
trans: RefFrameTransTyped {
position: Position::<P>::from_raw_si(s.trans.position),
velocity: Velocity::<P>::from_raw_si(s.trans.velocity),
},
rot: RefFrameRotTyped::new(
q_norm,
AngularVelocity::<C>::from_raw_si(s.rot.ang_vel_this),
),
}
}
pub fn negate(&self) -> RefFrameStateTyped<C, P> {
let untyped_neg = RefFrameState::negate(&self.to_untyped());
RefFrameStateTyped::<C, P>::from_untyped_unchecked(&untyped_neg)
}
pub fn incr_right<D: Frame>(
&self,
s_cd: &RefFrameStateTyped<C, D>,
) -> RefFrameStateTyped<P, D> {
let composed_untyped = self.to_untyped().incr_right(&s_cd.to_untyped());
RefFrameStateTyped::<P, D>::from_untyped_unchecked(&composed_untyped)
}
}
#[cfg(test)]
mod tests {
use super::*;
use astrodyn_math::test_utils::{approx_eq_f64, approx_eq_mat3, approx_eq_vec3};
use astrodyn_math::JeodQuat;
use glam::{DMat3, DVec3};
use std::f64::consts::FRAC_PI_2;
const TOL: f64 = 1e-12;
fn make_state(angle_z: f64, pos: DVec3, vel: DVec3, ang_vel: DVec3) -> RefFrameState {
let q = JeodQuat::left_quat_from_eigen_rotation(angle_z, DVec3::Z);
let t = q.left_quat_to_transformation();
RefFrameState {
trans: RefFrameTrans {
position: pos,
velocity: vel,
},
rot: RefFrameRot {
q_parent_this: q,
t_parent_this: t,
ang_vel_this: ang_vel,
},
}
}
fn make_state_axis(
angle: f64,
axis: DVec3,
pos: DVec3,
vel: DVec3,
ang_vel: DVec3,
) -> RefFrameState {
let q = JeodQuat::left_quat_from_eigen_rotation(angle, axis);
let t = q.left_quat_to_transformation();
RefFrameState {
trans: RefFrameTrans {
position: pos,
velocity: vel,
},
rot: RefFrameRot {
q_parent_this: q,
t_parent_this: t,
ang_vel_this: ang_vel,
},
}
}
#[test]
fn compose_identity_left() {
let s = make_state(
0.5,
DVec3::new(1e6, 2e6, 3e6),
DVec3::new(100.0, 200.0, 300.0),
DVec3::new(0.01, 0.02, 0.03),
);
let identity = RefFrameState::default();
let result = identity.incr_right(&s);
assert!(
approx_eq_vec3(result.trans.position, s.trans.position, TOL),
"Position mismatch: {:?} vs {:?}",
result.trans.position,
s.trans.position
);
assert!(
approx_eq_vec3(result.trans.velocity, s.trans.velocity, TOL),
"Velocity mismatch: {:?} vs {:?}",
result.trans.velocity,
s.trans.velocity
);
assert!(
approx_eq_mat3(&result.rot.t_parent_this, &s.rot.t_parent_this, TOL),
"T mismatch"
);
assert!(
approx_eq_vec3(result.rot.ang_vel_this, s.rot.ang_vel_this, TOL),
"Ang vel mismatch"
);
}
#[test]
fn compose_identity_right() {
let s = make_state(
0.5,
DVec3::new(1e6, 2e6, 3e6),
DVec3::new(100.0, 200.0, 300.0),
DVec3::new(0.01, 0.02, 0.03),
);
let identity = RefFrameState::default();
let result = s.incr_right(&identity);
assert!(
approx_eq_vec3(result.trans.position, s.trans.position, TOL),
"Position mismatch: {:?} vs {:?}",
result.trans.position,
s.trans.position
);
assert!(
approx_eq_vec3(result.trans.velocity, s.trans.velocity, TOL),
"Velocity mismatch: {:?} vs {:?}",
result.trans.velocity,
s.trans.velocity
);
assert!(
approx_eq_mat3(&result.rot.t_parent_this, &s.rot.t_parent_this, TOL),
"T mismatch"
);
assert!(
approx_eq_vec3(result.rot.ang_vel_this, s.rot.ang_vel_this, TOL),
"Ang vel mismatch"
);
}
#[test]
fn compose_with_negate_gives_identity() {
let s = make_state(
1.2,
DVec3::new(5e6, -3e6, 1e6),
DVec3::new(500.0, -300.0, 100.0),
DVec3::new(0.05, -0.02, 0.01),
);
let s_neg = RefFrameState::negate(&s);
let result = s.incr_right(&s_neg);
assert!(
approx_eq_vec3(result.trans.position, DVec3::ZERO, 1e-6),
"Position should be ~0, got {:?}",
result.trans.position
);
assert!(
approx_eq_vec3(result.trans.velocity, DVec3::ZERO, 1e-6),
"Velocity should be ~0, got {:?}",
result.trans.velocity
);
assert!(
approx_eq_mat3(&result.rot.t_parent_this, &DMat3::IDENTITY, 1e-10),
"T should be ~I, got {:?}",
result.rot.t_parent_this
);
assert!(
approx_eq_vec3(result.rot.ang_vel_this, DVec3::ZERO, 1e-10),
"Ang vel should be ~0, got {:?}",
result.rot.ang_vel_this
);
}
#[test]
fn negate_with_compose_gives_identity_reversed() {
let s = make_state_axis(
0.8,
DVec3::new(1.0, 1.0, 1.0).normalize(),
DVec3::new(1e7, 0.0, 0.0),
DVec3::new(1000.0, 2000.0, -500.0),
DVec3::new(0.0, 0.0, 7.292e-5), );
let s_neg = RefFrameState::negate(&s);
let result = s_neg.incr_right(&s);
assert!(
approx_eq_vec3(result.trans.position, DVec3::ZERO, 1e-4),
"Position should be ~0, got {:?}",
result.trans.position
);
assert!(
approx_eq_vec3(result.trans.velocity, DVec3::ZERO, 1e-4),
"Velocity should be ~0, got {:?}",
result.trans.velocity
);
assert!(
approx_eq_mat3(&result.rot.t_parent_this, &DMat3::IDENTITY, 1e-10),
"T should be ~I, got {:?}",
result.rot.t_parent_this
);
assert!(
approx_eq_vec3(result.rot.ang_vel_this, DVec3::ZERO, 1e-10),
"Ang vel should be ~0, got {:?}",
result.rot.ang_vel_this
);
}
#[test]
fn double_negate_is_identity_operation() {
let s = make_state(
0.7,
DVec3::new(2e6, 4e6, -1e6),
DVec3::new(300.0, -150.0, 75.0),
DVec3::new(0.01, 0.005, -0.003),
);
let s_neg = RefFrameState::negate(&s);
let s_double_neg = RefFrameState::negate(&s_neg);
assert!(
approx_eq_vec3(s_double_neg.trans.position, s.trans.position, 1e-6),
"Double negate position: {:?} vs {:?}",
s_double_neg.trans.position,
s.trans.position
);
assert!(
approx_eq_vec3(s_double_neg.trans.velocity, s.trans.velocity, 1e-6),
"Double negate velocity: {:?} vs {:?}",
s_double_neg.trans.velocity,
s.trans.velocity
);
assert!(
approx_eq_mat3(&s_double_neg.rot.t_parent_this, &s.rot.t_parent_this, 1e-10),
"Double negate T"
);
assert!(
approx_eq_vec3(s_double_neg.rot.ang_vel_this, s.rot.ang_vel_this, 1e-10),
"Double negate ang_vel"
);
}
#[test]
fn three_frame_chain() {
let s_ab = make_state(
FRAC_PI_2,
DVec3::new(1000.0, 0.0, 0.0),
DVec3::ZERO,
DVec3::ZERO,
);
let s_bc = make_state(0.0, DVec3::new(500.0, 0.0, 0.0), DVec3::ZERO, DVec3::ZERO);
let s_ac = s_ab.incr_right(&s_bc);
assert!(
approx_eq_mat3(&s_ac.rot.t_parent_this, &s_ab.rot.t_parent_this, TOL),
"T_AC should equal T_AB since T_BC=I"
);
let expected_pos = DVec3::new(1000.0, 500.0, 0.0);
assert!(
approx_eq_vec3(s_ac.trans.position, expected_pos, TOL),
"Position A->C: expected {:?}, got {:?}",
expected_pos,
s_ac.trans.position
);
}
#[test]
fn three_frame_chain_with_velocity() {
let omega_ab = DVec3::new(0.0, 0.0, 0.1); let s_ab = RefFrameState {
trans: RefFrameTrans {
position: DVec3::new(1000.0, 0.0, 0.0),
velocity: DVec3::new(10.0, 0.0, 0.0),
},
rot: {
let q = JeodQuat::left_quat_from_eigen_rotation(FRAC_PI_2, DVec3::Z);
let t = q.left_quat_to_transformation();
RefFrameRot {
q_parent_this: q,
t_parent_this: t,
ang_vel_this: omega_ab,
}
},
};
let s_bc = RefFrameState {
trans: RefFrameTrans {
position: DVec3::new(500.0, 0.0, 0.0),
velocity: DVec3::new(5.0, 0.0, 0.0),
},
rot: RefFrameRot::default(),
};
let s_ac = s_ab.incr_right(&s_bc);
let expected_vel = DVec3::new(-40.0, 5.0, 0.0);
assert!(
approx_eq_vec3(s_ac.trans.velocity, expected_vel, 1e-10),
"Velocity A->C: expected {:?}, got {:?}",
expected_vel,
s_ac.trans.velocity
);
}
#[test]
fn incr_left_matches_incr_right() {
let s_ab = make_state(
0.3,
DVec3::new(1e6, 2e6, 0.0),
DVec3::new(100.0, 50.0, 0.0),
DVec3::new(0.0, 0.0, 0.01),
);
let s_bc = make_state(
-0.7,
DVec3::new(5e5, 0.0, 1e5),
DVec3::new(20.0, 10.0, 5.0),
DVec3::new(0.001, 0.0, 0.002),
);
let result_right = s_ab.incr_right(&s_bc);
let mut s_bc_copy = s_bc;
s_bc_copy.incr_left(&s_ab);
assert!(
approx_eq_vec3(s_bc_copy.trans.position, result_right.trans.position, TOL),
"incr_left position mismatch"
);
assert!(
approx_eq_vec3(s_bc_copy.trans.velocity, result_right.trans.velocity, TOL),
"incr_left velocity mismatch"
);
assert!(
approx_eq_mat3(
&s_bc_copy.rot.t_parent_this,
&result_right.rot.t_parent_this,
TOL
),
"incr_left T mismatch"
);
assert!(
approx_eq_vec3(
s_bc_copy.rot.ang_vel_this,
result_right.rot.ang_vel_this,
TOL
),
"incr_left ang_vel mismatch"
);
}
#[test]
fn negate_identity_is_identity() {
let identity = RefFrameState::default();
let neg = RefFrameState::negate(&identity);
assert!(
approx_eq_vec3(neg.trans.position, DVec3::ZERO, TOL),
"Negate identity position"
);
assert!(
approx_eq_vec3(neg.trans.velocity, DVec3::ZERO, TOL),
"Negate identity velocity"
);
assert!(
approx_eq_mat3(&neg.rot.t_parent_this, &DMat3::IDENTITY, TOL),
"Negate identity T"
);
assert!(
approx_eq_vec3(neg.rot.ang_vel_this, DVec3::ZERO, TOL),
"Negate identity ang_vel"
);
}
#[test]
fn negate_pure_translation() {
let s = RefFrameState {
trans: RefFrameTrans {
position: DVec3::new(1000.0, 2000.0, 3000.0),
velocity: DVec3::new(10.0, 20.0, 30.0),
},
rot: RefFrameRot::default(), };
let neg = RefFrameState::negate(&s);
assert!(
approx_eq_vec3(
neg.trans.position,
DVec3::new(-1000.0, -2000.0, -3000.0),
TOL
),
"Negate pure translation position"
);
assert!(
approx_eq_vec3(neg.trans.velocity, DVec3::new(-10.0, -20.0, -30.0), TOL),
"Negate pure translation velocity"
);
}
#[test]
fn negate_pure_rotation() {
let q = JeodQuat::left_quat_from_eigen_rotation(1.0, DVec3::new(1.0, 1.0, 1.0).normalize());
let t = q.left_quat_to_transformation();
let s = RefFrameState {
trans: RefFrameTrans::default(),
rot: RefFrameRot {
q_parent_this: q,
t_parent_this: t,
ang_vel_this: DVec3::new(0.01, 0.02, 0.03),
},
};
let neg = RefFrameState::negate(&s);
let product = neg.rot.t_parent_this * s.rot.t_parent_this;
assert!(
approx_eq_mat3(&product, &DMat3::IDENTITY, TOL),
"T_neg * T should be I"
);
assert!(
approx_eq_vec3(neg.trans.position, DVec3::ZERO, TOL),
"Pure rotation negate should have zero position"
);
}
#[test]
fn composition_associativity() {
let s_ab = make_state_axis(
0.3,
DVec3::X,
DVec3::new(1e6, 0.0, 0.0),
DVec3::new(100.0, 0.0, 0.0),
DVec3::new(0.01, 0.0, 0.0),
);
let s_bc = make_state_axis(
0.5,
DVec3::Y,
DVec3::new(0.0, 5e5, 0.0),
DVec3::new(0.0, 50.0, 0.0),
DVec3::new(0.0, 0.02, 0.0),
);
let s_cd = make_state_axis(
-0.2,
DVec3::Z,
DVec3::new(0.0, 0.0, 3e5),
DVec3::new(0.0, 0.0, 30.0),
DVec3::new(0.0, 0.0, 0.005),
);
let s_ac = s_ab.incr_right(&s_bc);
let s_ad_left = s_ac.incr_right(&s_cd);
let s_bd = s_bc.incr_right(&s_cd);
let s_ad_right = s_ab.incr_right(&s_bd);
assert!(
approx_eq_vec3(s_ad_left.trans.position, s_ad_right.trans.position, 1e-6),
"Associativity position: {:?} vs {:?}",
s_ad_left.trans.position,
s_ad_right.trans.position
);
assert!(
approx_eq_vec3(s_ad_left.trans.velocity, s_ad_right.trans.velocity, 1e-6),
"Associativity velocity: {:?} vs {:?}",
s_ad_left.trans.velocity,
s_ad_right.trans.velocity
);
assert!(
approx_eq_mat3(
&s_ad_left.rot.t_parent_this,
&s_ad_right.rot.t_parent_this,
1e-10
),
"Associativity T"
);
assert!(
approx_eq_vec3(
s_ad_left.rot.ang_vel_this,
s_ad_right.rot.ang_vel_this,
1e-10
),
"Associativity ang_vel"
);
}
#[test]
fn default_state_is_identity() {
let s = RefFrameState::default();
assert_eq!(s.trans.position, DVec3::ZERO);
assert_eq!(s.trans.velocity, DVec3::ZERO);
assert_eq!(s.rot.t_parent_this, DMat3::IDENTITY);
assert_eq!(s.rot.ang_vel_this, DVec3::ZERO);
assert!(
approx_eq_f64(s.rot.q_parent_this.scalar(), 1.0, TOL),
"Default quaternion scalar"
);
assert!(
approx_eq_vec3(s.rot.q_parent_this.vector(), DVec3::ZERO, TOL),
"Default quaternion vector"
);
}
}
#[cfg(test)]
mod typed_tests {
use super::*;
use astrodyn_math::test_utils::{approx_eq_mat3, approx_eq_vec3};
use astrodyn_math::JeodQuat;
use astrodyn_quantities::frame::{Ecef, RootInertial};
use glam::{DMat3, DVec3};
use std::f64::consts::FRAC_PI_2;
const TOL: f64 = 1e-12;
fn make_state(angle_z: f64, pos: DVec3, vel: DVec3, ang_vel: DVec3) -> RefFrameState {
let q = JeodQuat::left_quat_from_eigen_rotation(angle_z, DVec3::Z);
let t = q.left_quat_to_transformation();
RefFrameState {
trans: RefFrameTrans {
position: pos,
velocity: vel,
},
rot: RefFrameRot {
q_parent_this: q,
t_parent_this: t,
ang_vel_this: ang_vel,
},
}
}
#[test]
fn untyped_to_typed_round_trip_is_identity() {
let s = make_state(
0.7,
DVec3::new(1e6, 2e6, 3e6),
DVec3::new(100.0, 200.0, 300.0),
DVec3::new(0.01, 0.02, 0.03),
);
let typed = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&s);
let back = typed.to_untyped();
assert_eq!(back.trans.position, s.trans.position);
assert_eq!(back.trans.velocity, s.trans.velocity);
assert_eq!(back.rot.q_parent_this, s.rot.q_parent_this);
assert_eq!(back.rot.t_parent_this, s.rot.t_parent_this);
assert_eq!(back.rot.ang_vel_this, s.rot.ang_vel_this);
}
#[test]
fn typed_negate_matches_untyped() {
let s = make_state(
1.2,
DVec3::new(5e6, -3e6, 1e6),
DVec3::new(500.0, -300.0, 100.0),
DVec3::new(0.05, -0.02, 0.01),
);
let typed = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&s);
let typed_neg: RefFrameStateTyped<Ecef, RootInertial> = typed.negate();
let untyped_neg = RefFrameState::negate(&s);
assert!(approx_eq_vec3(
typed_neg.trans.position.raw_si(),
untyped_neg.trans.position,
TOL
));
assert!(approx_eq_vec3(
typed_neg.trans.velocity.raw_si(),
untyped_neg.trans.velocity,
TOL
));
assert!(approx_eq_mat3(
&typed_neg.rot.t_parent_this,
&untyped_neg.rot.t_parent_this,
TOL
));
assert!(approx_eq_vec3(
typed_neg.rot.ang_vel_this.raw_si(),
untyped_neg.rot.ang_vel_this,
TOL
));
}
#[test]
fn typed_incr_right_matches_untyped() {
let s_ie = make_state(
FRAC_PI_2,
DVec3::new(1000.0, 0.0, 0.0),
DVec3::new(10.0, 0.0, 0.0),
DVec3::new(0.0, 0.0, 0.1),
);
let s_ei = make_state(
-0.4,
DVec3::new(0.0, 500.0, 0.0),
DVec3::new(0.0, 5.0, 0.0),
DVec3::new(0.001, 0.0, 0.0),
);
let typed_ie = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&s_ie);
let typed_ei = RefFrameStateTyped::<Ecef, RootInertial>::from_untyped_unchecked(&s_ei);
let composed_typed: RefFrameStateTyped<RootInertial, RootInertial> =
typed_ie.incr_right(&typed_ei);
let composed_untyped = s_ie.incr_right(&s_ei);
assert!(approx_eq_vec3(
composed_typed.trans.position.raw_si(),
composed_untyped.trans.position,
TOL
));
assert!(approx_eq_vec3(
composed_typed.trans.velocity.raw_si(),
composed_untyped.trans.velocity,
TOL
));
assert!(approx_eq_mat3(
&composed_typed.rot.t_parent_this,
&composed_untyped.rot.t_parent_this,
TOL
));
assert!(approx_eq_vec3(
composed_typed.rot.ang_vel_this.raw_si(),
composed_untyped.rot.ang_vel_this,
TOL
));
}
#[test]
fn typed_default_for_same_frame_is_identity() {
let id = RefFrameStateTyped::<RootInertial, RootInertial>::default();
let untyped = id.to_untyped();
assert_eq!(untyped.trans.position, DVec3::ZERO);
assert_eq!(untyped.trans.velocity, DVec3::ZERO);
assert_eq!(untyped.rot.t_parent_this, DMat3::IDENTITY);
assert_eq!(untyped.rot.ang_vel_this, DVec3::ZERO);
}
use astrodyn_quantities::quat::{LeftTransform, NormalizedQuat, ScalarFirst};
use proptest::prelude::*;
fn arb_finite_bounded() -> impl Strategy<Value = f64> {
prop_oneof![
(1.0e-9_f64..1.0e9_f64),
(1.0e-9_f64..1.0e9_f64).prop_map(|x| -x),
]
}
fn arb_dvec3_bounded() -> impl Strategy<Value = DVec3> {
(
arb_finite_bounded(),
arb_finite_bounded(),
arb_finite_bounded(),
)
.prop_map(|(x, y, z)| DVec3::new(x, y, z))
}
fn arb_unit_quat() -> impl Strategy<Value = NormalizedQuat<ScalarFirst, LeftTransform>> {
(
arb_finite_bounded(),
arb_finite_bounded(),
arb_finite_bounded(),
arb_finite_bounded(),
)
.prop_filter("non-zero norm", |(a, b, c, d)| {
let n2 = a * a + b * b + c * c + d * d;
n2.is_finite() && n2 > 1.0e-18
})
.prop_filter_map("renormalize succeeds", |(a, b, c, d)| {
NormalizedQuat::renormalize(JeodQuat::from_array([a, b, c, d]))
})
}
fn arb_ref_frame_trans() -> impl Strategy<Value = RefFrameTrans> {
(arb_dvec3_bounded(), arb_dvec3_bounded())
.prop_map(|(position, velocity)| RefFrameTrans { position, velocity })
}
fn arb_ref_frame_rot() -> impl Strategy<Value = RefFrameRot> {
(arb_unit_quat(), arb_dvec3_bounded()).prop_map(|(q, ang_vel_this)| {
let q_inner = q.inner();
let t = q_inner.left_quat_to_transformation();
RefFrameRot {
q_parent_this: q_inner,
t_parent_this: t,
ang_vel_this,
}
})
}
fn arb_ref_frame_state() -> impl Strategy<Value = RefFrameState> {
(arb_ref_frame_trans(), arb_ref_frame_rot())
.prop_map(|(trans, rot)| RefFrameState { trans, rot })
}
proptest! {
#[test]
fn round_trip_ref_frame_trans_untyped_typed_untyped(orig in arb_ref_frame_trans()) {
let typed = RefFrameTransTyped::<RootInertial>::from_untyped_unchecked(&orig);
prop_assert_eq!(typed.to_untyped(), orig);
}
#[test]
fn round_trip_ref_frame_trans_typed_untyped_typed(orig in arb_ref_frame_trans()) {
let typed = RefFrameTransTyped::<RootInertial>::from_untyped_unchecked(&orig);
let lifted = RefFrameTransTyped::<RootInertial>::from_untyped_unchecked(&typed.to_untyped());
prop_assert_eq!(lifted.to_untyped(), typed.to_untyped());
}
#[test]
fn round_trip_ref_frame_rot_untyped_typed_untyped(orig in arb_ref_frame_rot()) {
let typed = RefFrameRotTyped::<RootInertial, Ecef>::from_untyped_unchecked(&orig);
prop_assert_eq!(typed.to_untyped(), orig);
}
#[test]
fn round_trip_ref_frame_rot_typed_untyped_typed(orig in arb_ref_frame_rot()) {
let typed = RefFrameRotTyped::<RootInertial, Ecef>::from_untyped_unchecked(&orig);
let lifted = RefFrameRotTyped::<RootInertial, Ecef>::from_untyped_unchecked(&typed.to_untyped());
prop_assert_eq!(lifted.to_untyped(), typed.to_untyped());
}
#[test]
fn round_trip_ref_frame_state_untyped_typed_untyped(orig in arb_ref_frame_state()) {
let typed = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&orig);
prop_assert_eq!(typed.to_untyped(), orig);
}
#[test]
fn round_trip_ref_frame_state_typed_untyped_typed(orig in arb_ref_frame_state()) {
let typed = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&orig);
let lifted = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&typed.to_untyped());
prop_assert_eq!(lifted.to_untyped(), typed.to_untyped());
}
}
#[test]
fn typed_round_trip_through_negate_recovers_original() {
let s = make_state(
0.9,
DVec3::new(2e6, -1e6, 5e5),
DVec3::new(50.0, -25.0, 12.5),
DVec3::new(0.01, 0.0, 0.005),
);
let typed = RefFrameStateTyped::<RootInertial, Ecef>::from_untyped_unchecked(&s);
let twice_negated = typed.negate().negate();
let s_back = twice_negated.to_untyped();
assert!(approx_eq_vec3(
s_back.trans.position,
s.trans.position,
1e-6
));
assert!(approx_eq_vec3(
s_back.trans.velocity,
s.trans.velocity,
1e-6
));
assert!(approx_eq_mat3(
&s_back.rot.t_parent_this,
&s.rot.t_parent_this,
1e-10
));
}
}