use astrodyn_math::JeodQuat;
use astrodyn_quantities::aliases::{AngularVelocity, Position, Velocity};
use astrodyn_quantities::body_attitude::BodyAttitude;
use astrodyn_quantities::frame::{BodyFrame, RootInertial, Vehicle};
use astrodyn_quantities::quat::NormalizedQuat;
use glam::{DMat3, DQuat, DVec3};
use crate::rotational::RotationalState;
use crate::state::TranslationalState;
#[derive(Debug, Clone, Copy)]
pub struct KinematicChildInputs {
pub parent_t_inertial_body: DMat3,
pub parent_ang_vel_body: DVec3,
pub parent_position_inertial: DVec3,
pub parent_velocity_inertial: DVec3,
pub parent_t_struct_body: DMat3,
pub parent_composite_in_pstr: DVec3,
pub t_parent_child: DMat3,
pub link_offset_in_pstr: DVec3,
pub child_t_struct_body: DMat3,
pub child_composite_in_cstr: DVec3,
}
#[derive(Debug, Clone, Copy)]
pub struct KinematicChildOutputs {
pub child_t_inertial_body: DMat3,
pub child_q_inertial_body: JeodQuat,
pub child_ang_vel_body: DVec3,
pub child_position_inertial: DVec3,
pub child_velocity_inertial: DVec3,
}
#[inline]
pub fn compute_kinematic_child_state(inputs: KinematicChildInputs) -> KinematicChildOutputs {
let KinematicChildInputs {
parent_t_inertial_body,
parent_ang_vel_body,
parent_position_inertial,
parent_velocity_inertial,
parent_t_struct_body,
parent_composite_in_pstr,
t_parent_child,
link_offset_in_pstr,
child_t_struct_body,
child_composite_in_cstr,
} = inputs;
let parent_t_inertial_struct = parent_t_struct_body.transpose() * parent_t_inertial_body;
let child_t_inertial_struct = t_parent_child * parent_t_inertial_struct;
let child_t_inertial_body = child_t_struct_body * child_t_inertial_struct;
let child_q_inertial_body = JeodQuat::left_quat_from_transformation(&child_t_inertial_body);
let _normalized_witness = NormalizedQuat::new(child_q_inertial_body).unwrap_or_else(|err| {
panic!(
"compute_kinematic_child_state: composed inertial→body rotation produced a \
non-unit quaternion ({err:?}). This indicates a non-orthonormal input — most \
likely a `MassChildOf.t_parent_child` rotation matrix that has drifted off SO(3). \
Fix the upstream rotation: set `MassChildOf::with_rotation(parent, offset, T)` \
with an orthonormal `T`, or rebuild it from a unit quaternion via \
`JeodQuat::left_quat_to_transformation`."
)
});
let omega_inertial = parent_t_inertial_body.transpose() * parent_ang_vel_body;
let child_ang_vel_body = child_t_inertial_body * omega_inertial;
let pcm_to_ccm = link_offset_in_pstr + t_parent_child.transpose() * child_composite_in_cstr
- parent_composite_in_pstr;
let r_offset_inertial = parent_t_inertial_struct.transpose() * pcm_to_ccm;
let child_position_inertial = parent_position_inertial + r_offset_inertial;
let child_velocity_inertial =
parent_velocity_inertial + omega_inertial.cross(r_offset_inertial);
KinematicChildOutputs {
child_t_inertial_body,
child_q_inertial_body,
child_ang_vel_body,
child_position_inertial,
child_velocity_inertial,
}
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn derive_kinematic_child_from_states(
parent_rot: &RotationalState,
parent_trans: &TranslationalState,
parent_t_struct_body: DMat3,
parent_composite_in_pstr: DVec3,
t_parent_child: DMat3,
link_offset_in_pstr: DVec3,
child_t_struct_body: DMat3,
child_composite_in_cstr: DVec3,
) -> (RotationalState, TranslationalState) {
let parent_t_inertial_body = parent_rot.quaternion.left_quat_to_transformation();
let inputs = KinematicChildInputs {
parent_t_inertial_body,
parent_ang_vel_body: parent_rot.ang_vel_body,
parent_position_inertial: parent_trans.position,
parent_velocity_inertial: parent_trans.velocity,
parent_t_struct_body,
parent_composite_in_pstr,
t_parent_child,
link_offset_in_pstr,
child_t_struct_body,
child_composite_in_cstr,
};
let out = compute_kinematic_child_state(inputs);
let rot = RotationalState {
quaternion: out.child_q_inertial_body,
ang_vel_body: out.child_ang_vel_body,
};
let trans = TranslationalState {
position: out.child_position_inertial,
velocity: out.child_velocity_inertial,
};
(rot, trans)
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn compute_kinematic_child_state_typed<VParent: Vehicle, VChild: Vehicle>(
parent_q_inertial_body: BodyAttitude<VParent>,
parent_ang_vel_body: AngularVelocity<BodyFrame<VParent>>,
parent_position_inertial: Position<RootInertial>,
parent_velocity_inertial: Velocity<RootInertial>,
parent_t_struct_body: DMat3,
parent_composite_in_pstr: DVec3,
t_parent_child: DMat3,
link_offset_in_pstr: DVec3,
child_t_struct_body: DMat3,
child_composite_in_cstr: DVec3,
) -> (
BodyAttitude<VChild>,
AngularVelocity<BodyFrame<VChild>>,
Position<RootInertial>,
Velocity<RootInertial>,
) {
let parent_t_inertial_body = parent_q_inertial_body
.as_witness()
.left_quat_to_transformation();
let inputs = KinematicChildInputs {
parent_t_inertial_body,
parent_ang_vel_body: parent_ang_vel_body.raw_si(),
parent_position_inertial: parent_position_inertial.raw_si(),
parent_velocity_inertial: parent_velocity_inertial.raw_si(),
parent_t_struct_body,
parent_composite_in_pstr,
t_parent_child,
link_offset_in_pstr,
child_t_struct_body,
child_composite_in_cstr,
};
let out = compute_kinematic_child_state(inputs);
let q = NormalizedQuat::new(out.child_q_inertial_body)
.expect("child_q_inertial_body unit-norm: enforced inside compute_kinematic_child_state");
let attitude = BodyAttitude::<VChild>::from_witness(q);
let ang_vel = AngularVelocity::<BodyFrame<VChild>>::from_raw_si(out.child_ang_vel_body);
let position = Position::<RootInertial>::from_raw_si(out.child_position_inertial);
let velocity = Velocity::<RootInertial>::from_raw_si(out.child_velocity_inertial);
(attitude, ang_vel, position, velocity)
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn compute_kinematic_child_state_dquat(
parent_q_inertial_body: DQuat,
parent_ang_vel_body: DVec3,
parent_position_inertial: DVec3,
parent_velocity_inertial: DVec3,
parent_t_struct_body: DMat3,
parent_composite_in_pstr: DVec3,
t_parent_child: DMat3,
link_offset_in_pstr: DVec3,
child_t_struct_body: DMat3,
child_composite_in_cstr: DVec3,
) -> KinematicChildOutputs {
let parent_t_inertial_body = DMat3::from_quat(parent_q_inertial_body);
let inputs = KinematicChildInputs {
parent_t_inertial_body,
parent_ang_vel_body,
parent_position_inertial,
parent_velocity_inertial,
parent_t_struct_body,
parent_composite_in_pstr,
t_parent_child,
link_offset_in_pstr,
child_t_struct_body,
child_composite_in_cstr,
};
compute_kinematic_child_state(inputs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::propagation::propagate_forward;
use astrodyn_frames::{RefFrameRot, RefFrameState, RefFrameTrans};
use astrodyn_math::test_utils::{approx_eq_mat3, approx_eq_vec3};
use std::f64::consts::FRAC_PI_4;
const TOL: f64 = 1e-12;
fn jeod_rot_z(angle: f64) -> DMat3 {
JeodQuat::left_quat_from_eigen_rotation(angle, DVec3::Z).left_quat_to_transformation()
}
fn jeod_rot_axis(angle: f64, axis: DVec3) -> DMat3 {
JeodQuat::left_quat_from_eigen_rotation(angle, axis).left_quat_to_transformation()
}
#[test]
fn identity_attach_returns_parent_state() {
let parent_pos = DVec3::new(7e6, 0.0, 0.0);
let parent_vel = DVec3::new(0.0, 7500.0, 0.0);
let inputs = KinematicChildInputs {
parent_t_inertial_body: DMat3::IDENTITY,
parent_ang_vel_body: DVec3::ZERO,
parent_position_inertial: parent_pos,
parent_velocity_inertial: parent_vel,
parent_t_struct_body: DMat3::IDENTITY,
parent_composite_in_pstr: DVec3::ZERO,
t_parent_child: DMat3::IDENTITY,
link_offset_in_pstr: DVec3::ZERO,
child_t_struct_body: DMat3::IDENTITY,
child_composite_in_cstr: DVec3::ZERO,
};
let out = compute_kinematic_child_state(inputs);
assert!(approx_eq_mat3(
&out.child_t_inertial_body,
&DMat3::IDENTITY,
TOL
));
assert!(approx_eq_vec3(out.child_ang_vel_body, DVec3::ZERO, TOL));
assert!(approx_eq_vec3(out.child_position_inertial, parent_pos, TOL));
assert!(approx_eq_vec3(out.child_velocity_inertial, parent_vel, TOL));
}
#[test]
fn passive_z_rotation_attach_composes_correctly() {
let angle = std::f64::consts::PI / 6.0;
let t_pc = jeod_rot_z(angle);
let parent_pos = DVec3::new(7e6, 0.0, 0.0);
let parent_vel = DVec3::new(0.0, 7500.0, 0.0);
let inputs = KinematicChildInputs {
parent_t_inertial_body: DMat3::IDENTITY,
parent_ang_vel_body: DVec3::ZERO,
parent_position_inertial: parent_pos,
parent_velocity_inertial: parent_vel,
parent_t_struct_body: DMat3::IDENTITY,
parent_composite_in_pstr: DVec3::ZERO,
t_parent_child: t_pc,
link_offset_in_pstr: DVec3::ZERO,
child_t_struct_body: DMat3::IDENTITY,
child_composite_in_cstr: DVec3::ZERO,
};
let out = compute_kinematic_child_state(inputs);
assert!(approx_eq_mat3(&out.child_t_inertial_body, &t_pc, TOL));
assert!(approx_eq_vec3(out.child_ang_vel_body, DVec3::ZERO, TOL));
assert!(approx_eq_vec3(out.child_position_inertial, parent_pos, TOL));
assert!(approx_eq_vec3(out.child_velocity_inertial, parent_vel, TOL));
let q_to_t = out.child_q_inertial_body.left_quat_to_transformation();
assert!(approx_eq_mat3(&q_to_t, &t_pc, TOL));
}
#[test]
fn non_zero_offset_under_parent_omega_yields_omega_cross_r() {
let omega = DVec3::new(0.0, 0.0, 1e-3);
let offset = DVec3::new(1.0, 0.0, 0.0);
let parent_pos = DVec3::new(7e6, 0.0, 0.0);
let parent_vel = DVec3::new(0.0, 7500.0, 0.0);
let inputs = KinematicChildInputs {
parent_t_inertial_body: DMat3::IDENTITY,
parent_ang_vel_body: omega,
parent_position_inertial: parent_pos,
parent_velocity_inertial: parent_vel,
parent_t_struct_body: DMat3::IDENTITY,
parent_composite_in_pstr: DVec3::ZERO,
t_parent_child: DMat3::IDENTITY,
link_offset_in_pstr: offset,
child_t_struct_body: DMat3::IDENTITY,
child_composite_in_cstr: DVec3::ZERO,
};
let out = compute_kinematic_child_state(inputs);
assert!(approx_eq_vec3(
out.child_position_inertial,
parent_pos + offset,
TOL
));
let expected_vel = parent_vel + DVec3::new(0.0, 1e-3, 0.0);
assert!(
approx_eq_vec3(out.child_velocity_inertial, expected_vel, TOL),
"expected {expected_vel:?}, got {:?}",
out.child_velocity_inertial
);
assert!(approx_eq_vec3(out.child_ang_vel_body, omega, TOL));
}
#[test]
fn matches_three_propagate_forward_calls_under_complex_attitudes() {
let parent_q =
JeodQuat::left_quat_from_eigen_rotation(std::f64::consts::FRAC_PI_2, DVec3::Z);
let parent_t_ib = parent_q.left_quat_to_transformation();
let parent_omega = DVec3::new(0.001, -0.002, 0.0011);
let parent_pos = DVec3::new(6.778e6, 0.0, 0.0);
let parent_vel = DVec3::new(0.0, 7672.0, 0.0);
let parent_t_sb = jeod_rot_axis(std::f64::consts::FRAC_PI_6, DVec3::Y);
let parent_cm_in_pstr = DVec3::new(0.1, 0.0, -0.05);
let link_axis = DVec3::new(1.0, 1.0, 1.0).normalize();
let t_pc = jeod_rot_axis(FRAC_PI_4, link_axis);
let link_offset = DVec3::new(2.0, 0.5, -0.3);
let child_t_sb = jeod_rot_axis(0.15, DVec3::X);
let child_cm_in_cstr = DVec3::new(-0.05, 0.0, 0.02);
let inputs = KinematicChildInputs {
parent_t_inertial_body: parent_t_ib,
parent_ang_vel_body: parent_omega,
parent_position_inertial: parent_pos,
parent_velocity_inertial: parent_vel,
parent_t_struct_body: parent_t_sb,
parent_composite_in_pstr: parent_cm_in_pstr,
t_parent_child: t_pc,
link_offset_in_pstr: link_offset,
child_t_struct_body: child_t_sb,
child_composite_in_cstr: child_cm_in_cstr,
};
let out = compute_kinematic_child_state(inputs);
let parent_body_state = RefFrameState {
trans: RefFrameTrans {
position: parent_pos,
velocity: parent_vel,
},
rot: RefFrameRot {
q_parent_this: parent_q,
t_parent_this: parent_t_ib,
ang_vel_this: parent_omega,
},
};
let body_to_struct = crate::mass_body::MassPointState {
position: parent_t_sb * (-parent_cm_in_pstr),
t_parent_this: parent_t_sb.transpose(),
};
let parent_struct_state = propagate_forward(&parent_body_state, &body_to_struct);
let pstr_to_cstr = crate::mass_body::MassPointState {
position: link_offset,
t_parent_this: t_pc,
};
let child_struct_state = propagate_forward(&parent_struct_state, &pstr_to_cstr);
let cstr_to_cbody = crate::mass_body::MassPointState {
position: child_cm_in_cstr,
t_parent_this: child_t_sb,
};
let child_body_state = propagate_forward(&child_struct_state, &cstr_to_cbody);
assert!(
approx_eq_mat3(
&out.child_t_inertial_body,
&child_body_state.rot.t_parent_this,
1e-12
),
"T_inertial_body mismatch:\n kernel: {:?}\n propagate: {:?}",
out.child_t_inertial_body,
child_body_state.rot.t_parent_this
);
assert!(
approx_eq_vec3(
out.child_ang_vel_body,
child_body_state.rot.ang_vel_this,
1e-12
),
"ang_vel_body mismatch:\n kernel: {:?}\n propagate: {:?}",
out.child_ang_vel_body,
child_body_state.rot.ang_vel_this
);
assert!(
approx_eq_vec3(
out.child_position_inertial,
child_body_state.trans.position,
1e-9
),
"position mismatch:\n kernel: {:?}\n propagate: {:?}",
out.child_position_inertial,
child_body_state.trans.position
);
assert!(
approx_eq_vec3(
out.child_velocity_inertial,
child_body_state.trans.velocity,
1e-9
),
"velocity mismatch:\n kernel: {:?}\n propagate: {:?}",
out.child_velocity_inertial,
child_body_state.trans.velocity
);
}
#[test]
fn typed_sibling_matches_raw_kernel() {
use astrodyn_quantities::frame::SelfRef;
let omega = DVec3::new(0.0, 0.0, 1e-3);
let offset = DVec3::new(1.0, 0.0, 0.0);
let parent_pos = DVec3::new(7e6, 0.0, 0.0);
let parent_vel = DVec3::new(0.0, 7500.0, 0.0);
let parent_q = BodyAttitude::<SelfRef>::identity();
let parent_omega_typed = AngularVelocity::<BodyFrame<SelfRef>>::from_raw_si(omega);
let parent_pos_typed = Position::<RootInertial>::from_raw_si(parent_pos);
let parent_vel_typed = Velocity::<RootInertial>::from_raw_si(parent_vel);
let (child_q, child_omega, child_pos, child_vel) =
compute_kinematic_child_state_typed::<SelfRef, SelfRef>(
parent_q,
parent_omega_typed,
parent_pos_typed,
parent_vel_typed,
DMat3::IDENTITY,
DVec3::ZERO,
DMat3::IDENTITY,
offset,
DMat3::IDENTITY,
DVec3::ZERO,
);
assert!(approx_eq_vec3(child_pos.raw_si(), parent_pos + offset, TOL));
let expected_vel = parent_vel + DVec3::new(0.0, 1e-3, 0.0);
assert!(approx_eq_vec3(child_vel.raw_si(), expected_vel, TOL));
assert!(approx_eq_vec3(child_omega.raw_si(), omega, TOL));
assert!(approx_eq_mat3(
&child_q.as_witness().left_quat_to_transformation(),
&DMat3::IDENTITY,
TOL,
));
}
}