use std::collections::HashMap;
use glam::DVec3;
use astrodyn_dynamics::mass_storage::{MassNodeOutputs, MassStorage};
use astrodyn_dynamics::wrench::{shift_wrench_to_parent, Wrench};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EdgeGeometry {
pub pcm_to_ccm: DVec3,
pub t_parent_child: glam::DMat3,
}
impl Default for EdgeGeometry {
fn default() -> Self {
Self {
pcm_to_ccm: DVec3::ZERO,
t_parent_child: glam::DMat3::IDENTITY,
}
}
}
pub fn aggregate_wrenches_via_storage<S: MassStorage>(
storage: &S,
wrenches: &HashMap<S::Id, Wrench>,
edges: &HashMap<S::Id, EdgeGeometry>,
) -> HashMap<S::Id, Wrench> {
let expected = storage.node_count();
let mut acc: HashMap<S::Id, Wrench> = HashMap::with_capacity(expected);
let mut state: HashMap<S::Id, VisitState> = HashMap::with_capacity(expected);
let roots = storage.roots();
let mut out: HashMap<S::Id, Wrench> = HashMap::with_capacity(roots.len());
for root in &roots {
walk(storage, *root, wrenches, edges, &mut acc, &mut state);
let root_acc = acc.get(root).copied().unwrap_or_else(Wrench::zero);
out.insert(*root, root_acc);
}
let visited_count = state.len();
assert!(
visited_count == expected,
"MassStorage topology has an orphan: {} of {} nodes unreachable from roots(). \
Wrench aggregation skipped {} child wrenches; composite-rigid-body integration would \
silently drop child forces. 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,
}
pub fn edge_geometry_from_composites<S: MassStorage>(
storage: &S,
composites: &HashMap<S::Id, MassNodeOutputs>,
) -> HashMap<S::Id, EdgeGeometry> {
let mut edges: HashMap<S::Id, EdgeGeometry> = HashMap::new();
for root in storage.roots() {
edge_walk(storage, root, composites, &mut edges);
}
edges
}
fn edge_walk<S: MassStorage>(
storage: &S,
id: S::Id,
composites: &HashMap<S::Id, MassNodeOutputs>,
out: &mut HashMap<S::Id, EdgeGeometry>,
) {
let parent_composite = composites.get(&id).unwrap_or_else(|| {
panic!(
"edge_geometry_from_composites: parent node missing from composites \
map. Run `recompute_composites_via_storage` first."
)
});
for &child_id in storage.children(id) {
let child_composite = composites.get(&child_id).unwrap_or_else(|| {
panic!(
"edge_geometry_from_composites: child node missing from composites \
map. Run `recompute_composites_via_storage` first."
)
});
out.insert(
child_id,
EdgeGeometry {
pcm_to_ccm: child_composite.composite_wrt_pstr.position
- parent_composite.composite.position,
t_parent_child: child_composite.composite_wrt_pstr.t_parent_this,
},
);
edge_walk(storage, child_id, composites, out);
}
}
fn walk<S: MassStorage>(
storage: &S,
id: S::Id,
wrenches: &HashMap<S::Id, Wrench>,
edges: &HashMap<S::Id, EdgeGeometry>,
acc: &mut HashMap<S::Id, Wrench>,
state: &mut HashMap<S::Id, VisitState>,
) {
match state.get(&id) {
Some(VisitState::Visited) => return,
Some(VisitState::Visiting) => {
panic!(
"MassStorage topology has a cycle reachable from roots(): \
node revisited while still on the active aggregation \
stack. The composite-rigid-body wrench walk 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(id, VisitState::Visiting);
for &child_id in storage.children(id) {
walk(storage, child_id, wrenches, edges, acc, state);
}
let mut node_acc = wrenches.get(&id).copied().unwrap_or_else(Wrench::zero);
for &child_id in storage.children(id) {
let edge = edges.get(&child_id).unwrap_or_else(|| {
panic!(
"wrench aggregation: child edge missing from `edges` map. \
Every non-root node must have an EdgeGeometry entry — \
build it from your live mass-tree state \
(e.g. `edge_geometry_from_composites` on a fresh \
`recompute_composites_via_storage` output)."
)
});
let child_acc = *acc
.get(&child_id)
.expect("post-order walk: child accumulator must be finalised before parent shift");
let (df, dtau) = shift_wrench_to_parent(
child_acc.force,
child_acc.torque,
edge.pcm_to_ccm,
edge.t_parent_child,
);
node_acc.force += df;
node_acc.torque += dtau;
}
acc.insert(id, node_acc);
state.insert(id, VisitState::Visited);
}
#[cfg(test)]
mod tests {
use super::*;
use astrodyn_dynamics::mass::MassProperties;
use astrodyn_dynamics::mass_body::MassTree;
use astrodyn_dynamics::mass_storage::recompute_composites_via_storage;
use glam::{DMat3, DVec3};
fn assert_close(a: DVec3, b: DVec3, tol: f64, label: &str) {
let d = (a - b).length();
assert!(d < tol, "{label}: |Δ|={d:.3e}, a={a:?}, b={b:?}");
}
fn composites_map<S: MassStorage>(storage: &S) -> HashMap<S::Id, MassNodeOutputs> {
recompute_composites_via_storage(storage)
.into_iter()
.collect()
}
fn edges_for<S: MassStorage>(storage: &S) -> HashMap<S::Id, EdgeGeometry> {
let comps = composites_map(storage);
edge_geometry_from_composites(storage, &comps)
}
#[test]
fn lone_root_passes_through() {
let mut tree = MassTree::new();
let r = tree.add_root("root".into(), MassProperties::new(10.0));
let comps = edges_for(&tree);
let mut w = HashMap::new();
w.insert(
r,
Wrench::new(DVec3::new(1.0, 2.0, 3.0), DVec3::new(0.5, -1.0, 0.0)),
);
let out = aggregate_wrenches_via_storage(&tree, &w, &comps);
let agg = out[&r];
assert_eq!(agg.force, DVec3::new(1.0, 2.0, 3.0));
assert_eq!(agg.torque, DVec3::new(0.5, -1.0, 0.0));
}
#[test]
fn single_child_force_creates_root_torque() {
let mut tree = MassTree::new();
let p = tree.add_root("parent".into(), MassProperties::new(10.0));
let c = tree.add_body("child".into(), MassProperties::new(5.0));
tree.attach(c, p, DVec3::new(1.0, 0.0, 0.0), DMat3::IDENTITY);
let comps = edges_for(&tree);
let mut w = HashMap::new();
w.insert(c, Wrench::new(DVec3::new(0.0, 1.0, 0.0), DVec3::ZERO));
let out = aggregate_wrenches_via_storage(&tree, &w, &comps);
let root_acc = out[&p];
assert_close(root_acc.force, DVec3::new(0.0, 1.0, 0.0), 1e-15, "force");
let two_thirds = 2.0 / 3.0;
assert_close(
root_acc.torque,
DVec3::new(0.0, 0.0, two_thirds),
1e-15,
"parallel-axis torque",
);
}
#[test]
fn pure_child_torque_passes_through_with_no_cross_term() {
let mut tree = MassTree::new();
let p = tree.add_root("parent".into(), MassProperties::new(10.0));
let c = tree.add_body("child".into(), MassProperties::new(5.0));
tree.attach(c, p, DVec3::new(1.0, 0.0, 0.0), DMat3::IDENTITY);
let comps = edges_for(&tree);
let mut w = HashMap::new();
let child_torque = DVec3::new(0.7, -0.3, 1.2);
w.insert(c, Wrench::new(DVec3::ZERO, child_torque));
let out = aggregate_wrenches_via_storage(&tree, &w, &comps);
let root_acc = out[&p];
assert_eq!(root_acc.force, DVec3::ZERO);
assert_close(root_acc.torque, child_torque, 1e-15, "rotated child torque");
}
#[test]
fn parent_plus_two_children_sum_correctly() {
let mut tree = MassTree::new();
let p = tree.add_root("parent".into(), MassProperties::new(10.0));
let a = tree.add_body("child_a".into(), MassProperties::new(5.0));
let b = tree.add_body("child_b".into(), MassProperties::new(5.0));
tree.attach(a, p, DVec3::new(0.0, 1.0, 0.0), DMat3::IDENTITY);
tree.attach(b, p, DVec3::new(0.0, -1.0, 0.0), DMat3::IDENTITY);
let comps = edges_for(&tree);
let parent_force = DVec3::new(0.0, 0.0, 9.81);
let parent_torque = DVec3::new(0.5, 0.0, 0.0);
let child_force = DVec3::new(1.0, 0.0, 0.0);
let mut w = HashMap::new();
w.insert(p, Wrench::new(parent_force, parent_torque));
w.insert(a, Wrench::new(child_force, DVec3::ZERO));
w.insert(b, Wrench::new(child_force, DVec3::ZERO));
let out = aggregate_wrenches_via_storage(&tree, &w, &comps);
let root_acc = out[&p];
let expected_force = parent_force + child_force + child_force;
let expected_torque = parent_torque;
assert_close(root_acc.force, expected_force, 1e-15, "force sum");
assert_close(
root_acc.torque,
expected_torque,
1e-14,
"torque cancellation",
);
}
#[test]
fn two_level_chain_propagates_through() {
let mut tree = MassTree::new();
let a = tree.add_root("A".into(), MassProperties::new(1.0));
let b = tree.add_body("B".into(), MassProperties::new(1.0));
let c = tree.add_body("C".into(), MassProperties::new(1.0));
tree.attach(b, a, DVec3::new(1.0, 0.0, 0.0), DMat3::IDENTITY);
tree.attach(c, b, DVec3::new(1.0, 0.0, 0.0), DMat3::IDENTITY);
let comps = edges_for(&tree);
let mut w = HashMap::new();
w.insert(c, Wrench::new(DVec3::new(0.0, 1.0, 0.0), DVec3::ZERO));
let out = aggregate_wrenches_via_storage(&tree, &w, &comps);
let root_acc = out[&a];
assert_close(
root_acc.force,
DVec3::new(0.0, 1.0, 0.0),
1e-14,
"chain force",
);
assert_close(
root_acc.torque,
DVec3::new(0.0, 0.0, 1.0),
1e-14,
"chain torque",
);
}
#[test]
fn missing_wrench_treated_as_zero() {
let mut tree = MassTree::new();
let p = tree.add_root("parent".into(), MassProperties::new(10.0));
let c = tree.add_body("child".into(), MassProperties::new(5.0));
tree.attach(c, p, DVec3::new(1.0, 0.0, 0.0), DMat3::IDENTITY);
let comps = edges_for(&tree);
let w = HashMap::new();
let out = aggregate_wrenches_via_storage(&tree, &w, &comps);
assert_eq!(out[&p], Wrench::zero());
}
struct CyclicStorage {
roots: Vec<u32>,
children: HashMap<u32, Vec<u32>>,
nodes: Vec<u32>,
}
impl MassStorage for CyclicStorage {
type Id = u32;
fn parent(&self, id: u32) -> Option<u32> {
self.children
.iter()
.find_map(|(p, kids)| kids.contains(&id).then_some(*p))
}
fn node(&self, _id: u32) -> astrodyn_dynamics::mass_storage::MassNodeView<'_> {
astrodyn_dynamics::mass_storage::MassNodeView {
core: MassProperties::new(1.0),
structure_point: astrodyn_dynamics::mass_body::MassPointState::default(),
name: "mock",
}
}
fn children(&self, id: u32) -> &[u32] {
self.children
.get(&id)
.map_or(&[][..], std::vec::Vec::as_slice)
}
fn roots(&self) -> Vec<u32> {
self.roots.clone()
}
fn node_count(&self) -> usize {
self.nodes.len()
}
}
#[test]
#[should_panic(expected = "cycle reachable from roots()")]
fn cycle_reachable_from_roots_panics_loudly() {
let storage = CyclicStorage {
roots: vec![0],
children: {
let mut m = HashMap::new();
m.insert(0_u32, vec![1_u32]);
m.insert(1_u32, vec![0_u32]);
m
},
nodes: vec![0, 1],
};
let mut wrenches: HashMap<u32, Wrench> = HashMap::new();
wrenches.insert(0, Wrench::new(DVec3::X, DVec3::ZERO));
wrenches.insert(1, Wrench::new(DVec3::Y, DVec3::ZERO));
let mut edges: HashMap<u32, EdgeGeometry> = HashMap::new();
edges.insert(0, EdgeGeometry::default());
edges.insert(1, EdgeGeometry::default());
let _ = aggregate_wrenches_via_storage(&storage, &wrenches, &edges);
}
}