use nalgebra::DMatrix;
use super::body::ArticulatedBody;
use super::spatial::{SpatialInertia, SpatialVector};
pub fn crba_mass_matrix(body: &ArticulatedBody) -> DMatrix<f32> {
let n = body.dof_count();
let nb = body.body_count();
if n == 0 {
return DMatrix::zeros(0, 0);
}
let mut h = DMatrix::zeros(n, n);
let mut i_c: Vec<SpatialInertia> = body.bodies.iter().map(|b| b.inertia.clone()).collect();
let x_up: Vec<_> = (0..nb)
.map(|i| {
let bd = &body.bodies[i];
let x_j = bd.joint_type.transform(body.joint_q(i));
x_j.compose(&bd.x_tree)
})
.collect();
for i in body.iter_tip_to_base() {
let bd = &body.bodies[i];
if bd.parent >= 0 {
let parent = bd.parent as usize;
let x_inv = x_up[i].inverse();
let i_c_parent = x_inv.apply_inertia(&i_c[i]);
i_c[parent] = i_c[parent].add(&i_c_parent);
}
}
let s_all: Vec<Vec<SpatialVector>> = (0..nb)
.map(|i| body.bodies[i].joint_type.motion_subspace(body.joint_q(i)))
.collect();
for i in 0..nb {
let dof_i = body.bodies[i].joint_type.dof();
if dof_i == 0 {
continue;
}
let v_idx_i = body.bodies[i].v_index;
for r in 0..dof_i {
let ic_sr = i_c[i].mul_vector(&s_all[i][r]);
for c in r..dof_i {
let val = s_all[i][c].dot(&ic_sr);
h[(v_idx_i + r, v_idx_i + c)] = val;
h[(v_idx_i + c, v_idx_i + r)] = val; }
}
let mut f_cols: Vec<SpatialVector> = s_all[i]
.iter()
.map(|s| i_c[i].mul_vector(s))
.collect();
let mut j = i;
while body.bodies[j].parent >= 0 {
let x_inv = x_up[j].inverse();
f_cols = f_cols.iter().map(|f| x_inv.apply_force(f)).collect();
j = body.bodies[j].parent as usize;
let dof_j = body.bodies[j].joint_type.dof();
if dof_j == 0 {
continue;
}
let v_idx_j = body.bodies[j].v_index;
for r in 0..dof_j {
for c in 0..dof_i {
let val = s_all[j][r].dot(&f_cols[c]);
h[(v_idx_j + r, v_idx_i + c)] = val;
h[(v_idx_i + c, v_idx_j + r)] = val; }
}
}
}
h
}
pub fn bias_forces(body: &mut ArticulatedBody) -> nalgebra::DVector<f32> {
let saved_qdd = body.qdd.clone();
let saved_tau = body.tau.clone();
body.qdd.fill(0.0);
body.tau.fill(0.0);
let n = body.dof_count();
let nb = body.body_count();
let mut c = nalgebra::DVector::zeros(n);
if nb == 0 {
return c;
}
let x_up: Vec<_> = (0..nb)
.map(|i| {
let bd = &body.bodies[i];
let x_j = bd.joint_type.transform(body.joint_q(i));
x_j.compose(&bd.x_tree)
})
.collect();
let s_all: Vec<Vec<SpatialVector>> = (0..nb)
.map(|i| body.bodies[i].joint_type.motion_subspace(body.joint_q(i)))
.collect();
let mut v = vec![SpatialVector::zero(); nb];
let mut a = vec![SpatialVector::zero(); nb];
for i in body.iter_base_to_tip() {
let bd = &body.bodies[i];
let qd_slice = body.joint_qd(i);
let v_j = compute_joint_velocity(&s_all[i], qd_slice);
if bd.parent < 0 {
v[i] = v_j.clone();
a[i] = x_up[i].apply_motion(&body.gravity);
a[i] = a[i].add(&v[i].cross_motion(&v_j));
} else {
let parent = bd.parent as usize;
let v_parent = x_up[i].apply_motion(&v[parent]);
v[i] = v_parent.add(&v_j);
let a_parent = x_up[i].apply_motion(&a[parent]);
a[i] = a_parent.add(&v[i].cross_motion(&v_j));
}
}
let mut f = vec![SpatialVector::zero(); nb];
for i in body.iter_tip_to_base() {
let bd = &body.bodies[i];
let iv = bd.inertia.mul_vector(&v[i]);
let own_force = bd.inertia.mul_vector(&a[i]).add(&v[i].cross_force(&iv)).sub(&body.f_ext[i]);
f[i] = f[i].add(&own_force);
let dof = bd.joint_type.dof();
if dof > 0 {
let v_idx = bd.v_index;
for j in 0..dof {
c[v_idx + j] = s_all[i][j].dot(&f[i]);
}
}
if bd.parent >= 0 {
let parent = bd.parent as usize;
let x_inv = x_up[i].inverse();
f[parent] = f[parent].add(&x_inv.apply_force(&f[i]));
}
}
body.qdd = saved_qdd;
body.tau = saved_tau;
c
}
fn compute_joint_velocity(s: &[SpatialVector], qd: &[f32]) -> SpatialVector {
let mut result = SpatialVector::zero();
for (k, s_col) in s.iter().enumerate() {
if k < qd.len() {
result = result.add(&s_col.scale(qd[k]));
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::body::GenJointType;
use super::super::spatial::{SpatialInertia, SpatialTransform};
use approx::assert_relative_eq;
use nalgebra::{Matrix3, Vector3};
fn make_inertia(mass: f32) -> SpatialInertia {
SpatialInertia::from_mass_inertia_at_com(mass, Matrix3::identity() * 0.01 * mass)
}
#[test]
fn test_crba_empty() {
let body = ArticulatedBody::new();
let h = crba_mass_matrix(&body);
assert_eq!(h.nrows(), 0);
assert_eq!(h.ncols(), 0);
}
#[test]
fn test_crba_single_revolute() {
let mut body = ArticulatedBody::new();
let i_zz = 0.1_f32;
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::from_mass_inertia_at_com(
1.0,
Matrix3::from_diagonal(&Vector3::new(0.01, 0.01, i_zz)),
),
SpatialTransform::identity(),
);
let h = crba_mass_matrix(&body);
assert_eq!(h.nrows(), 1);
assert_eq!(h.ncols(), 1);
assert_relative_eq!(h[(0, 0)], i_zz, epsilon = 1e-6);
}
#[test]
fn test_crba_symmetry() {
let mut body = ArticulatedBody::new();
for i in 0..4 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(
format!("link{i}"),
parent,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
}
body.set_joint_q(0, &[0.3]);
body.set_joint_q(1, &[-0.5]);
body.set_joint_q(2, &[0.7]);
let h = crba_mass_matrix(&body);
for i in 0..h.nrows() {
for j in 0..h.ncols() {
assert_relative_eq!(h[(i, j)], h[(j, i)], epsilon = 1e-6);
}
}
}
#[test]
fn test_crba_positive_definite() {
let mut body = ArticulatedBody::new();
for i in 0..3 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(
format!("link{i}"),
parent,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
}
let h = crba_mass_matrix(&body);
let eigenvalues = h.symmetric_eigenvalues();
for &ev in eigenvalues.iter() {
assert!(ev > 0.0, "Mass matrix eigenvalue should be positive: {ev}");
}
}
#[test]
fn test_crba_dimensions() {
let mut body = ArticulatedBody::new();
body.add_body(
"base",
-1,
GenJointType::Floating,
make_inertia(5.0),
SpatialTransform::identity(),
);
body.add_body(
"arm1",
0,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
body.add_body(
"arm2",
1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
let h = crba_mass_matrix(&body);
assert_eq!(h.nrows(), 8); assert_eq!(h.ncols(), 8);
}
#[test]
fn test_crba_aba_consistency() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
for i in 0..3 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(
format!("link{i}"),
parent,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
}
body.set_joint_q(0, &[0.3]);
body.set_joint_q(1, &[-0.2]);
body.set_joint_tau(0, &[0.5]);
body.set_joint_tau(1, &[-0.3]);
let h = crba_mass_matrix(&body);
let c = bias_forces(&mut body);
let tau_minus_c = &body.tau - &c;
let h_inv = h.clone().try_inverse().expect("H should be invertible");
let qdd_crba = h_inv * tau_minus_c;
super::super::aba::aba_forward_dynamics(&mut body);
let qdd_aba = body.qdd.clone();
for i in 0..body.dof_count() {
assert_relative_eq!(qdd_crba[i], qdd_aba[i], epsilon = 0.1);
}
}
#[test]
fn test_bias_forces_zero_velocity() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::identity(),
);
let c = bias_forces(&mut body);
for i in 0..c.len() {
assert_relative_eq!(c[i], 0.0, epsilon = 1e-8);
}
}
#[test]
fn test_crba_heavier_body_larger_diagonal() {
let make_body = |mass: f32| {
let mut body = ArticulatedBody::new();
body.add_body(
"link", -1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(mass),
SpatialTransform::identity(),
);
body
};
let mut light = make_body(1.0);
let mut heavy = make_body(10.0);
let m_light = crba_mass_matrix(&mut light);
let m_heavy = crba_mass_matrix(&mut heavy);
assert!(
m_heavy[(0, 0)] > m_light[(0, 0)],
"Heavier body should have larger mass matrix diagonal: light={}, heavy={}",
m_light[(0, 0)], m_heavy[(0, 0)]
);
}
#[test]
fn test_crba_multi_link_finite() {
let mut body = ArticulatedBody::new();
for i in 0..4 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(
format!("link{i}"), parent,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(0.5),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
}
let m = crba_mass_matrix(&mut body);
for i in 0..m.nrows() {
for j in 0..m.ncols() {
assert!(m[(i, j)].is_finite(), "M[{i},{j}] should be finite: {}", m[(i, j)]);
}
}
}
#[test]
fn intent_crba_mass_matrix_symmetric() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(2.0), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
body.add_body("l3", 1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(0.5), SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
let m = crba_mass_matrix(&mut body);
for i in 0..m.nrows() {
for j in i+1..m.ncols() {
assert!(
(m[(i,j)] - m[(j,i)]).abs() < 1e-5,
"mass matrix not symmetric: M[{},{}]={} vs M[{},{}]={}",
i, j, m[(i,j)], j, i, m[(j,i)]
);
}
}
}
#[test]
fn intent_crba_mass_matrix_positive_definite() {
let mut body = ArticulatedBody::new();
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::x() },
make_inertia(1.0), SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
let m = crba_mass_matrix(&mut body);
for i in 0..m.nrows() {
assert!(m[(i,i)] > 0.0, "diagonal M[{i},{i}] should be positive: {}", m[(i,i)]);
}
}
#[test]
fn intent_crba_heavier_body_larger_diagonal() {
let mut light = ArticulatedBody::new();
light.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::identity());
let mut heavy = ArticulatedBody::new();
heavy.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(10.0), SpatialTransform::identity());
let m_light = crba_mass_matrix(&mut light);
let m_heavy = crba_mass_matrix(&mut heavy);
assert!(
m_heavy[(0,0)] > m_light[(0,0)],
"heavier body should have larger diagonal: light={}, heavy={}",
m_light[(0,0)], m_heavy[(0,0)]
);
}
#[test]
fn determinism_crba_same_config_same_matrix() {
let make_body = || {
let mut body = ArticulatedBody::new();
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(2.0), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::x() },
make_inertia(1.0), SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
body.q[0] = 0.7;
body.q[1] = -0.3;
body
};
let mut a = make_body();
let mut b = make_body();
let ma = crba_mass_matrix(&mut a);
let mb = crba_mass_matrix(&mut b);
for i in 0..ma.nrows() {
for j in 0..ma.ncols() {
assert_eq!(ma[(i,j)], mb[(i,j)],
"CRBA must be deterministic: M[{},{}] differs", i, j);
}
}
}
#[test]
fn intent_crba_aba_consistency_m_qdd_equals_tau_minus_bias() {
use super::super::aba::aba_forward_dynamics;
use super::super::rnea::rnea_inverse_dynamics;
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(2.0), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::x() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
body.q[0] = 0.5;
body.q[1] = -0.3;
body.qd[0] = 1.0;
body.qd[1] = -0.5;
body.tau[0] = 2.0;
body.tau[1] = -1.0;
aba_forward_dynamics(&mut body);
let qdd = body.qdd.clone();
let m = crba_mass_matrix(&body);
let mut body_bias = body.clone();
body_bias.qdd.fill(0.0);
let (bias, _) = rnea_inverse_dynamics(&body_bias);
let m_qdd = &m * &qdd;
let tau_minus_bias = &body.tau - &bias;
for i in 0..m_qdd.len() {
assert!((m_qdd[i] - tau_minus_bias[i]).abs() < 0.5,
"M*qdd should equal tau-bias at [{i}]: M*qdd={}, tau-bias={}",
m_qdd[i], tau_minus_bias[i]);
}
}
#[test]
fn intent_crba_all_eigenvalues_positive() {
let mut body = ArticulatedBody::new();
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(2.0), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::x() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
body.q[0] = 1.2;
body.q[1] = -0.7;
let m = crba_mass_matrix(&body);
let n = m.nrows();
for i in 0..n {
assert!(m[(i, i)] > 0.0,
"mass matrix diagonal must be positive: M[{i},{i}]={}", m[(i, i)]);
}
let qd = nalgebra::DVector::from_vec(vec![1.0, -0.5]);
let energy = qd.dot(&(&m * &qd));
assert!(energy > 0.0,
"kinetic energy must be positive for non-zero velocity: E={}", energy);
}
#[test]
fn property_crba_mass_matrix_positive_diagonal() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
for i in 0..3 {
let parent = if i == 0 { -1 } else { (i - 1) as i32 };
body.add_body(&format!("l{i}"), parent,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
}
let m = crba_mass_matrix(&body);
for i in 0..m.nrows() {
assert!(m[(i, i)] > 0.0,
"M[{i},{i}]={} must be strictly positive", m[(i, i)]);
}
let eigenvalues = m.symmetric_eigenvalues();
for &ev in eigenvalues.iter() {
assert!(ev > 0.0, "All eigenvalues must be positive: {ev}");
}
}
#[test]
fn intent_crba_empty_body_zero_matrix() {
let body = ArticulatedBody::new();
let m = crba_mass_matrix(&body);
assert_eq!(m.nrows(), 0);
assert_eq!(m.ncols(), 0);
}
#[test]
fn intent_crba_floating_base_dimensions() {
let mut body = ArticulatedBody::new();
body.add_body("base", -1, GenJointType::Floating,
SpatialInertia::sphere(5.0, 0.3), SpatialTransform::identity());
body.add_body("arm1", 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("arm2", 1, GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.1),
SpatialTransform::from_translation(Vector3::new(0.5, 0.0, 0.0)));
let m = crba_mass_matrix(&body);
assert_eq!(m.nrows(), 8, "6+1+1=8 DOF");
assert_eq!(m.ncols(), 8);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_crba_symmetric(
q0 in -2.0f32..2.0,
q1 in -2.0f32..2.0,
) {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("l1", -1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::identity());
body.add_body("l2", 0,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(0.5),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
body.q[0] = q0;
body.q[1] = q1;
let m = crba_mass_matrix(&body);
for i in 0..m.nrows() {
for j in i+1..m.ncols() {
prop_assert!((m[(i,j)] - m[(j,i)]).abs() < 1e-5,
"M must be symmetric: M[{i},{j}]={} vs M[{j},{i}]={}", m[(i,j)], m[(j,i)]);
}
}
}
#[test]
fn prop_crba_positive_definite_energy(
q in -2.0f32..2.0,
) {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::identity());
body.q[0] = q;
let m = crba_mass_matrix(&body);
let qd = nalgebra::DVector::from_vec(vec![1.0]);
let energy = qd.dot(&(&m * &qd));
prop_assert!(energy > 0.0,
"kinetic energy must be positive: E={energy} at q={q}");
}
}
proptest! {
#[test]
fn prop_crba_floating_base_6x6_block_spd(
q0 in -2.0f32..2.0,
) {
let mut body = ArticulatedBody::new();
body.add_body("base", -1, GenJointType::Floating,
SpatialInertia::sphere(2.0, 0.2), SpatialTransform::identity());
body.set_joint_q(0, &[q0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
let m = crba_mass_matrix(&body);
prop_assert_eq!(m.nrows(), 6);
for i in 0..6 {
prop_assert!(m[(i, i)] > 0.0, "diagonal M[{i},{i}]={} must be positive", m[(i, i)]);
}
}
}
#[test]
fn test_bias_forces_nonzero_with_velocity() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(0.5),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
body.q[0] = 0.5;
body.qd[0] = 2.0;
body.qd[1] = -1.0;
let c = bias_forces(&mut body);
let mag: f32 = c.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(mag > 0.1, "bias forces should be nonzero with gravity + velocity: ||c||={mag}");
}
#[test]
fn test_bias_forces_equals_gravity_at_zero_velocity() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
body.q[0] = 0.5;
let c = bias_forces(&mut body);
let tau_g = {
use super::super::rnea::gravity_compensation;
gravity_compensation(&body)
};
for i in 0..c.len() {
assert!((c[i] - tau_g[i]).abs() < 0.1,
"bias[{i}]={} should match gravity_comp[{i}]={}", c[i], tau_g[i]);
}
}
}