use astrodyn_dynamics::{
combine_states_at_attach, AttachCombineInputs, AttachCombineOutputs, DetachedSubtreeState,
MassProperties,
};
use astrodyn_frames::{RefFrameRot, RefFrameState, RefFrameTrans};
use astrodyn_math::JeodQuat;
use astrodyn_quantities::quat::NormalizedQuat;
type ScalarFirstLeftNormalizedQuat = NormalizedQuat<
astrodyn_quantities::quat::ScalarFirst,
astrodyn_quantities::quat::LeftTransform,
>;
#[derive(Debug, Clone, Copy)]
pub struct StageAttachInputs {
pub parent_position: glam::DVec3,
pub parent_velocity: glam::DVec3,
pub parent_quaternion: JeodQuat,
pub parent_ang_vel_body: glam::DVec3,
pub parent_mass: MassProperties,
pub orig_parent_cm_struct: glam::DVec3,
pub parent_t_inertial_struct: glam::DMat3,
pub child_position: glam::DVec3,
pub child_velocity: glam::DVec3,
pub child_quaternion: JeodQuat,
pub child_ang_vel_body: glam::DVec3,
pub child_mass: MassProperties,
pub combined_mass: MassProperties,
}
#[derive(Debug, Clone, Copy)]
pub struct StageAttachOutputs {
pub position: glam::DVec3,
pub velocity: glam::DVec3,
pub quaternion: JeodQuat,
pub ang_vel_body: glam::DVec3,
}
pub fn stage_attach_combine(input: StageAttachInputs) -> StageAttachOutputs {
let parent_t_parent_this = input.parent_quaternion.left_quat_to_transformation();
let child_t_parent_this = input.child_quaternion.left_quat_to_transformation();
let parent_composite = RefFrameState {
trans: RefFrameTrans {
position: input.parent_position,
velocity: input.parent_velocity,
},
rot: RefFrameRot {
q_parent_this: input.parent_quaternion,
t_parent_this: parent_t_parent_this,
ang_vel_this: input.parent_ang_vel_body,
},
};
let child_composite = RefFrameState {
trans: RefFrameTrans {
position: input.child_position,
velocity: input.child_velocity,
},
rot: RefFrameRot {
q_parent_this: input.child_quaternion,
t_parent_this: child_t_parent_this,
ang_vel_this: input.child_ang_vel_body,
},
};
let AttachCombineOutputs { composite_state } = combine_states_at_attach(AttachCombineInputs {
parent_composite,
parent_mass: input.parent_mass,
parent_t_inertial_struct: input.parent_t_inertial_struct,
child_composite,
child_mass: input.child_mass,
combined_mass: input.combined_mass,
orig_parent_cm_struct: input.orig_parent_cm_struct,
});
StageAttachOutputs {
position: composite_state.trans.position,
velocity: composite_state.trans.velocity,
quaternion: composite_state.rot.q_parent_this,
ang_vel_body: composite_state.rot.ang_vel_this,
}
}
pub fn stage_detach_capture(
position: glam::DVec3,
velocity: glam::DVec3,
quaternion: JeodQuat,
ang_vel_body: glam::DVec3,
) -> DetachedSubtreeState {
let _: ScalarFirstLeftNormalizedQuat = NormalizedQuat::new(quaternion).unwrap_or_else(|err| {
panic!(
"stage_detach_capture: pre-detach quaternion is not unit-norm: {err}. \
Call `JeodQuat::normalize` on the input before passing it here \
(the integrators' end-of-step normalize_integ guarantees this for \
integrated bodies — mission code constructing detach state by hand \
must do the same)."
)
});
DetachedSubtreeState::from_ref_frame_state(&RefFrameState {
trans: RefFrameTrans { position, velocity },
rot: RefFrameRot {
q_parent_this: quaternion,
t_parent_this: quaternion.left_quat_to_transformation(),
ang_vel_this: ang_vel_body,
},
})
}
#[derive(Debug, Clone, Copy)]
pub struct CrossIntegFrameStateShift {
shift_pos: glam::DVec3,
shift_vel: glam::DVec3,
}
impl CrossIntegFrameStateShift {
#[inline]
pub fn between_integ_origins(
old_origin_pos: glam::DVec3,
old_origin_vel: glam::DVec3,
new_origin_pos: glam::DVec3,
new_origin_vel: glam::DVec3,
) -> Self {
Self {
shift_pos: old_origin_pos - new_origin_pos,
shift_vel: old_origin_vel - new_origin_vel,
}
}
#[inline]
pub fn shift_pos(&self) -> glam::DVec3 {
self.shift_pos
}
#[inline]
pub fn shift_vel(&self) -> glam::DVec3 {
self.shift_vel
}
#[inline]
pub fn apply(
&self,
position: glam::DVec3,
velocity: glam::DVec3,
) -> (glam::DVec3, glam::DVec3) {
(position + self.shift_pos, velocity + self.shift_vel)
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::{DMat3, DVec3};
#[test]
fn stage_attach_combine_preserves_state_on_soft_merge() {
let parent_mass = MassProperties::with_inertia(
100.0,
DMat3::from_diagonal(DVec3::new(50.0, 50.0, 50.0)),
DVec3::ZERO,
);
let child_mass = MassProperties::with_inertia(
50.0,
DMat3::from_diagonal(DVec3::new(20.0, 20.0, 20.0)),
DVec3::ZERO,
);
let combined_mass = MassProperties::with_inertia(
150.0,
DMat3::from_diagonal(DVec3::new(70.0, 70.0, 70.0)),
DVec3::ZERO,
);
let v = DVec3::new(0.0, 7600.0, 0.0);
let w = DVec3::new(0.0, -1.13e-3, 0.0);
let pos = DVec3::new(7e6, 0.0, 0.0);
let q = JeodQuat::identity();
let out = stage_attach_combine(StageAttachInputs {
parent_position: pos,
parent_velocity: v,
parent_quaternion: q,
parent_ang_vel_body: w,
parent_mass,
orig_parent_cm_struct: DVec3::ZERO,
parent_t_inertial_struct: DMat3::IDENTITY,
child_position: pos,
child_velocity: v,
child_quaternion: q,
child_ang_vel_body: w,
child_mass,
combined_mass,
});
assert!((out.velocity - v).length() < 1e-9);
assert!((out.ang_vel_body - w).length() < 1e-9);
}
#[test]
fn stage_attach_combine_conserves_linear_and_angular_momentum() {
let parent_mass = MassProperties::with_inertia(
1000.0,
DMat3::from_diagonal(DVec3::new(500.0, 500.0, 500.0)),
DVec3::ZERO,
);
let child_mass = MassProperties::with_inertia(
1000.0,
DMat3::from_diagonal(DVec3::new(500.0, 500.0, 500.0)),
DVec3::ZERO,
);
let combined_mass = MassProperties::with_inertia(
2000.0,
DMat3::from_diagonal(DVec3::new(2000.0, 2000.0, 2000.0)),
DVec3::new(1.0, 0.0, 0.0),
);
let q = JeodQuat::identity();
let out = stage_attach_combine(StageAttachInputs {
parent_position: DVec3::ZERO,
parent_velocity: DVec3::ZERO,
parent_quaternion: q,
parent_ang_vel_body: DVec3::ZERO,
parent_mass,
orig_parent_cm_struct: DVec3::ZERO,
parent_t_inertial_struct: DMat3::IDENTITY,
child_position: DVec3::new(2.0, 0.0, 0.0),
child_velocity: DVec3::new(0.0, 1.0, 0.0),
child_quaternion: q,
child_ang_vel_body: DVec3::ZERO,
child_mass,
combined_mass,
});
let expected_v = DVec3::new(0.0, 0.5, 0.0);
let expected_w = DVec3::new(0.0, 0.0, 0.5);
assert!((out.velocity - expected_v).length() < 1e-9);
assert!((out.ang_vel_body - expected_w).length() < 1e-9);
}
#[test]
fn stage_detach_capture_round_trips() {
let pos = DVec3::new(7e6, 1.0, -3.0);
let vel = DVec3::new(0.0, 7672.0, 0.0);
let q = JeodQuat::identity();
let omega = DVec3::new(0.001, -0.002, 0.0011);
let captured = stage_detach_capture(pos, vel, q, omega);
assert_eq!(captured.composite_position.raw_si(), pos);
assert_eq!(captured.composite_velocity.raw_si(), vel);
assert_eq!(captured.composite_ang_vel_body, omega);
}
#[test]
fn stage_detach_capture_propagates_ballistically() {
let pos = DVec3::new(7e6, 0.0, 0.0);
let vel = DVec3::new(0.0, 7672.0, 0.0);
let q = JeodQuat::identity();
let omega = DVec3::ZERO;
let mut captured = stage_detach_capture(pos, vel, q, omega);
let dt = 60.0;
captured.step_ballistic(dt);
assert!((captured.composite_position.raw_si() - (pos + vel * dt)).length() < 1e-9);
assert!((captured.composite_velocity.raw_si() - vel).length() < 1e-9);
assert!((captured.composite_ang_vel_body - omega).length() < 1e-9);
}
#[test]
#[should_panic(expected = "not unit-norm")]
fn stage_detach_capture_panics_on_non_unit_quat() {
let bad_q = JeodQuat::new(2.0, 0.0, 0.0, 0.0);
let _ = stage_detach_capture(DVec3::ZERO, DVec3::ZERO, bad_q, DVec3::ZERO);
}
#[test]
fn cross_integ_frame_state_shift_translates_state_between_origins() {
let old_origin_pos = DVec3::new(1.5e11, 2.0, -3.0);
let old_origin_vel = DVec3::new(0.1, 30_000.0, 0.0);
let new_origin_pos = DVec3::new(1.0e11, -4.0, 2.5);
let new_origin_vel = DVec3::new(-0.05, 25_000.0, 0.5);
let shift = CrossIntegFrameStateShift::between_integ_origins(
old_origin_pos,
old_origin_vel,
new_origin_pos,
new_origin_vel,
);
assert_eq!(shift.shift_pos(), old_origin_pos - new_origin_pos);
assert_eq!(shift.shift_vel(), old_origin_vel - new_origin_vel);
let p_old = DVec3::new(7e6, 0.0, 0.0);
let v_old = DVec3::new(0.0, 7500.0, 0.0);
let (p_new, v_new) = shift.apply(p_old, v_old);
assert!((p_old + old_origin_pos - (p_new + new_origin_pos)).length() < 1e-9);
assert!((v_old + old_origin_vel - (v_new + new_origin_vel)).length() < 1e-9);
}
#[test]
fn cross_integ_frame_state_shift_is_no_op_for_matching_origins() {
let origin_pos = DVec3::new(1.5e11, 0.0, 0.0);
let origin_vel = DVec3::new(0.0, 30_000.0, 0.0);
let shift = CrossIntegFrameStateShift::between_integ_origins(
origin_pos, origin_vel, origin_pos, origin_vel,
);
assert_eq!(shift.shift_pos(), DVec3::ZERO);
assert_eq!(shift.shift_vel(), DVec3::ZERO);
let p = DVec3::new(7e6, 0.0, 0.0);
let v = DVec3::new(0.0, 7500.0, 0.0);
let (p_new, v_new) = shift.apply(p, v);
assert_eq!(p_new, p);
assert_eq!(v_new, v);
}
}