use glam::{DQuat, DVec3};
use crate::{EulerSequence, LvlhFrame, OrbitalElements, RotationalState};
use astrodyn_math::OrbitalError;
use astrodyn_quantities::aliases::{Position, Velocity};
use astrodyn_quantities::dims::GravParam;
use astrodyn_quantities::ext::Vec3Ext;
use astrodyn_quantities::frame::{BodyFrame, Lvlh, RootInertial, SelfPlanet, Vehicle};
use uom::si::angle::radian;
use uom::si::f64::Angle;
#[derive(Debug, Clone)]
pub struct RelativeState<Subject: Vehicle, Reference: Vehicle> {
pub trans: RelativeTranslation<Reference>,
pub quaternion: DQuat,
pub ang_vel: astrodyn_quantities::aliases::AngularVelocity<BodyFrame<Subject>>,
}
impl<Subject: Vehicle, Reference: Vehicle> RelativeState<Subject, Reference> {
#[inline]
pub fn assert_pair<S: Vehicle, R: Vehicle>(self) -> Self
where
(): astrodyn_quantities::diagnostics::CompatibleVehiclePair<Subject, Reference, S, R>,
{
self
}
}
#[derive(Debug, Clone)]
pub enum RelativeTranslation<Reference: Vehicle> {
BodyFrame {
position: Position<BodyFrame<Reference>>,
velocity: Velocity<BodyFrame<Reference>>,
},
Inertial {
position: Position<RootInertial>,
velocity: Velocity<RootInertial>,
},
}
impl<Reference: Vehicle> RelativeTranslation<Reference> {
pub fn position_raw(&self) -> DVec3 {
match self {
Self::BodyFrame { position, .. } => position.raw_si(),
Self::Inertial { position, .. } => position.raw_si(),
}
}
pub fn velocity_raw(&self) -> DVec3 {
match self {
Self::BodyFrame { velocity, .. } => velocity.raw_si(),
Self::Inertial { velocity, .. } => velocity.raw_si(),
}
}
#[inline]
pub fn assert_reference<R: Vehicle>(self) -> Self
where
(): astrodyn_quantities::diagnostics::CompatibleVehicles<Reference, R>,
{
self
}
}
#[derive(Debug, Clone)]
pub struct LvlhRelativeState<Chief: Vehicle> {
pub position: Position<Lvlh<Chief>>,
pub velocity: Velocity<Lvlh<Chief>>,
}
impl<Chief: Vehicle> LvlhRelativeState<Chief> {
#[inline]
pub fn assert_chief<C: Vehicle>(self) -> Self
where
(): astrodyn_quantities::diagnostics::CompatibleVehicles<Chief, C>,
{
self
}
}
pub fn compute_orbital_elements(
mu: f64,
position: DVec3,
velocity: DVec3,
) -> Result<OrbitalElements<SelfPlanet>, OrbitalError> {
use astrodyn_quantities::ext::{F64Ext, Vec3Ext};
use astrodyn_quantities::frame::PlanetInertial;
OrbitalElements::<SelfPlanet>::from_cartesian_typed(
F64Ext::m3_per_s2(mu),
position.m_at::<PlanetInertial<SelfPlanet>>(),
velocity.m_per_s_at::<PlanetInertial<SelfPlanet>>(),
)
}
pub fn compute_body_euler_angles(rot: &RotationalState, sequence: EulerSequence) -> [f64; 3] {
let t_parent_body = rot.quaternion.left_quat_to_transformation();
let typed = astrodyn_math::compute_euler_angles_from_matrix_typed(&t_parent_body, sequence);
[
typed[0].get::<radian>(),
typed[1].get::<radian>(),
typed[2].get::<radian>(),
]
}
pub fn compute_body_lvlh_frame(position: DVec3, velocity: DVec3) -> LvlhFrame {
use astrodyn_quantities::ext::Vec3Ext;
use astrodyn_quantities::frame::{Earth, PlanetInertial};
LvlhFrame::compute(
position.m_at::<PlanetInertial<Earth>>(),
velocity.m_per_s_at::<PlanetInertial<Earth>>(),
)
}
pub use astrodyn_math::{compute_body_geodetic, compute_body_solar_beta};
pub fn compute_relative_state<Subject: Vehicle, Reference: Vehicle>(
ref_trans: &crate::TranslationalState,
ref_rot: Option<&RotationalState>,
subj_trans: &crate::TranslationalState,
subj_rot: Option<&RotationalState>,
) -> RelativeState<Subject, Reference> {
let rel_pos_inertial = subj_trans.position - ref_trans.position;
let rel_vel_inertial = subj_trans.velocity - ref_trans.velocity;
let (trans, t_ref_opt) = if let Some(r_ref) = ref_rot {
let t_ref = r_ref.quaternion.left_quat_to_transformation();
let pos = t_ref * rel_pos_inertial;
let vel = t_ref * rel_vel_inertial - r_ref.ang_vel_body.cross(pos);
(
RelativeTranslation::BodyFrame {
position: pos.m_at::<BodyFrame<Reference>>(),
velocity: vel.m_per_s_at::<BodyFrame<Reference>>(),
},
Some(t_ref),
)
} else {
(
RelativeTranslation::Inertial {
position: rel_pos_inertial.m_at::<RootInertial>(),
velocity: rel_vel_inertial.m_per_s_at::<RootInertial>(),
},
None,
)
};
let (quaternion, ang_vel) = match (ref_rot, subj_rot) {
(Some(r_ref), Some(r_subj)) => {
let q_ref = r_ref.quaternion.to_glam();
let q_subj = r_subj.quaternion.to_glam();
let q_rel = q_subj * q_ref.conjugate();
let t_subj = r_subj.quaternion.left_quat_to_transformation();
let t_ref = t_ref_opt.unwrap_or_else(|| r_ref.quaternion.left_quat_to_transformation());
let t_ref_to_subj = t_subj * t_ref.transpose();
let rel_ang_vel = r_subj.ang_vel_body - t_ref_to_subj * r_ref.ang_vel_body;
(q_rel, rel_ang_vel)
}
_ => (DQuat::IDENTITY, DVec3::ZERO),
};
RelativeState {
trans,
quaternion,
ang_vel: ang_vel.rad_per_s_at::<BodyFrame<Subject>>(),
}
}
pub fn compute_lvlh_relative_state<Chief: Vehicle>(
ref_pos: DVec3,
ref_vel: DVec3,
subj_pos: DVec3,
subj_vel: DVec3,
) -> LvlhRelativeState<Chief> {
let lvlh = compute_body_lvlh_frame(ref_pos, ref_vel);
let rel_pos_inertial = subj_pos - ref_pos;
let rel_vel_inertial = subj_vel - ref_vel;
let pos_lvlh = lvlh.t_parent_this * rel_pos_inertial;
let vel_lvlh = lvlh.t_parent_this * rel_vel_inertial - lvlh.ang_vel_this.cross(pos_lvlh);
LvlhRelativeState {
position: pos_lvlh.m_at::<Lvlh<Chief>>(),
velocity: vel_lvlh.m_per_s_at::<Lvlh<Chief>>(),
}
}
pub fn compute_lvlh_relative_state_typed<P: astrodyn_quantities::frame::Planet, Chief: Vehicle>(
ref_pos: Position<astrodyn_quantities::frame::PlanetInertial<P>>,
ref_vel: Velocity<astrodyn_quantities::frame::PlanetInertial<P>>,
subj_pos: Position<astrodyn_quantities::frame::PlanetInertial<P>>,
subj_vel: Velocity<astrodyn_quantities::frame::PlanetInertial<P>>,
) -> LvlhRelativeState<Chief> {
compute_lvlh_relative_state::<Chief>(
ref_pos.raw_si(),
ref_vel.raw_si(),
subj_pos.raw_si(),
subj_vel.raw_si(),
)
}
pub fn compute_orbital_elements_typed<P: astrodyn_quantities::frame::Planet>(
mu: GravParam<P>,
position: Position<astrodyn_quantities::frame::PlanetInertial<P>>,
velocity: Velocity<astrodyn_quantities::frame::PlanetInertial<P>>,
) -> Result<OrbitalElements<P>, OrbitalError> {
OrbitalElements::<P>::from_cartesian_typed(mu, position, velocity)
}
pub fn compute_body_euler_angles_typed(
rot: &RotationalState,
sequence: EulerSequence,
) -> [Angle; 3] {
let t_parent_body = rot.quaternion.left_quat_to_transformation();
astrodyn_math::compute_euler_angles_from_matrix_typed(&t_parent_body, sequence)
}
pub fn compute_body_lvlh_frame_typed<P: astrodyn_quantities::frame::Planet>(
position: Position<astrodyn_quantities::frame::PlanetInertial<P>>,
velocity: Velocity<astrodyn_quantities::frame::PlanetInertial<P>>,
) -> LvlhFrame {
LvlhFrame::compute(position, velocity)
}
pub use astrodyn_math::{compute_body_geodetic_typed, compute_body_solar_beta_typed};
#[cfg(test)]
mod tests {
use super::*;
use crate::GeodeticState;
use astrodyn_quantities::frame::SelfRef;
use glam::DMat3;
#[test]
fn geodetic_with_rotation() {
use std::f64::consts::FRAC_PI_2;
const R_EQ: f64 = 6_378_137.0;
const R_POL: f64 = R_EQ * (1.0 - 1.0 / 298.257_223_563);
let t_inertial_pfix = DMat3::from_cols(
DVec3::new(0.0, 1.0, 0.0), DVec3::new(-1.0, 0.0, 0.0), DVec3::new(0.0, 0.0, 1.0), );
let pos_inertial = DVec3::new(R_EQ + 408_000.0, 0.0, 0.0);
let geo = compute_body_geodetic(pos_inertial, &t_inertial_pfix, R_EQ, R_POL);
assert!(
(geo.longitude - FRAC_PI_2).abs() < 1e-10,
"expected longitude ≈ π/2, got {}",
geo.longitude
);
assert!(
geo.latitude.abs() < 1e-10,
"expected latitude ≈ 0, got {}",
geo.latitude
);
assert!(
(geo.altitude - 408_000.0).abs() < 1.0,
"expected altitude ≈ 408000 m, got {}",
geo.altitude
);
let pos_pfix = t_inertial_pfix * pos_inertial;
let expected = GeodeticState::from_planet_fixed(pos_pfix, R_EQ, R_POL);
assert_eq!(geo, expected);
}
#[test]
fn lvlh_relative_typed_round_trip() {
use astrodyn_quantities::ext::Vec3Ext;
use astrodyn_quantities::frame::{Earth, PlanetInertial};
let ref_pos = DVec3::new(6.778e6, 0.0, 0.0);
let ref_vel = DVec3::new(0.0, 7.668e3, 0.0);
let subj_pos = DVec3::new(6.778e6 + 50.0, 80.0, 10.0);
let subj_vel = DVec3::new(-0.05, 7.668e3 + 0.06, 0.0);
let raw = compute_lvlh_relative_state::<SelfRef>(ref_pos, ref_vel, subj_pos, subj_vel);
let typed = compute_lvlh_relative_state_typed::<Earth, SelfRef>(
ref_pos.m_at::<PlanetInertial<Earth>>(),
ref_vel.m_per_s_at::<PlanetInertial<Earth>>(),
subj_pos.m_at::<PlanetInertial<Earth>>(),
subj_vel.m_per_s_at::<PlanetInertial<Earth>>(),
);
assert_eq!(typed.position.raw_si(), raw.position.raw_si());
assert_eq!(typed.velocity.raw_si(), raw.velocity.raw_si());
let _: Position<astrodyn_quantities::frame::Lvlh<astrodyn_quantities::frame::SelfRef>> =
typed.position;
let _: Velocity<astrodyn_quantities::frame::Lvlh<astrodyn_quantities::frame::SelfRef>> =
typed.velocity;
}
#[test]
fn relative_state_variant_matches_reference_rotation() {
use astrodyn_math::JeodQuat;
use astrodyn_quantities::frame::RootInertial;
let trans_a = crate::TranslationalState {
position: DVec3::new(6_778_137.0, 0.0, 0.0),
velocity: DVec3::new(0.0, 7668.56, 0.0),
};
let trans_b = crate::TranslationalState {
position: DVec3::new(6_778_237.0, 100.0, -50.0),
velocity: DVec3::new(0.01, 7668.55, 0.005),
};
let rot_a = RotationalState {
quaternion: {
let mut q = JeodQuat::new(0.5_f64.sqrt(), 0.5, 0.0, 0.5_f64.sqrt() - 0.5);
q.normalize();
q
},
ang_vel_body: DVec3::new(0.001, -0.0005, 0.001),
};
let with_rot =
compute_relative_state::<SelfRef, SelfRef>(&trans_a, Some(&rot_a), &trans_b, None);
let RelativeTranslation::BodyFrame { position, velocity } = with_rot.trans else {
panic!("Some reference rotation must yield BodyFrame variant");
};
let _: Position<BodyFrame<SelfRef>> = position;
let _: astrodyn_quantities::aliases::Velocity<BodyFrame<SelfRef>> = velocity;
let no_rot = compute_relative_state::<SelfRef, SelfRef>(&trans_a, None, &trans_b, None);
let RelativeTranslation::Inertial { position, velocity } = no_rot.trans else {
panic!("None reference rotation must yield Inertial variant");
};
let _: Position<RootInertial> = position;
let _: astrodyn_quantities::aliases::Velocity<RootInertial> = velocity;
let no_rot_again =
compute_relative_state::<SelfRef, SelfRef>(&trans_a, None, &trans_b, None);
let raw_pos = no_rot_again.trans.position_raw();
let raw_vel = no_rot_again.trans.velocity_raw();
assert_eq!(raw_pos, trans_b.position - trans_a.position);
assert_eq!(raw_vel, trans_b.velocity - trans_a.velocity);
}
#[test]
fn relative_state_paired_phantoms_propagate() {
use astrodyn_math::JeodQuat;
use astrodyn_quantities::define_vehicle;
define_vehicle!(Subj);
define_vehicle!(Ref);
let trans_a = crate::TranslationalState {
position: DVec3::new(6_778_137.0, 0.0, 0.0),
velocity: DVec3::new(0.0, 7668.56, 0.0),
};
let trans_b = crate::TranslationalState {
position: DVec3::new(6_778_237.0, 100.0, -50.0),
velocity: DVec3::new(0.01, 7668.55, 0.005),
};
let rot_ref = RotationalState {
quaternion: {
let mut q = JeodQuat::new(0.5_f64.sqrt(), 0.5, 0.0, 0.5_f64.sqrt() - 0.5);
q.normalize();
q
},
ang_vel_body: DVec3::new(0.001, -0.0005, 0.001),
};
let rot_subj = RotationalState {
quaternion: JeodQuat::identity(),
ang_vel_body: DVec3::new(0.0, 0.0, 0.001),
};
let rel = compute_relative_state::<Subj, Ref>(
&trans_a,
Some(&rot_ref),
&trans_b,
Some(&rot_subj),
);
let _: astrodyn_quantities::aliases::AngularVelocity<BodyFrame<Subj>> = rel.ang_vel;
let RelativeTranslation::BodyFrame { position, velocity } = rel.trans else {
panic!("Some reference rotation must yield BodyFrame variant");
};
let _: Position<BodyFrame<Ref>> = position;
let _: astrodyn_quantities::aliases::Velocity<BodyFrame<Ref>> = velocity;
}
#[test]
fn vehicle_witness_methods_compile_when_phantoms_match() {
use astrodyn_math::JeodQuat;
use astrodyn_quantities::define_vehicle;
define_vehicle!(Iss);
define_vehicle!(Soyuz);
let trans = crate::TranslationalState {
position: DVec3::new(6_778_137.0, 0.0, 0.0),
velocity: DVec3::new(0.0, 7668.56, 0.0),
};
let rot = RotationalState {
quaternion: JeodQuat::identity(),
ang_vel_body: DVec3::ZERO,
};
let rel: RelativeState<Iss, Soyuz> =
compute_relative_state::<Iss, Soyuz>(&trans, Some(&rot), &trans, Some(&rot));
let rel = rel.assert_pair::<Iss, Soyuz>();
let _ = rel.trans.assert_reference::<Soyuz>();
let lvlh: LvlhRelativeState<Iss> = compute_lvlh_relative_state::<Iss>(
DVec3::new(6.778e6, 0.0, 0.0),
DVec3::new(0.0, 7.668e3, 0.0),
DVec3::new(6.778e6 + 50.0, 0.0, 0.0),
DVec3::new(0.0, 7.668e3, 0.0),
);
let _ = lvlh.assert_chief::<Iss>();
}
}