use nalgebra::{DVector, Vector3};
use super::joint::GenJoint;
use super::spatial::{SpatialInertia, SpatialTransform, SpatialVector};
pub type GenJointType = GenJoint;
#[derive(Clone, Debug)]
pub struct BodyDef {
pub name: String,
pub parent: i32,
pub joint_type: GenJoint,
pub inertia: SpatialInertia,
pub x_tree: SpatialTransform,
pub q_index: usize,
pub v_index: usize,
}
#[derive(Clone, Debug)]
pub struct ArticulatedBody {
pub bodies: Vec<BodyDef>,
children: Vec<Vec<usize>>,
total_nq: usize,
total_nv: usize,
pub q: DVector<f32>,
pub qd: DVector<f32>,
pub qdd: DVector<f32>,
pub tau: DVector<f32>,
pub f_ext: Vec<SpatialVector>,
pub q_compensation: DVector<f32>,
pub qd_compensation: DVector<f32>,
pub gravity: SpatialVector,
}
impl Default for ArticulatedBody {
fn default() -> Self {
Self::new()
}
}
impl ArticulatedBody {
pub fn new() -> Self {
Self {
bodies: Vec::new(),
children: Vec::new(),
total_nq: 0,
total_nv: 0,
q: DVector::zeros(0),
qd: DVector::zeros(0),
qdd: DVector::zeros(0),
tau: DVector::zeros(0),
f_ext: Vec::new(),
q_compensation: DVector::zeros(0),
qd_compensation: DVector::zeros(0),
gravity: SpatialVector::new(Vector3::zeros(), Vector3::new(0.0, 9.81, 0.0)),
}
}
pub fn set_gravity(&mut self, g: Vector3<f32>) {
self.gravity = SpatialVector::new(Vector3::zeros(), -g);
}
pub fn add_body(
&mut self,
name: impl Into<String>,
parent: i32,
joint_type: GenJoint,
inertia: SpatialInertia,
x_tree: SpatialTransform,
) -> usize {
let body_index = self.bodies.len();
let q_index = self.total_nq;
let v_index = self.total_nv;
let npos = joint_type.n_pos();
let dof = joint_type.dof();
let default_q = joint_type.default_q();
self.bodies.push(BodyDef {
name: name.into(),
parent,
joint_type,
inertia,
x_tree,
q_index,
v_index,
});
self.children.push(Vec::new());
if parent >= 0 {
let pidx = parent as usize;
assert!(
pidx < body_index,
"add_body: parent index {} out of range (0..{})",
pidx, body_index
);
self.children[pidx].push(body_index);
}
self.total_nq += npos;
self.total_nv += dof;
let mut new_q = DVector::zeros(self.total_nq);
for i in 0..q_index {
new_q[i] = self.q[i];
}
for (i, &val) in default_q.iter().enumerate() {
new_q[q_index + i] = val;
}
self.q = new_q;
let mut new_comp = DVector::zeros(self.total_nq);
for i in 0..q_index {
new_comp[i] = self.q_compensation[i];
}
self.q_compensation = new_comp;
let mut new_qd = DVector::zeros(self.total_nv);
let mut new_qdd = DVector::zeros(self.total_nv);
let mut new_tau = DVector::zeros(self.total_nv);
for i in 0..v_index {
new_qd[i] = self.qd[i];
new_qdd[i] = self.qdd[i];
new_tau[i] = self.tau[i];
}
self.qd = new_qd;
self.qdd = new_qdd;
self.tau = new_tau;
let mut new_qd_comp = DVector::zeros(self.total_nv);
for i in 0..v_index {
new_qd_comp[i] = self.qd_compensation[i];
}
self.qd_compensation = new_qd_comp;
self.f_ext.push(SpatialVector::zero());
body_index
}
pub fn body_count(&self) -> usize {
self.bodies.len()
}
pub fn dof_count(&self) -> usize {
self.total_nv
}
pub fn nq(&self) -> usize {
self.total_nq
}
pub fn parent(&self, i: usize) -> i32 {
self.bodies[i].parent
}
pub fn children(&self, i: usize) -> &[usize] {
&self.children[i]
}
pub fn body_by_name(&self, name: &str) -> Option<usize> {
self.bodies.iter().position(|b| b.name == name)
}
pub fn joint_q(&self, i: usize) -> &[f32] {
let body = &self.bodies[i];
let npos = body.joint_type.n_pos();
if npos == 0 {
return &[];
}
&self.q.as_slice()[body.q_index..body.q_index + npos]
}
pub fn joint_qd(&self, i: usize) -> &[f32] {
let body = &self.bodies[i];
let dof = body.joint_type.dof();
if dof == 0 {
return &[];
}
&self.qd.as_slice()[body.v_index..body.v_index + dof]
}
pub fn set_joint_q(&mut self, i: usize, values: &[f32]) {
let body = &self.bodies[i];
let npos = body.joint_type.n_pos();
let start = body.q_index;
for (j, &val) in values.iter().take(npos).enumerate() {
self.q[start + j] = val;
self.q_compensation[start + j] = 0.0;
}
}
pub fn set_joint_qd(&mut self, i: usize, values: &[f32]) {
let body = &self.bodies[i];
let dof = body.joint_type.dof();
let start = body.v_index;
for (j, &val) in values.iter().take(dof).enumerate() {
self.qd[start + j] = val;
self.qd_compensation[start + j] = 0.0;
}
}
pub fn set_joint_tau(&mut self, i: usize, values: &[f32]) {
let body = &self.bodies[i];
let dof = body.joint_type.dof();
let start = body.v_index;
for (j, &val) in values.iter().take(dof).enumerate() {
self.tau[start + j] = val;
}
}
pub fn set_external_force(&mut self, i: usize, force: SpatialVector) {
self.f_ext[i] = force;
}
pub fn clear_external_forces(&mut self) {
for f in &mut self.f_ext {
*f = SpatialVector::zero();
}
}
pub fn reset(&mut self) {
for body in &self.bodies {
let default_q = body.joint_type.default_q();
for (j, &val) in default_q.iter().enumerate() {
self.q[body.q_index + j] = val;
}
}
self.qd.fill(0.0);
self.qdd.fill(0.0);
self.tau.fill(0.0);
self.q_compensation.fill(0.0);
self.qd_compensation.fill(0.0);
self.clear_external_forces();
}
pub fn normalize_quaternions(&mut self) {
for body in &self.bodies {
let npos = body.joint_type.n_pos();
if npos > 0 {
let start = body.q_index;
let q_slice = &mut self.q.as_mut_slice()[start..start + npos];
body.joint_type.normalize_q(q_slice);
}
}
}
pub fn iter_base_to_tip(&self) -> impl Iterator<Item = usize> {
0..self.bodies.len()
}
pub fn iter_tip_to_base(&self) -> impl Iterator<Item = usize> {
(0..self.bodies.len()).rev()
}
pub fn is_root(&self, i: usize) -> bool {
self.bodies[i].parent < 0
}
pub fn is_leaf(&self, i: usize) -> bool {
self.children[i].is_empty()
}
pub fn joint_transform(&self, i: usize) -> SpatialTransform {
let body = &self.bodies[i];
body.joint_type.transform(self.joint_q(i))
}
pub fn joint_motion_subspace(&self, i: usize) -> Vec<SpatialVector> {
let body = &self.bodies[i];
body.joint_type.motion_subspace(self.joint_q(i))
}
pub fn world_transform(&self, i: usize) -> SpatialTransform {
let body = &self.bodies[i];
let x_j = body.joint_type.transform(self.joint_q(i));
let x_local = x_j.compose(&body.x_tree);
if body.parent < 0 {
x_local
} else {
let x_parent = self.world_transform(body.parent as usize);
x_parent.compose(&x_local)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::{Matrix3, Vector3};
fn make_simple_inertia(mass: f32) -> SpatialInertia {
SpatialInertia::from_mass_inertia_at_com(mass, Matrix3::identity() * 0.01 * mass)
}
#[test]
fn test_empty_body() {
let ab = ArticulatedBody::new();
assert_eq!(ab.body_count(), 0);
assert_eq!(ab.dof_count(), 0);
assert_eq!(ab.nq(), 0);
}
#[test]
fn test_single_revolute() {
let mut ab = ArticulatedBody::new();
let idx = ab.add_body(
"link1",
-1,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
assert_eq!(idx, 0);
assert_eq!(ab.body_count(), 1);
assert_eq!(ab.dof_count(), 1);
assert_eq!(ab.nq(), 1);
assert!(ab.is_root(0));
assert!(ab.is_leaf(0));
}
#[test]
fn test_serial_chain_6dof() {
let mut ab = ArticulatedBody::new();
for i in 0..6 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
ab.add_body(
format!("link{i}"),
parent,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.1, 0.0, 0.0)),
);
}
assert_eq!(ab.body_count(), 6);
assert_eq!(ab.dof_count(), 6);
assert_eq!(ab.nq(), 6);
assert!(ab.is_root(0));
assert!(!ab.is_root(1));
assert!(ab.is_leaf(5));
assert!(!ab.is_leaf(0));
assert_eq!(ab.children(0), &[1]);
assert_eq!(ab.children(4), &[5]);
}
#[test]
fn test_branching_tree() {
let mut ab = ArticulatedBody::new();
let torso = ab.add_body(
"torso",
-1,
GenJoint::Fixed,
make_simple_inertia(10.0),
SpatialTransform::identity(),
);
let left = ab.add_body(
"left_arm",
torso as i32,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.0, 0.3, 0.0)),
);
let right = ab.add_body(
"right_arm",
torso as i32,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.0, -0.3, 0.0)),
);
assert_eq!(ab.body_count(), 3);
assert_eq!(ab.dof_count(), 2);
assert_eq!(ab.children(torso), &[left, right]);
}
#[test]
fn test_floating_base() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"base",
-1,
GenJoint::Floating,
make_simple_inertia(5.0),
SpatialTransform::identity(),
);
ab.add_body(
"arm",
0,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
assert_eq!(ab.dof_count(), 7); assert_eq!(ab.nq(), 8); }
#[test]
fn test_spherical_dimensions() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"ball",
-1,
GenJoint::Spherical,
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
assert_eq!(ab.dof_count(), 3);
assert_eq!(ab.nq(), 4);
let q = ab.joint_q(0);
assert_eq!(q.len(), 4);
assert!((q[0] - 1.0).abs() < 1e-10); }
#[test]
fn test_joint_dof_types() {
assert_eq!(GenJoint::Fixed.dof(), 0);
assert_eq!(
GenJoint::Revolute {
axis: Vector3::z()
}
.dof(),
1
);
assert_eq!(
GenJoint::Prismatic {
axis: Vector3::x()
}
.dof(),
1
);
assert_eq!(GenJoint::Spherical.dof(), 3);
assert_eq!(GenJoint::Floating.dof(), 6);
assert_eq!(
GenJoint::Planar {
normal: Vector3::z()
}
.dof(),
3
);
}
#[test]
fn test_set_get_joint_state() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"link1",
-1,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.add_body(
"link2",
0,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.5, 0.0, 0.0)),
);
ab.set_joint_q(0, &[1.0]);
ab.set_joint_q(1, &[2.0]);
assert_eq!(ab.joint_q(0), &[1.0]);
assert_eq!(ab.joint_q(1), &[2.0]);
ab.set_joint_qd(0, &[0.5]);
assert_eq!(ab.joint_qd(0), &[0.5]);
}
#[test]
fn test_body_by_name() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"link_a",
-1,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.add_body(
"link_b",
0,
GenJoint::Revolute {
axis: Vector3::x(),
},
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
assert_eq!(ab.body_by_name("link_a"), Some(0));
assert_eq!(ab.body_by_name("link_b"), Some(1));
assert_eq!(ab.body_by_name("nonexistent"), None);
}
#[test]
fn test_reset() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"link1",
-1,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.set_joint_q(0, &[1.5]);
ab.set_joint_qd(0, &[0.5]);
ab.reset();
assert_eq!(ab.joint_q(0), &[0.0]);
assert_eq!(ab.joint_qd(0), &[0.0]);
}
#[test]
fn test_reset_spherical() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"ball",
-1,
GenJoint::Spherical,
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.set_joint_q(0, &[0.5, 0.5, 0.5, 0.5]);
ab.reset();
let q = ab.joint_q(0);
assert!((q[0] - 1.0).abs() < 1e-10); assert!(q[1].abs() < 1e-10);
}
#[test]
fn test_revolute_transform() {
let jt = GenJoint::Revolute {
axis: Vector3::z(),
};
let x = jt.transform(&[std::f32::consts::FRAC_PI_2]);
let p = x.rotation * Vector3::x();
assert!((p.y - 1.0).abs() < 1e-5);
assert!(p.x.abs() < 1e-5);
}
#[test]
fn test_prismatic_transform() {
let jt = GenJoint::Prismatic {
axis: Vector3::x(),
};
let x = jt.transform(&[0.5]);
assert!((x.translation.x - 0.5).abs() < 1e-10);
assert!(x.translation.y.abs() < 1e-10);
}
#[test]
fn test_fixed_transform_identity() {
let jt = GenJoint::Fixed;
let x = jt.transform(&[]);
assert!((x.rotation - nalgebra::Matrix3::identity()).norm() < 1e-10);
}
#[test]
fn test_motion_subspace_revolute() {
let jt = GenJoint::Revolute {
axis: Vector3::z(),
};
let s = jt.motion_subspace(&[0.0]);
assert_eq!(s.len(), 1);
assert!((s[0].angular().z - 1.0).abs() < 1e-10);
assert!(s[0].linear().norm() < 1e-10);
}
#[test]
fn test_motion_subspace_prismatic() {
let jt = GenJoint::Prismatic {
axis: Vector3::y(),
};
let s = jt.motion_subspace(&[0.0]);
assert!(s[0].angular().norm() < 1e-10);
assert!((s[0].linear().y - 1.0).abs() < 1e-10);
}
#[test]
fn test_motion_subspace_floating() {
let jt = GenJoint::Floating;
let s = jt.motion_subspace(&[0.0; 7]);
assert_eq!(s.len(), 6);
}
#[test]
fn test_iteration_order() {
let mut ab = ArticulatedBody::new();
for i in 0..5 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
ab.add_body(
format!("link{i}"),
parent,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
}
let fwd: Vec<usize> = ab.iter_base_to_tip().collect();
assert_eq!(fwd, vec![0, 1, 2, 3, 4]);
let bwd: Vec<usize> = ab.iter_tip_to_base().collect();
assert_eq!(bwd, vec![4, 3, 2, 1, 0]);
}
#[test]
fn test_quadruped_topology() {
let mut ab = ArticulatedBody::new();
let base = ab.add_body(
"base",
-1,
GenJoint::Floating,
make_simple_inertia(10.0),
SpatialTransform::identity(),
);
for leg_name in &["fl", "fr", "rl", "rr"] {
let hip = ab.add_body(
format!("{leg_name}_hip"),
base as i32,
GenJoint::Revolute {
axis: Vector3::y(),
},
make_simple_inertia(0.5),
SpatialTransform::identity(),
);
ab.add_body(
format!("{leg_name}_knee"),
hip as i32,
GenJoint::Revolute {
axis: Vector3::y(),
},
make_simple_inertia(0.3),
SpatialTransform::from_translation(Vector3::new(0.0, 0.0, -0.2)),
);
}
assert_eq!(ab.body_count(), 9);
assert_eq!(ab.dof_count(), 14); assert_eq!(ab.nq(), 15); assert_eq!(ab.children(base).len(), 4);
}
#[test]
fn test_normalize_quaternions() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"ball",
-1,
GenJoint::Spherical,
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.set_joint_q(0, &[2.0, 0.0, 0.0, 0.0]);
ab.normalize_quaternions();
let q = ab.joint_q(0);
let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
assert!((norm - 1.0).abs() < 1e-7);
}
#[test]
fn test_mixed_nq_nv() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"ball",
-1,
GenJoint::Spherical,
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.add_body(
"arm",
0,
GenJoint::Revolute {
axis: Vector3::z(),
},
make_simple_inertia(0.5),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
assert_eq!(ab.nq(), 5); assert_eq!(ab.dof_count(), 4);
assert_eq!(ab.bodies[0].q_index, 0);
assert_eq!(ab.bodies[0].v_index, 0);
assert_eq!(ab.bodies[1].q_index, 4);
assert_eq!(ab.bodies[1].v_index, 3);
ab.set_joint_q(0, &[0.707, 0.0, 0.0, 0.707]);
ab.set_joint_q(1, &[1.5]);
assert_eq!(ab.joint_q(0).len(), 4);
assert_eq!(ab.joint_q(1), &[1.5]);
ab.set_joint_qd(0, &[0.1, 0.2, 0.3]);
ab.set_joint_qd(1, &[0.5]);
assert_eq!(ab.joint_qd(0), &[0.1, 0.2, 0.3]);
assert_eq!(ab.joint_qd(1), &[0.5]);
}
#[test]
fn test_set_and_clear_external_forces() {
let mut ab = ArticulatedBody::new();
ab.add_body(
"link1", -1,
GenJoint::Revolute { axis: Vector3::z() },
make_simple_inertia(1.0),
SpatialTransform::identity(),
);
ab.add_body(
"link2", 0,
GenJoint::Revolute { axis: Vector3::z() },
make_simple_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(1.0, 0.0, 0.0)),
);
let force = SpatialVector::new(Vector3::zeros(), Vector3::new(0.0, -9.81, 0.0));
ab.set_external_force(1, force);
assert!(ab.f_ext[1].data.norm() > 0.0);
ab.clear_external_forces();
for f in &ab.f_ext {
assert!(f.data.norm() < 1e-10, "External force should be zero after clear");
}
}
#[test]
fn test_body_count_matches_add() {
let mut ab = ArticulatedBody::new();
assert_eq!(ab.body_count(), 0);
ab.add_body("a", -1, GenJoint::Fixed, make_simple_inertia(1.0), SpatialTransform::identity());
assert_eq!(ab.body_count(), 1);
ab.add_body("b", 0, GenJoint::Fixed, make_simple_inertia(1.0), SpatialTransform::identity());
assert_eq!(ab.body_count(), 2);
}
#[test]
fn test_set_gravity() {
let mut ab = ArticulatedBody::new();
ab.set_gravity(Vector3::new(0.0, -9.81, 0.0));
assert!(ab.gravity.linear().y > 9.0, "Gravity stored as base accel: {:?}", ab.gravity.linear());
ab.set_gravity(Vector3::new(0.0, -1.62, 0.0));
assert!((ab.gravity.linear().y - 1.62).abs() < 0.01);
}
#[test]
fn intent_body_nq_equals_sum_of_joint_npos() {
let mut body = ArticulatedBody::new();
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Spherical,
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
assert_eq!(body.q.len(), 5, "nq should be 1 + 4 = 5");
assert_eq!(body.qd.len(), 4, "nv should be 1 + 3 = 4");
}
#[test]
fn intent_body_empty_has_zero_dimensions() {
let body = ArticulatedBody::new();
assert_eq!(body.q.len(), 0);
assert_eq!(body.qd.len(), 0);
assert_eq!(body.qdd.len(), 0);
assert_eq!(body.tau.len(), 0);
assert_eq!(body.body_count(), 0);
assert_eq!(body.dof_count(), 0);
}
#[test]
fn intent_body_branching_tree_correct_count() {
let mut body = ArticulatedBody::new();
body.add_body("torso", -1, GenJointType::Fixed,
SpatialInertia::sphere(5.0, 0.2), SpatialTransform::identity());
body.add_body("left_arm", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::from_translation(Vector3::new(-0.5, 0.0, 0.0)));
body.add_body("right_arm", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::from_translation(Vector3::new(0.5, 0.0, 0.0)));
assert_eq!(body.body_count(), 3);
assert_eq!(body.dof_count(), 2); }
#[test]
fn intent_parent_index_always_less_than_child() {
let mut body = ArticulatedBody::new();
body.add_body("root", -1, GenJointType::Fixed, SpatialInertia::zero(), SpatialTransform::identity());
body.add_body("l1", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("l3", 1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
for i in 0..body.body_count() {
let p = body.bodies[i].parent;
if p >= 0 {
assert!((p as usize) < i, "parent({i})={p} must be < {i}");
}
}
}
#[test]
fn intent_state_dimensions_consistent() {
let mut body = ArticulatedBody::new();
body.add_body("f", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("r", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.1), SpatialTransform::identity());
assert_eq!(body.q.len(), body.nq(), "q.len()={} != nq()={}", body.q.len(), body.nq());
assert_eq!(body.qd.len(), body.dof_count(), "qd.len()={} != nv()={}", body.qd.len(), body.dof_count());
assert_eq!(body.qdd.len(), body.dof_count());
assert_eq!(body.tau.len(), body.dof_count());
assert_eq!(body.f_ext.len(), body.body_count());
}
#[test]
fn intent_reset_restores_default_state() {
let mut body = ArticulatedBody::new();
body.add_body("link", -1, GenJointType::Spherical,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.qd[0] = 5.0;
body.qd[1] = 3.0;
body.q[0] = 0.5;
body.reset();
assert!((body.q[0] - 1.0).abs() < 1e-6, "q[0] (w) should be 1.0 after reset, got {}", body.q[0]);
for i in 0..body.dof_count() {
assert!(body.qd[i].abs() < 1e-6, "qd[{i}] should be 0 after reset");
}
}
#[test]
fn test_set_joint_tau() {
let mut body = ArticulatedBody::new();
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.1), SpatialTransform::identity());
body.set_joint_tau(0, &[5.0]);
body.set_joint_tau(1, &[-3.0]);
assert!((body.tau[0] - 5.0).abs() < 1e-6, "tau[0]={}", body.tau[0]);
assert!((body.tau[1] - (-3.0)).abs() < 1e-6, "tau[1]={}", body.tau[1]);
}
#[test]
fn test_joint_transform_revolute() {
let mut body = ArticulatedBody::new();
body.add_body("link", -1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.q[0] = 0.0;
let t = body.joint_transform(0);
let rot_diff = (t.rotation - nalgebra::Matrix3::identity()).norm();
assert!(rot_diff < 1e-5, "q=0 should give identity transform, got diff={rot_diff}");
}
#[test]
fn test_joint_motion_subspace_revolute() {
let mut body = ArticulatedBody::new();
body.add_body("link", -1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let s = body.joint_motion_subspace(0);
assert_eq!(s.len(), 1, "Revolute joint has 1 DOF");
assert!(s[0].angular().z.abs() > 0.5, "revolute Z should have angular Z component");
}
#[test]
fn test_world_transform_chain() {
let mut body = ArticulatedBody::new();
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
let t0 = body.world_transform(0);
let t1 = body.world_transform(1);
assert!(t0.translation.norm() < 1e-4, "root should be at origin");
assert!((t1.translation.y - (-1.0)).abs() < 0.1,
"child should be ~1m below root: y={}", t1.translation.y);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_body_count_matches_additions(n in 1usize..10) {
let mut body = ArticulatedBody::new();
for i in 0..n {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(format!("l{i}"), parent,
GenJoint::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
}
prop_assert_eq!(body.body_count(), n);
prop_assert_eq!(body.dof_count(), n); prop_assert_eq!(body.nq(), n); }
#[test]
fn prop_external_forces_cleared(n in 1usize..5) {
let mut body = ArticulatedBody::new();
for i in 0..n {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(format!("l{i}"), parent,
GenJoint::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
}
for i in 0..n {
body.set_external_force(i, SpatialVector::new(
Vector3::new(1.0, 2.0, 3.0), Vector3::new(4.0, 5.0, 6.0)));
}
body.clear_external_forces();
for i in 0..n {
prop_assert!(body.f_ext[i].data.norm() < 1e-10,
"external force[{i}] should be zero after clear");
}
}
}
#[test]
fn edge_fixed_joint_contributes_zero_dof() {
let mut body = ArticulatedBody::new();
body.add_body("root", -1, GenJoint::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("fixed", 0, GenJoint::Fixed,
SpatialInertia::sphere(2.0, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
body.add_body("tip", 1, GenJoint::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
assert_eq!(body.body_count(), 3, "should have 3 bodies");
assert_eq!(body.dof_count(), 2, "Fixed joint adds 0 DOF: 1+0+1=2");
assert_eq!(body.nq(), 2, "position params: 1+0+1=2");
}
#[test]
fn intent_body_by_name_returns_correct_index() {
let mut body = ArticulatedBody::new();
body.add_body("alpha", -1, GenJoint::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
body.add_body("beta", 0, GenJoint::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.1), SpatialTransform::identity());
body.add_body("gamma", 1, GenJoint::Prismatic { axis: Vector3::y() },
SpatialInertia::sphere(0.3, 0.1), SpatialTransform::identity());
assert_eq!(body.body_by_name("alpha"), Some(0));
assert_eq!(body.body_by_name("beta"), Some(1));
assert_eq!(body.body_by_name("gamma"), Some(2));
assert_eq!(body.body_by_name("nonexistent"), None);
}
}