use std::collections::HashMap;
use astrodyn_dynamics::kinematic_propagation::{
compute_kinematic_child_state, KinematicChildInputs,
};
use astrodyn_dynamics::mass_storage::MassStorage;
use astrodyn_dynamics::rotational::RotationalState;
use astrodyn_dynamics::state::TranslationalStateTyped;
use astrodyn_math::JeodQuat;
use astrodyn_quantities::aliases::{Position, Velocity};
use astrodyn_quantities::frame::MassNode;
use glam::{DMat3, DVec3};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KinematicNodeState {
pub rot: RotationalState,
pub trans: TranslationalStateTyped<MassNode>,
pub t_struct_body: DMat3,
pub composite_in_struct: DVec3,
}
impl Default for KinematicNodeState {
fn default() -> Self {
Self {
rot: RotationalState::default(),
trans: TranslationalStateTyped::<MassNode>::default(),
t_struct_body: DMat3::IDENTITY,
composite_in_struct: DVec3::ZERO,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KinematicEdge {
pub t_parent_child: DMat3,
pub offset_in_pstr: DVec3,
}
impl Default for KinematicEdge {
fn default() -> Self {
Self {
t_parent_child: DMat3::IDENTITY,
offset_in_pstr: DVec3::ZERO,
}
}
}
pub fn propagate_state_via_storage<S: MassStorage>(
storage: &S,
nodes: &HashMap<S::Id, KinematicNodeState>,
edges: &HashMap<S::Id, KinematicEdge>,
) -> HashMap<S::Id, KinematicNodeState> {
let expected = storage.node_count();
let mut out: HashMap<S::Id, KinematicNodeState> = HashMap::with_capacity(expected);
let mut state: HashMap<S::Id, VisitState> = HashMap::with_capacity(expected);
for root in storage.roots() {
let root_state = *nodes.get(&root).unwrap_or_else(|| {
panic!(
"propagate_state_via_storage: root {root:?} missing from `nodes`. Every \
storage entity must declare a KinematicNodeState (even roots — their \
state seeds the walk). Backend likely failed to populate the per-entity \
state map; check the orchestration glue."
)
});
out.insert(root, root_state);
walk(
storage,
root,
&root_state,
nodes,
edges,
&mut out,
&mut state,
);
}
let visited_count = state.len();
assert!(
visited_count == expected,
"MassStorage topology has an orphan: {} of {} nodes unreachable from roots(). \
Kinematic propagation skipped {} children; their RotationalState / \
TranslationalState would stay frozen at the previous tick's value, silently \
drifting once the parent moves. Check MassChildOf edges for parents that are \
not roots and not children of any other node.",
expected.saturating_sub(visited_count),
expected,
expected.saturating_sub(visited_count),
);
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VisitState {
Visiting,
Visited,
}
fn walk<S: MassStorage>(
storage: &S,
parent_id: S::Id,
parent_state: &KinematicNodeState,
nodes: &HashMap<S::Id, KinematicNodeState>,
edges: &HashMap<S::Id, KinematicEdge>,
out: &mut HashMap<S::Id, KinematicNodeState>,
state: &mut HashMap<S::Id, VisitState>,
) {
match state.get(&parent_id) {
Some(VisitState::Visited) => return,
Some(VisitState::Visiting) => {
panic!(
"MassStorage topology has a cycle reachable from roots(): node \
{parent_id:?} revisited while still on the active propagation \
stack. Kinematic state propagation requires a forest of trees \
rooted at MassStorage::roots(); remove the back-edge from \
MassChildOf so the node's parent chain terminates at a root."
);
}
None => {}
}
state.insert(parent_id, VisitState::Visiting);
let parent_t_inertial_body = parent_state.rot.quaternion.left_quat_to_transformation();
for &child_id in storage.children(parent_id) {
let edge = edges.get(&child_id).unwrap_or_else(|| {
panic!(
"kinematic propagation: child edge {child_id:?} missing from `edges` \
map. Every non-root node must have a KinematicEdge entry — build it \
from the live mass-tree state (e.g. MassChildOf.t_parent_child + \
MassChildOf.offset on the Bevy side)."
)
});
let child_node = nodes.get(&child_id).unwrap_or_else(|| {
panic!(
"kinematic propagation: child {child_id:?} missing from `nodes` \
map. Every storage entity in the mass tree must declare a \
KinematicNodeState; backend likely forgot to seed it. \
Even kinematic children whose `rot`/`trans` are about to be \
derived must declare their `t_struct_body` and \
`composite_in_struct` so the kernel can route through the \
child's structural frame."
)
});
let inputs = KinematicChildInputs {
parent_t_inertial_body,
parent_ang_vel_body: parent_state.rot.ang_vel_body,
parent_position_inertial: parent_state.trans.position.raw_si(),
parent_velocity_inertial: parent_state.trans.velocity.raw_si(),
parent_t_struct_body: parent_state.t_struct_body,
parent_composite_in_pstr: parent_state.composite_in_struct,
t_parent_child: edge.t_parent_child,
link_offset_in_pstr: edge.offset_in_pstr,
child_t_struct_body: child_node.t_struct_body,
child_composite_in_cstr: child_node.composite_in_struct,
};
let kernel_out = compute_kinematic_child_state(inputs);
let derived_rot = RotationalState {
quaternion: kernel_out.child_q_inertial_body,
ang_vel_body: kernel_out.child_ang_vel_body,
};
let derived_trans = TranslationalStateTyped::<MassNode> {
position: Position::<MassNode>::from_raw_si(kernel_out.child_position_inertial),
velocity: Velocity::<MassNode>::from_raw_si(kernel_out.child_velocity_inertial),
};
let derived = KinematicNodeState {
rot: derived_rot,
trans: derived_trans,
t_struct_body: child_node.t_struct_body,
composite_in_struct: child_node.composite_in_struct,
};
out.insert(child_id, derived);
walk(storage, child_id, &derived, nodes, edges, out, state);
}
state.insert(parent_id, VisitState::Visited);
}
#[inline]
fn _unit_norm(q: &JeodQuat, tol: f64) -> bool {
(q.norm_sq() - 1.0).abs() < tol
}
#[cfg(test)]
mod tests {
use super::*;
use astrodyn_dynamics::mass::MassProperties;
use astrodyn_dynamics::mass_body::MassTree;
fn approx_eq_vec3(a: DVec3, b: DVec3, tol: f64) -> bool {
(a - b).length() < tol
}
fn approx_eq_mat3(a: &DMat3, b: &DMat3, tol: f64) -> bool {
(a.x_axis - b.x_axis).length() < tol
&& (a.y_axis - b.y_axis).length() < tol
&& (a.z_axis - b.z_axis).length() < tol
}
fn jeod_rot_z(angle: f64) -> DMat3 {
JeodQuat::left_quat_from_eigen_rotation(angle, DVec3::Z).left_quat_to_transformation()
}
fn edges_for(tree: &MassTree) -> HashMap<<MassTree as MassStorage>::Id, KinematicEdge> {
let mut out = HashMap::new();
for id in 0..tree.len() {
if MassStorage::parent(tree, id).is_some() {
let view = MassStorage::node(tree, id);
out.insert(
id,
KinematicEdge {
t_parent_child: view.structure_point.t_parent_this,
offset_in_pstr: view.structure_point.position,
},
);
}
}
out
}
#[test]
fn identity_chain_propagates_root_state_unchanged() {
let mut tree = MassTree::new();
let root = tree.add_root("root".into(), MassProperties::new(10.0));
let mid = tree.add_body("mid".into(), MassProperties::new(5.0));
let leaf = tree.add_body("leaf".into(), MassProperties::new(2.0));
tree.attach(mid, root, DVec3::ZERO, DMat3::IDENTITY);
tree.attach(leaf, mid, DVec3::ZERO, DMat3::IDENTITY);
let root_pos = DVec3::new(7e6, 0.0, 0.0);
let root_vel = DVec3::new(0.0, 7500.0, 0.0);
let mut nodes = HashMap::new();
nodes.insert(
root,
KinematicNodeState {
rot: RotationalState::default(),
trans: TranslationalStateTyped::<MassNode> {
position: Position::<MassNode>::from_raw_si(root_pos),
velocity: Velocity::<MassNode>::from_raw_si(root_vel),
},
t_struct_body: DMat3::IDENTITY,
composite_in_struct: DVec3::ZERO,
},
);
nodes.insert(mid, KinematicNodeState::default());
nodes.insert(leaf, KinematicNodeState::default());
let edges = edges_for(&tree);
let out = propagate_state_via_storage(&tree, &nodes, &edges);
assert_eq!(out[&root].trans.position.raw_si(), root_pos);
assert_eq!(out[&root].trans.velocity.raw_si(), root_vel);
assert!(approx_eq_vec3(
out[&mid].trans.position.raw_si(),
root_pos,
1e-12
));
assert!(approx_eq_vec3(
out[&mid].trans.velocity.raw_si(),
root_vel,
1e-12
));
assert!(approx_eq_vec3(
out[&leaf].trans.position.raw_si(),
root_pos,
1e-12
));
assert!(approx_eq_vec3(
out[&leaf].trans.velocity.raw_si(),
root_vel,
1e-12
));
}
#[test]
fn three_body_chain_composes_rotations_through_middle() {
let angle = std::f64::consts::PI / 6.0;
let t_pc = jeod_rot_z(angle);
let offset = DVec3::new(1.0, 0.0, 0.0);
let mut tree = MassTree::new();
let root = tree.add_root("root".into(), MassProperties::new(10.0));
let mid = tree.add_body("mid".into(), MassProperties::new(5.0));
let leaf = tree.add_body("leaf".into(), MassProperties::new(2.0));
tree.attach(mid, root, offset, t_pc);
tree.attach(leaf, mid, offset, t_pc);
let mut nodes = HashMap::new();
nodes.insert(root, KinematicNodeState::default());
nodes.insert(mid, KinematicNodeState::default());
nodes.insert(leaf, KinematicNodeState::default());
let edges = edges_for(&tree);
let out = propagate_state_via_storage(&tree, &nodes, &edges);
let expected_t = t_pc * t_pc;
let leaf_t = out[&leaf].rot.quaternion.left_quat_to_transformation();
assert!(
approx_eq_mat3(&leaf_t, &expected_t, 1e-12),
"leaf T_inertial_body: expected {:?}, got {:?}",
expected_t,
leaf_t
);
let expected_leaf_pos = offset + t_pc.transpose() * offset;
assert!(
approx_eq_vec3(out[&leaf].trans.position.raw_si(), expected_leaf_pos, 1e-12),
"leaf position: expected {expected_leaf_pos:?}, got {:?}",
out[&leaf].trans.position.raw_si()
);
}
#[test]
#[should_panic(expected = "missing from `nodes` map")]
fn missing_child_node_state_panics() {
let mut tree = MassTree::new();
let root = tree.add_root("root".into(), MassProperties::new(10.0));
let child = tree.add_body("child".into(), MassProperties::new(5.0));
tree.attach(child, root, DVec3::ZERO, DMat3::IDENTITY);
let mut nodes = HashMap::new();
nodes.insert(root, KinematicNodeState::default());
let edges = edges_for(&tree);
let _ = propagate_state_via_storage(&tree, &nodes, &edges);
}
#[test]
#[should_panic(expected = "missing from `edges` map")]
fn missing_child_edge_panics() {
let mut tree = MassTree::new();
let root = tree.add_root("root".into(), MassProperties::new(10.0));
let child = tree.add_body("child".into(), MassProperties::new(5.0));
tree.attach(child, root, DVec3::ZERO, DMat3::IDENTITY);
let mut nodes = HashMap::new();
nodes.insert(root, KinematicNodeState::default());
nodes.insert(child, KinematicNodeState::default());
let edges: HashMap<<MassTree as MassStorage>::Id, KinematicEdge> = HashMap::new();
let _ = propagate_state_via_storage(&tree, &nodes, &edges);
}
}