use nalgebra::{DMatrix, DVector};
use super::body::ArticulatedBody;
use super::spatial::{SpatialInertia, SpatialTransform, SpatialVector};
#[derive(Clone, Debug)]
pub struct AbaData {
pub v: Vec<SpatialVector>,
pub c: Vec<SpatialVector>,
pub i_a: Vec<SpatialInertia>,
pub p_a: Vec<SpatialVector>,
pub a: Vec<SpatialVector>,
pub x_up: Vec<SpatialTransform>,
pub s: Vec<Vec<SpatialVector>>,
pub d: Vec<DMatrix<f32>>,
pub u: Vec<Vec<SpatialVector>>,
pub uu: Vec<DVector<f32>>,
}
impl AbaData {
pub fn new(n: usize) -> Self {
Self {
v: vec![SpatialVector::zero(); n],
c: vec![SpatialVector::zero(); n],
i_a: vec![SpatialInertia::zero(); n],
p_a: vec![SpatialVector::zero(); n],
a: vec![SpatialVector::zero(); n],
x_up: vec![SpatialTransform::identity(); n],
s: vec![Vec::new(); n],
d: vec![DMatrix::zeros(0, 0); n],
u: vec![Vec::new(); n],
uu: vec![DVector::zeros(0); n],
}
}
}
pub fn aba_forward_dynamics(body: &mut ArticulatedBody) -> &DVector<f32> {
let n = body.body_count();
if n == 0 {
return &body.qdd;
}
let mut data = AbaData::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 qd_slice = body.joint_qd(i);
let v_j = compute_joint_velocity(&data.s[i], qd_slice);
if bd.parent < 0 {
data.v[i] = v_j;
data.c[i] = SpatialVector::zero(); } else {
let parent = bd.parent as usize;
let v_parent_transformed = data.x_up[i].apply_motion(&data.v[parent]);
data.v[i] = v_parent_transformed.add(&v_j);
data.c[i] = data.v[i].cross_motion(&v_j);
}
data.i_a[i] = bd.inertia.clone();
let iv = data.i_a[i].mul_vector(&data.v[i]);
data.p_a[i] = data.v[i].cross_force(&iv).sub(&body.f_ext[i]);
}
for i in body.iter_tip_to_base() {
let bd = &body.bodies[i];
let dof = bd.joint_type.dof();
if dof > 0 {
data.u[i] = data.s[i]
.iter()
.map(|s_col| data.i_a[i].mul_vector(s_col))
.collect();
let mut d_mat = DMatrix::zeros(dof, dof);
for r in 0..dof {
for c in 0..dof {
d_mat[(r, c)] = data.s[i][r].dot(&data.u[i][c]);
}
}
data.d[i] = d_mat;
let mut uu_vec = DVector::zeros(dof);
let v_idx = bd.v_index;
for j in 0..dof {
let tau_j = body.tau[v_idx + j];
let st_pa = data.s[i][j].dot(&data.p_a[i]);
uu_vec[j] = tau_j - st_pa;
}
data.uu[i] = uu_vec;
if bd.parent >= 0 {
let parent = bd.parent as usize;
let d_inv_u = solve_dof_system(&data.d[i], &data.uu[i]);
let i_a_reduced = subtract_rank_update(&data.i_a[i], &data.u[i], &data.d[i]);
let ia_c = i_a_reduced.mul_vector(&data.c[i]);
let u_dinv_u = linear_combination(&data.u[i], &d_inv_u);
let p_a_new = data.p_a[i].add(&ia_c).add(&u_dinv_u);
let x_inv = data.x_up[i].inverse();
let i_a_parent = x_inv.apply_inertia(&i_a_reduced);
let p_a_parent = x_inv.apply_force(&p_a_new);
data.i_a[parent] = data.i_a[parent].add(&i_a_parent);
data.p_a[parent] = data.p_a[parent].add(&p_a_parent);
}
} else if bd.parent >= 0 {
let parent = bd.parent as usize;
let x_inv = data.x_up[i].inverse();
let ia_c = data.i_a[i].mul_vector(&data.c[i]);
let p_a_new = data.p_a[i].add(&ia_c);
data.i_a[parent] = data.i_a[parent].add(&x_inv.apply_inertia(&data.i_a[i]));
data.p_a[parent] = data.p_a[parent].add(&x_inv.apply_force(&p_a_new));
}
}
for i in body.iter_base_to_tip() {
let bd = &body.bodies[i];
let dof = bd.joint_type.dof();
let a_parent = if bd.parent < 0 {
body.gravity.clone()
} else {
data.a[bd.parent as usize].clone()
};
let a_parent_local = data.x_up[i].apply_motion(&a_parent);
if dof > 0 {
let a_plus_c = a_parent_local.add(&data.c[i]);
let mut rhs = data.uu[i].clone();
for j in 0..dof {
rhs[j] -= data.u[i][j].dot(&a_plus_c);
}
let qdd_joint = solve_dof_system(&data.d[i], &rhs);
let v_idx = bd.v_index;
for j in 0..dof {
body.qdd[v_idx + j] = qdd_joint[j];
}
let s_qdd = linear_combination(&data.s[i], &qdd_joint);
data.a[i] = a_parent_local.add(&data.c[i]).add(&s_qdd);
} else {
data.a[i] = a_parent_local.add(&data.c[i]);
}
}
&body.qdd
}
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
}
fn linear_combination(vectors: &[SpatialVector], coeffs: &DVector<f32>) -> SpatialVector {
let mut result = SpatialVector::zero();
for (k, v) in vectors.iter().enumerate() {
if k < coeffs.len() {
result = result.add(&v.scale(coeffs[k]));
}
}
result
}
fn solve_dof_system(d: &DMatrix<f32>, b: &DVector<f32>) -> DVector<f32> {
let n = d.nrows();
if n == 0 {
return DVector::zeros(0);
}
if n == 1 {
let d_val = d[(0, 0)];
if d_val.abs() > 1e-6 {
return DVector::from_element(1, b[0] / d_val);
}
return DVector::zeros(1);
}
if let Some(lu) = nalgebra::linalg::LU::new(d.clone()).try_inverse() {
lu * b
} else {
let reg = d + DMatrix::identity(n, n) * 1e-8;
if let Some(inv) = nalgebra::linalg::LU::new(reg).try_inverse() {
inv * b
} else {
DVector::zeros(n)
}
}
}
fn subtract_rank_update(
i_a: &SpatialInertia,
u_cols: &[SpatialVector],
d: &DMatrix<f32>,
) -> SpatialInertia {
let dof = u_cols.len();
if dof == 0 {
return i_a.clone();
}
let d_inv = if dof == 1 {
let d_val = d[(0, 0)];
if d_val.abs() > 1e-6 {
DMatrix::from_element(1, 1, 1.0 / d_val)
} else {
DMatrix::zeros(1, 1)
}
} else {
nalgebra::linalg::LU::new(d.clone())
.try_inverse()
.unwrap_or_else(|| DMatrix::zeros(dof, dof))
};
let mut u_mat = DMatrix::zeros(6, dof);
for (j, col) in u_cols.iter().enumerate() {
for r in 0..6 {
u_mat[(r, j)] = col.data[r];
}
}
let update = &u_mat * &d_inv * u_mat.transpose();
let mut result_data = i_a.data;
for r in 0..6 {
for c in 0..6 {
result_data[(r, c)] -= update[(r, c)];
}
}
SpatialInertia {
data: result_data,
mass: i_a.mass,
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::body::GenJointType;
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_aba_empty() {
let mut body = ArticulatedBody::new();
let qdd = aba_forward_dynamics(&mut body);
assert_eq!(qdd.len(), 0);
}
#[test]
fn test_aba_free_fall() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"free",
-1,
GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::identity(),
);
aba_forward_dynamics(&mut body);
assert_relative_eq!(body.qdd[0], 0.0, epsilon = 1e-6); assert_relative_eq!(body.qdd[1], -9.81, epsilon = 1e-4); assert_relative_eq!(body.qdd[2], 0.0, epsilon = 1e-6); assert_relative_eq!(body.qdd[3], 0.0, epsilon = 1e-6); assert_relative_eq!(body.qdd[4], 0.0, epsilon = 1e-6); assert_relative_eq!(body.qdd[5], 0.0, epsilon = 1e-6); }
#[test]
fn test_aba_single_pendulum_gravity_torque() {
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(),
);
aba_forward_dynamics(&mut body);
assert!(body.qdd[0] < 0.0, "Pendulum should accelerate under gravity: {}", body.qdd[0]);
assert!(body.qdd[0].abs() > 1.0, "Acceleration should be significant");
}
#[test]
fn test_aba_zero_gravity_no_acceleration() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
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.5, 0.0, 0.0)),
);
aba_forward_dynamics(&mut body);
for i in 0..body.dof_count() {
assert_relative_eq!(body.qdd[i], 0.0, epsilon = 1e-8);
}
}
#[test]
fn test_aba_applied_torque() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
let inertia = SpatialInertia::from_mass_inertia_at_com(
1.0,
Matrix3::from_diagonal(&Vector3::new(0.1, 0.1, 0.1)),
);
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
inertia,
SpatialTransform::identity(),
);
body.set_joint_tau(0, &[1.0]);
aba_forward_dynamics(&mut body);
assert_relative_eq!(body.qdd[0], 10.0, epsilon = 0.1);
}
#[test]
fn test_aba_two_link_torque() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
let inertia = SpatialInertia::from_mass_inertia_at_com(
1.0,
Matrix3::from_diagonal(&Vector3::new(0.01, 0.01, 0.01)),
);
body.add_body(
"link1",
-1,
GenJointType::Revolute { axis: Vector3::z() },
inertia.clone(),
SpatialTransform::identity(),
);
body.add_body(
"link2",
0,
GenJointType::Revolute { axis: Vector3::z() },
inertia,
SpatialTransform::from_translation(Vector3::new(0.1, 0.0, 0.0)),
);
body.set_joint_tau(1, &[1.0]);
aba_forward_dynamics(&mut body);
assert!(body.qdd[1] > 0.0, "Distal joint should accelerate: qdd[1]={}", body.qdd[1]);
assert!(body.qdd[0].is_finite(), "Joint 0 should be finite");
}
#[test]
fn test_aba_prismatic_free_fall() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"slider",
-1,
GenJointType::Prismatic { axis: Vector3::new(0.0, -1.0, 0.0) },
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::identity(),
);
aba_forward_dynamics(&mut body);
assert_relative_eq!(body.qdd[0], 9.81, epsilon = 0.1);
}
#[test]
fn test_aba_fixed_joint_no_dof() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body(
"base",
-1,
GenJointType::Fixed,
make_inertia(1.0),
SpatialTransform::identity(),
);
body.add_body(
"arm",
0,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.5, 0.0, 0.0)),
);
assert_eq!(body.dof_count(), 1);
body.set_joint_tau(1, &[0.5]);
aba_forward_dynamics(&mut body);
assert!(body.qdd[0] > 0.0, "Applied torque should produce acceleration");
}
#[test]
fn test_aba_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, 2.0), Vector3::zeros()),
);
aba_forward_dynamics(&mut body);
assert!(body.qdd[0].abs() > 0.1, "External force should cause acceleration");
}
#[test]
fn test_aba_spherical_free_fall() {
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(1.0, 0.1),
SpatialTransform::identity(),
);
body.add_body(
"ball",
0,
GenJointType::Spherical,
SpatialInertia::sphere(0.5, 0.05),
SpatialTransform::from_translation(Vector3::new(0.3, 0.0, 0.0)),
);
assert_eq!(body.dof_count(), 9); assert_eq!(body.nq(), 11);
aba_forward_dynamics(&mut body);
assert_relative_eq!(body.qdd[1], -9.81, epsilon = 0.5);
for i in 0..body.dof_count() {
assert!(body.qdd[i].is_finite(), "qdd[{i}] should be finite: {}", body.qdd[i]);
}
}
#[test]
fn test_aba_symmetric_torques_produce_opposite_accelerations() {
let mut body1 = ArticulatedBody::new();
body1.set_gravity(Vector3::zeros());
body1.add_body(
"link", -1,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(1.0, 0.1),
SpatialTransform::identity(),
);
let mut body2 = body1.clone();
body1.tau[0] = 5.0;
body2.tau[0] = -5.0;
aba_forward_dynamics(&mut body1);
aba_forward_dynamics(&mut body2);
assert_relative_eq!(body1.qdd[0], -body2.qdd[0], epsilon = 1e-5);
}
#[test]
fn test_aba_heavier_body_accelerates_less() {
let make_pendulum = |mass: f32| {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body(
"link", -1,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(mass, 0.1),
SpatialTransform::identity(),
);
body.tau[0] = 10.0;
body
};
let mut light = make_pendulum(1.0);
let mut heavy = make_pendulum(10.0);
aba_forward_dynamics(&mut light);
aba_forward_dynamics(&mut heavy);
assert!(
light.qdd[0].abs() > heavy.qdd[0].abs(),
"Light body (qdd={}) should accelerate more than heavy (qdd={})",
light.qdd[0], heavy.qdd[0]
);
}
#[test]
fn test_aba_all_accelerations_finite_multi_link() {
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!("link{i}"),
parent,
GenJointType::Revolute { axis: Vector3::z() },
SpatialInertia::sphere(0.5, 0.05),
SpatialTransform::from_translation(Vector3::new(0.2, 0.0, 0.0)),
);
}
aba_forward_dynamics(&mut body);
for i in 0..body.dof_count() {
assert!(body.qdd[i].is_finite(), "qdd[{i}] should be finite: {}", body.qdd[i]);
}
}
#[test]
fn intent_aba_double_mass_halves_acceleration() {
let make_pendulum = |mass: f32| -> ArticulatedBody {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, 0.0, 0.0));
body.add_body(
"link", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(mass),
SpatialTransform::identity(),
);
body
};
let mut body1 = make_pendulum(1.0);
body1.tau[0] = 1.0;
aba_forward_dynamics(&mut body1);
let acc1 = body1.qdd[0];
let mut body2 = make_pendulum(2.0);
body2.tau[0] = 1.0;
aba_forward_dynamics(&mut body2);
let acc2 = body2.qdd[0];
assert!(
(acc2 / acc1 - 0.5).abs() < 0.15,
"doubling mass should halve accel: acc1={}, acc2={}, ratio={}",
acc1, acc2, acc2 / acc1
);
}
#[test]
fn intent_aba_symmetric_torques_produce_opposite_accelerations() {
let make_pendulum = || -> ArticulatedBody {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, 0.0, 0.0));
body.add_body(
"link", -1, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body
};
let mut body_pos = make_pendulum();
body_pos.tau[0] = 5.0;
aba_forward_dynamics(&mut body_pos);
let mut body_neg = make_pendulum();
body_neg.tau[0] = -5.0;
aba_forward_dynamics(&mut body_neg);
assert!(
(body_pos.qdd[0] + body_neg.qdd[0]).abs() < 1e-5,
"symmetric torques should produce opposite accel: {} vs {}",
body_pos.qdd[0], body_neg.qdd[0]
);
}
#[test]
fn intent_aba_rnea_roundtrip_recovers_torques() {
use crate::rnea::rnea_inverse_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() },
make_inertia(2.0),
SpatialTransform::identity(),
);
body.add_body(
"link2", 0, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)),
);
let tau_original = [3.0_f32, -1.5];
body.tau[0] = tau_original[0];
body.tau[1] = tau_original[1];
body.qd[0] = 0.5;
body.qd[1] = -0.3;
aba_forward_dynamics(&mut body);
let (tau_recovered, _) = rnea_inverse_dynamics(&body);
for i in 0..2 {
assert!(
(tau_recovered[i] - tau_original[i]).abs() < 0.5,
"RNEA should recover torque[{}]: original={}, recovered={}",
i, tau_original[i], tau_recovered[i]
);
}
}
#[test]
fn determinism_aba_same_input_same_output() {
let make_body = || {
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.q[0] = 0.3;
body.q[1] = -0.5;
body.qd[0] = 1.0;
body.qd[1] = -0.7;
body.tau[0] = 2.5;
body.tau[1] = -1.0;
body
};
let mut body_a = make_body();
let mut body_b = make_body();
aba_forward_dynamics(&mut body_a);
aba_forward_dynamics(&mut body_b);
for i in 0..body_a.dof_count() {
assert_eq!(body_a.qdd[i], body_b.qdd[i],
"ABA must be deterministic: qdd[{}] differs", i);
}
}
#[test]
fn stress_aba_1000_steps_no_nan() {
use crate::integrator::{step, IntegratorConfig, IntegrationMethod};
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.tau[0] = 0.1;
let config = IntegratorConfig {
dt: 0.001,
method: IntegrationMethod::SemiImplicitEuler,
..Default::default()
};
for s in 0..1000 {
step(&mut body, &config);
for i in 0..body.q.len() {
assert!(body.q[i].is_finite(), "q[{}] NaN at step {}", i, s);
}
for i in 0..body.qd.len() {
assert!(body.qd[i].is_finite(), "qd[{}] NaN at step {}", i, s);
}
}
}
#[test]
fn stress_aba_branching_tree_100_steps() {
use crate::integrator::{step, IntegratorConfig, IntegrationMethod};
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("torso", -1, GenJointType::Floating,
SpatialInertia::sphere(5.0, 0.2), SpatialTransform::identity());
body.add_body("left", 0, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::from_translation(Vector3::new(-0.5, 0.0, 0.0)));
body.add_body("right", 0, GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0), SpatialTransform::from_translation(Vector3::new(0.5, 0.0, 0.0)));
body.add_body("head", 0, GenJointType::Revolute { axis: Vector3::x() },
make_inertia(0.5), SpatialTransform::from_translation(Vector3::new(0.0, 0.5, 0.0)));
let config = IntegratorConfig {
dt: 0.004,
method: IntegrationMethod::SemiImplicitEuler,
..Default::default()
};
for s in 0..100 {
step(&mut body, &config);
for i in 0..body.q.len() {
assert!(body.q[i].is_finite(), "q[{}] NaN at step {} (branching tree)", i, s);
}
}
}
#[test]
fn stress_aba_20_body_chain_all_finite() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
let q_values: [f32; 20] = [
0.3, -0.7, 1.2, -0.1, 0.8, -1.5, 0.05, 0.9, -0.4, 1.1,
-0.6, 0.2, -1.0, 0.55, -0.35, 1.4, -0.85, 0.15, -1.3, 0.65,
];
for i in 0..20 {
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.15, 0.0, 0.0)),
);
}
for i in 0..20 {
body.q[i] = q_values[i];
}
aba_forward_dynamics(&mut body);
assert_eq!(body.dof_count(), 20);
for i in 0..body.dof_count() {
assert!(
body.qdd[i].is_finite(),
"qdd[{i}] is not finite ({}) in 20-link chain",
body.qdd[i]
);
}
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_aba_double_torque_doubles_acceleration(
tau in 0.1f32..10.0,
) {
let run = |t: f32| -> f32 {
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.tau[0] = t;
aba_forward_dynamics(&mut body);
body.qdd[0]
};
let qdd1 = run(tau);
let qdd2 = run(2.0 * tau);
if qdd1.abs() > 1e-6 {
let ratio = qdd2 / qdd1;
prop_assert!((ratio - 2.0).abs() < 0.1,
"2x torque should give 2x accel: ratio={ratio}");
}
}
#[test]
fn prop_aba_output_always_finite(
q in -2.0f32..2.0,
qd in -5.0f32..5.0,
tau 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() },
make_inertia(1.0), SpatialTransform::identity());
body.q[0] = q;
body.qd[0] = qd;
body.tau[0] = tau;
aba_forward_dynamics(&mut body);
prop_assert!(body.qdd[0].is_finite(),
"qdd must be finite for q={q}, qd={qd}, tau={tau}");
}
}
#[test]
fn intent_aba_zero_torque_zero_gravity_zero_velocity_zero_acceleration() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
for i in 0..5 {
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)),
);
}
for i in 0..body.dof_count() {
body.q[i] = 0.0;
body.qd[i] = 0.0;
body.tau[i] = 0.0;
}
aba_forward_dynamics(&mut body);
for i in 0..body.dof_count() {
assert_relative_eq!(
body.qdd[i], 0.0, epsilon = 1e-8,
);
}
}
}