use nalgebra::DVector;
use super::body::ArticulatedBody;
use super::spatial::{SpatialTransform, SpatialVector};
#[derive(Clone, Debug)]
pub struct RneaData {
pub v: Vec<SpatialVector>,
pub a: Vec<SpatialVector>,
pub f: Vec<SpatialVector>,
pub x_up: Vec<SpatialTransform>,
pub s: Vec<Vec<SpatialVector>>,
}
impl RneaData {
pub fn new(n: usize) -> Self {
Self {
v: vec![SpatialVector::zero(); n],
a: vec![SpatialVector::zero(); n],
f: vec![SpatialVector::zero(); n],
x_up: vec![SpatialTransform::identity(); n],
s: vec![Vec::new(); n],
}
}
}
pub fn rnea_inverse_dynamics(body: &ArticulatedBody) -> (DVector<f32>, RneaData) {
let n = body.body_count();
let nv = body.dof_count();
let mut tau = DVector::zeros(nv);
if n == 0 {
return (tau, RneaData::new(0));
}
let mut data = RneaData::new(n);
for i in body.iter_base_to_tip() {
let bd = &body.bodies[i];
let x_j = bd.joint_type.transform(body.joint_q(i));
data.x_up[i] = x_j.compose(&bd.x_tree);
data.s[i] = bd.joint_type.motion_subspace(body.joint_q(i));
let _dof = bd.joint_type.dof();
let qd_slice = body.joint_qd(i);
let v_j = spatial_linear_combination(&data.s[i], qd_slice);
let qdd_slice = joint_qdd_slice(body, i);
let a_j = spatial_linear_combination(&data.s[i], qdd_slice);
if bd.parent < 0 {
data.v[i] = v_j.clone();
let a_gravity = data.x_up[i].apply_motion(&body.gravity);
let c_i = data.v[i].cross_motion(&v_j);
data.a[i] = a_gravity.add(&a_j).add(&c_i);
} else {
let parent = bd.parent as usize;
let v_parent = data.x_up[i].apply_motion(&data.v[parent]);
data.v[i] = v_parent.add(&v_j);
let a_parent = data.x_up[i].apply_motion(&data.a[parent]);
let c_i = data.v[i].cross_motion(&v_j);
data.a[i] = a_parent.add(&a_j).add(&c_i);
}
}
for i in body.iter_tip_to_base() {
let bd = &body.bodies[i];
let i_a = bd.inertia.mul_vector(&data.a[i]);
let i_v = bd.inertia.mul_vector(&data.v[i]);
let gyroscopic = data.v[i].cross_force(&i_v);
data.f[i] = data.f[i].add(&i_a).add(&gyroscopic).sub(&body.f_ext[i]);
let dof = bd.joint_type.dof();
if dof > 0 {
let v_idx = bd.v_index;
for j in 0..dof {
tau[v_idx + j] = data.s[i][j].dot(&data.f[i]);
}
}
if bd.parent >= 0 {
let parent = bd.parent as usize;
let x_inv = data.x_up[i].inverse();
data.f[parent] = data.f[parent].add(&x_inv.apply_force(&data.f[i]));
}
}
(tau, data)
}
pub fn gravity_compensation(body: &ArticulatedBody) -> DVector<f32> {
let mut temp = body.clone();
temp.qd.fill(0.0);
temp.qdd.fill(0.0);
temp.clear_external_forces();
let (tau, _) = rnea_inverse_dynamics(&temp);
tau
}
pub fn bias_forces_rnea(body: &ArticulatedBody) -> DVector<f32> {
let mut temp = body.clone();
temp.qdd.fill(0.0);
let (tau, _) = rnea_inverse_dynamics(&temp);
tau
}
pub fn coriolis_forces(body: &ArticulatedBody) -> DVector<f32> {
let bias = bias_forces_rnea(body);
let grav = gravity_compensation(body);
bias - grav
}
fn joint_qdd_slice(body: &ArticulatedBody, i: usize) -> &[f32] {
let bd = &body.bodies[i];
let dof = bd.joint_type.dof();
if dof == 0 {
return &[];
}
&body.qdd.as_slice()[bd.v_index..bd.v_index + dof]
}
fn spatial_linear_combination(s: &[SpatialVector], coeffs: &[f32]) -> SpatialVector {
let mut result = SpatialVector::zero();
for (k, s_col) in s.iter().enumerate() {
if k < coeffs.len() {
result = result.add(&s_col.scale(coeffs[k]));
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::aba::aba_forward_dynamics;
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_rnea_empty() {
let body = ArticulatedBody::new();
let (tau, _) = rnea_inverse_dynamics(&body);
assert_eq!(tau.len(), 0);
}
#[test]
fn test_gravity_compensation_single_pendulum() {
let mass = 1.0_f32;
let length = 0.5_f32;
let g = 9.81_f32;
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -g, 0.0));
let inertia = SpatialInertia::from_mass_inertia(
mass,
Vector3::new(length, 0.0, 0.0),
Matrix3::from_diagonal(&Vector3::new(0.001, 0.001, 0.001)),
);
body.add_body(
"pendulum",
-1,
GenJointType::Revolute { axis: Vector3::z() },
inertia,
SpatialTransform::identity(),
);
let tau = gravity_compensation(&body);
let expected = mass * g * length;
assert_relative_eq!(tau[0], expected, epsilon = 0.01);
}
#[test]
fn test_rnea_aba_round_trip() {
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_qd(0, &[1.0]);
body.set_joint_qd(1, &[-0.5]);
body.set_joint_tau(0, &[0.5]);
body.set_joint_tau(1, &[-0.3]);
body.set_joint_tau(2, &[0.1]);
let original_tau = body.tau.clone();
aba_forward_dynamics(&mut body);
let _qdd = body.qdd.clone();
let (tau_recovered, _) = rnea_inverse_dynamics(&body);
for i in 0..body.dof_count() {
assert_relative_eq!(
tau_recovered[i],
original_tau[i],
epsilon = 0.01
);
}
}
#[test]
fn test_coriolis_zero_at_zero_velocity() {
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.5]);
let c = coriolis_forces(&body);
for i in 0..c.len() {
assert_relative_eq!(c[i], 0.0, epsilon = 1e-6);
}
}
#[test]
fn test_gravity_compensation_balances_aba() {
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.5]);
body.set_joint_q(2, &[0.7]);
let tau_grav = gravity_compensation(&body);
body.tau = tau_grav;
aba_forward_dynamics(&mut body);
for i in 0..body.dof_count() {
assert_relative_eq!(body.qdd[i], 0.0, epsilon = 1e-4);
}
}
#[test]
fn test_rnea_zero_gravity_zero_state() {
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 (tau, _) = rnea_inverse_dynamics(&body);
assert_relative_eq!(tau[0], 0.0, epsilon = 1e-8);
}
#[test]
fn test_rnea_with_acceleration() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
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(i_zz, i_zz, i_zz)),
),
SpatialTransform::identity(),
);
body.qdd[0] = 10.0;
let (tau, _) = rnea_inverse_dynamics(&body);
assert_relative_eq!(tau[0], 1.0, epsilon = 0.01);
}
#[test]
fn test_rnea_cached_data() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"link1",
-1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.add_body(
"link2",
0,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
body.set_joint_qd(0, &[1.0]);
let (_, data) = rnea_inverse_dynamics(&body);
assert_eq!(data.v.len(), 2);
assert_eq!(data.a.len(), 2);
assert_eq!(data.f.len(), 2);
assert!(data.v[0].angular().z.abs() > 0.5);
}
#[test]
fn test_rnea_floating_base() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"base",
-1,
GenJointType::Floating,
SpatialInertia::sphere(5.0, 0.2),
SpatialTransform::identity(),
);
body.add_body(
"arm",
0,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
let (tau, _) = rnea_inverse_dynamics(&body);
assert_eq!(tau.len(), 7);
for i in 0..tau.len() {
assert!(tau[i].is_finite(), "tau[{i}] = {}", tau[i]);
}
}
#[test]
fn test_rnea_external_force() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::from_mass_inertia_at_com(
1.0,
Matrix3::from_diagonal(&Vector3::new(0.1, 0.1, 0.1)),
),
SpatialTransform::identity(),
);
body.set_external_force(
0,
SpatialVector::new(Vector3::new(0.0, 0.0, 5.0), Vector3::zeros()),
);
let (tau, _) = rnea_inverse_dynamics(&body);
assert_relative_eq!(tau[0], -5.0, epsilon = 0.1);
}
#[test]
fn test_rnea_aba_consistency() {
use crate::aba::aba_forward_dynamics;
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"link1", -1,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::from_mass_inertia_at_com(
1.0,
Matrix3::from_diagonal(&Vector3::new(0.1, 0.1, 0.1)),
),
SpatialTransform::identity(),
);
body.add_body(
"link2", 0,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::from_mass_inertia_at_com(
0.5,
Matrix3::from_diagonal(&Vector3::new(0.05, 0.05, 0.05)),
),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
body.tau[0] = 5.0;
body.tau[1] = -2.0;
aba_forward_dynamics(&mut body);
let (tau_recovered, _) = rnea_inverse_dynamics(&body);
assert_relative_eq!(tau_recovered[0], 5.0, epsilon = 0.1);
assert_relative_eq!(tau_recovered[1], -2.0, epsilon = 0.1);
}
#[test]
fn test_rnea_zero_state_finite() {
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() },
SpatialInertia::from_mass_inertia_at_com(
1.0,
Matrix3::from_diagonal(&Vector3::new(0.01, 0.01, 0.01)),
),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
}
let (tau, _) = rnea_inverse_dynamics(&body);
for (i, &t) in tau.iter().enumerate() {
assert!(t.is_finite(), "tau[{i}] should be finite: {t}");
}
}
#[test]
fn intent_rnea_gravity_compensation_balances_gravity() {
use crate::aba::aba_forward_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.q[0] = 0.5;
let (tau_grav, _) = rnea_inverse_dynamics(&body);
body.tau[0] = tau_grav[0];
aba_forward_dynamics(&mut body);
assert!(
body.qdd[0].abs() < 0.1,
"gravity-compensated torque should yield near-zero accel: qdd={}",
body.qdd[0]
);
}
#[test]
fn intent_rnea_zero_state_zero_gravity_gives_zero_torques() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, 0.0, 0.0));
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::identity());
let (tau, _) = rnea_inverse_dynamics(&body);
assert!(tau[0].abs() < 1e-6, "zero state + zero gravity → zero torque: {}", tau[0]);
}
#[test]
fn intent_rnea_aba_roundtrip_multi_dof() {
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.3;
body.q[1] = -0.2;
body.qd[0] = 1.0;
body.qd[1] = -0.5;
aba_forward_dynamics(&mut body);
let qdd_aba = vec![body.qdd[0], body.qdd[1]];
body.qdd[0] = qdd_aba[0];
body.qdd[1] = qdd_aba[1];
body.tau = DVector::zeros(2);
let (tau_rnea, _) = rnea_inverse_dynamics(&body);
for i in 0..2 {
assert!(tau_rnea[i].abs() < 0.5,
"RNEA/ABA roundtrip: tau[{i}]={} should be near zero (natural dynamics)",
tau_rnea[i]);
}
}
#[test]
fn intent_rnea_bias_forces_grow_with_velocity() {
use nalgebra::Matrix3;
let mut body_slow = ArticulatedBody::new();
body_slow.set_gravity(Vector3::zeros());
let inertia = SpatialInertia::from_mass_inertia_at_com(
2.0,
Matrix3::new(1.0, 0.0, 0.0,
0.0, 2.0, 0.0,
0.0, 0.0, 3.0),
);
body_slow.add_body("body", -1, GenJointType::Floating, inertia, SpatialTransform::identity());
body_slow.qd[3] = 1.0; body_slow.qd[4] = 0.5; body_slow.qd[5] = 0.3;
let mut body_fast = body_slow.clone();
body_fast.qd[3] = 10.0;
body_fast.qd[4] = 5.0;
body_fast.qd[5] = 3.0;
let (tau_slow, _) = rnea_inverse_dynamics(&body_slow);
let (tau_fast, _) = rnea_inverse_dynamics(&body_fast);
let mag_slow: f32 = tau_slow.iter().map(|t| t * t).sum::<f32>().sqrt();
let mag_fast: f32 = tau_fast.iter().map(|t| t * t).sum::<f32>().sqrt();
assert!(mag_fast > mag_slow,
"higher angular velocity should produce larger gyroscopic torques: slow={mag_slow}, fast={mag_fast}");
}
#[test]
fn property_rnea_linear_in_qdd() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body("link", -1,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::from_mass_inertia_at_com(1.0, Matrix3::identity() * 0.1),
SpatialTransform::identity());
body.qdd[0] = 1.0;
let (tau1, _) = rnea_inverse_dynamics(&body);
body.qdd[0] = 3.0;
let (tau3, _) = rnea_inverse_dynamics(&body);
if tau1[0].abs() > 1e-8 {
let ratio = tau3[0] / tau1[0];
assert!((ratio - 3.0).abs() < 0.1,
"RNEA should be linear in qdd: tau(1)={}, tau(3)={}, ratio={ratio}", tau1[0], tau3[0]);
}
}
#[test]
fn intent_gravity_compensation_produces_finite_torques() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
for i in 0..5 {
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.3, 0.0)));
}
body.q[0] = 0.5; body.q[1] = -0.8; body.q[2] = 1.2;
let tau = gravity_compensation(&body);
for i in 0..tau.len() {
assert!(tau[i].is_finite(), "gravity_comp tau[{i}]={} must be finite", tau[i]);
}
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_rnea_output_always_finite(
q in -2.0f32..2.0,
qd in -5.0f32..5.0,
qdd in -10.0f32..10.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() },
SpatialInertia::from_mass_inertia_at_com(1.0, Matrix3::identity() * 0.1),
SpatialTransform::identity());
body.q[0] = q;
body.qd[0] = qd;
body.qdd[0] = qdd;
let (tau, _) = rnea_inverse_dynamics(&body);
prop_assert!(tau[0].is_finite(), "RNEA tau must be finite for q={q}, qd={qd}, qdd={qdd}");
}
#[test]
fn prop_gravity_compensation_finite(
q in -3.0f32..3.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() },
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
body.q[0] = q;
let tau = gravity_compensation(&body);
prop_assert!(tau[0].is_finite(), "gravity comp must be finite at q={q}");
}
}
#[test]
fn determinism_rnea_same_input_same_output() {
let make = || {
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, -0.5, 0.0)));
body.q[0] = 0.7;
body.q[1] = -0.3;
body.qd[0] = 2.0;
body.qd[1] = -1.5;
body.qdd[0] = 0.5;
body.qdd[1] = -0.2;
body
};
let (tau1, _) = rnea_inverse_dynamics(&make());
let (tau2, _) = rnea_inverse_dynamics(&make());
for i in 0..tau1.len() {
assert_eq!(tau1[i], tau2[i], "RNEA must be deterministic at tau[{i}]");
}
}
#[test]
fn edge_rnea_large_velocity_produces_finite_torques() {
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.qd[0] = 1000.0;
let (tau, _) = rnea_inverse_dynamics(&body);
assert!(tau[0].is_finite(), "large velocity should still give finite torque: {}", tau[0]);
}
#[test]
fn intent_coriolis_nonzero_for_multi_link_with_velocity() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
let inertia = SpatialInertia::from_mass_inertia(
1.0, Vector3::new(0.0, -0.25, 0.0),
Matrix3::from_diagonal(&Vector3::new(0.1, 0.01, 0.1)),
);
body.add_body("l1", -1, GenJointType::Revolute { axis: Vector3::z() },
inertia.clone(), SpatialTransform::identity());
body.add_body("l2", 0, GenJointType::Revolute { axis: Vector3::z() },
inertia,
SpatialTransform::from_translation(Vector3::new(0.0, -0.5, 0.0)));
body.q[0] = 0.5; body.q[1] = -0.3;
body.qd[0] = 5.0;
body.qd[1] = -3.0;
let c = coriolis_forces(&body);
let mag: f32 = c.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(mag > 1e-6,
"multi-link with velocity+nonzero q should have nonzero Coriolis: ||c||={mag}");
}
#[test]
fn intent_coriolis_zero_for_single_revolute() {
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());
body.qd[0] = 5.0;
let c = coriolis_forces(&body);
assert!(c[0].abs() < 1e-4,
"single revolute should have ~zero coriolis: {}", c[0]);
}
}