use nalgebra::{DMatrix, DVector, Vector3};
use super::body::ArticulatedBody;
use super::contact::{ContactManifold, ContactPoint};
use super::kinematics::forward_kinematics;
#[derive(Clone, Debug)]
pub struct ContactJacobianData {
pub body_id: usize,
pub j_normal: DVector<f32>,
pub j_tangent: DMatrix<f32>,
pub normal: Vector3<f32>,
pub tangent1: Vector3<f32>,
pub tangent2: Vector3<f32>,
pub friction: f32,
pub restitution: f32,
pub penetration: f32,
pub v_normal: f32,
pub v_tangent: [f32; 2],
}
#[derive(Clone, Debug)]
pub struct FrictionCone {
pub mu: f32,
pub num_sides: usize,
pub directions: Vec<[f32; 2]>,
}
impl FrictionCone {
pub fn new(mu: f32, num_sides: usize) -> Self {
let num_sides = num_sides.max(3);
let mut directions = Vec::with_capacity(num_sides);
for k in 0..num_sides {
let angle = (k as f32) * 2.0 * std::f32::consts::PI / (num_sides as f32);
directions.push([angle.cos(), angle.sin()]);
}
Self {
mu,
num_sides,
directions,
}
}
pub fn box_friction(mu: f32) -> Self {
Self {
mu,
num_sides: 4,
directions: vec![[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]],
}
}
pub fn is_within_cone(&self, f_tangent: [f32; 2], f_normal: f32) -> bool {
let ft_mag = (f_tangent[0] * f_tangent[0] + f_tangent[1] * f_tangent[1]).sqrt();
ft_mag <= self.mu * f_normal
}
pub fn project_to_cone(&self, f_tangent: [f32; 2], f_normal: f32) -> [f32; 2] {
let ft_mag = (f_tangent[0] * f_tangent[0] + f_tangent[1] * f_tangent[1]).sqrt();
let max_ft = self.mu * f_normal.max(0.0);
if ft_mag <= max_ft || ft_mag < 1e-10 {
f_tangent
} else {
let scale = max_ft / ft_mag;
[f_tangent[0] * scale, f_tangent[1] * scale]
}
}
}
#[derive(Clone, Debug)]
pub struct ContactConstraints {
pub j_n: DMatrix<f32>,
pub j_t: DMatrix<f32>,
pub phi: DVector<f32>,
pub v_n: DVector<f32>,
pub v_t: DVector<f32>,
pub mu: DVector<f32>,
pub restitution: DVector<f32>,
pub per_contact: Vec<ContactJacobianData>,
pub num_contacts: usize,
pub num_dofs: usize,
}
impl ContactConstraints {
pub fn from_manifold(body: &ArticulatedBody, manifold: &ContactManifold) -> Self {
let nv = body.dof_count();
let active: Vec<&ContactPoint> = manifold.active_contacts().collect();
let nc = active.len();
if nc == 0 || nv == 0 {
return Self::empty(nv);
}
let fk = forward_kinematics(body);
let mut j_n = DMatrix::zeros(nc, nv);
let mut j_t = DMatrix::zeros(2 * nc, nv);
let mut phi = DVector::zeros(nc);
let mut v_n = DVector::zeros(nc);
let mut v_t = DVector::zeros(2 * nc);
let mut mu = DVector::zeros(nc);
let mut restitution = DVector::zeros(nc);
let mut per_contact = Vec::with_capacity(nc);
for (k, cp) in active.iter().enumerate() {
let j_point = contact_point_jacobian(body, &fk, cp.body_id, &cp.point_world);
let (t1, t2) = cp.tangent_frame();
let mut jn_row = DVector::zeros(nv);
for j in 0..nv {
jn_row[j] = cp.normal.x * j_point[(0, j)]
+ cp.normal.y * j_point[(1, j)]
+ cp.normal.z * j_point[(2, j)];
}
let mut jt1_row = DVector::zeros(nv);
let mut jt2_row = DVector::zeros(nv);
for j in 0..nv {
jt1_row[j] = t1.x * j_point[(0, j)]
+ t1.y * j_point[(1, j)]
+ t1.z * j_point[(2, j)];
jt2_row[j] = t2.x * j_point[(0, j)]
+ t2.y * j_point[(1, j)]
+ t2.z * j_point[(2, j)];
}
let v_contact = &j_point * &body.qd;
let vn = cp.normal.x * v_contact[0]
+ cp.normal.y * v_contact[1]
+ cp.normal.z * v_contact[2];
let vt1 = t1.x * v_contact[0] + t1.y * v_contact[1] + t1.z * v_contact[2];
let vt2 = t2.x * v_contact[0] + t2.y * v_contact[1] + t2.z * v_contact[2];
j_n.set_row(k, &jn_row.transpose());
j_t.set_row(2 * k, &jt1_row.transpose());
j_t.set_row(2 * k + 1, &jt2_row.transpose());
phi[k] = cp.penetration;
v_n[k] = vn;
v_t[2 * k] = vt1;
v_t[2 * k + 1] = vt2;
mu[k] = cp.friction;
restitution[k] = cp.restitution;
per_contact.push(ContactJacobianData {
body_id: cp.body_id,
j_normal: jn_row,
j_tangent: DMatrix::from_rows(&[jt1_row.transpose(), jt2_row.transpose()]),
normal: cp.normal,
tangent1: t1,
tangent2: t2,
friction: cp.friction,
restitution: cp.restitution,
penetration: cp.penetration,
v_normal: vn,
v_tangent: [vt1, vt2],
});
}
Self {
j_n,
j_t,
phi,
v_n,
v_t,
mu,
restitution,
per_contact,
num_contacts: nc,
num_dofs: nv,
}
}
pub fn empty(nv: usize) -> Self {
Self {
j_n: DMatrix::zeros(0, nv),
j_t: DMatrix::zeros(0, nv),
phi: DVector::zeros(0),
v_n: DVector::zeros(0),
v_t: DVector::zeros(0),
mu: DVector::zeros(0),
restitution: DVector::zeros(0),
per_contact: Vec::new(),
num_contacts: 0,
num_dofs: nv,
}
}
pub fn has_contacts(&self) -> bool {
self.num_contacts > 0
}
pub fn delassus_matrix(&self, m_inv: &DMatrix<f32>) -> DMatrix<f32> {
&self.j_n * m_inv * self.j_n.transpose()
}
pub fn delassus_matrix_full(&self, m_inv: &DMatrix<f32>) -> DMatrix<f32> {
let nc = self.num_contacts;
let nv = self.num_dofs;
let nrows = 3 * nc;
let mut j_full = DMatrix::zeros(nrows, nv);
for k in 0..nc {
j_full.set_row(k, &self.j_n.row(k));
j_full.set_row(nc + 2 * k, &self.j_t.row(2 * k));
j_full.set_row(nc + 2 * k + 1, &self.j_t.row(2 * k + 1));
}
&j_full * m_inv * j_full.transpose()
}
}
#[derive(Clone, Debug)]
pub struct GlobalContactConstraints {
pub j_n: DMatrix<f32>,
pub j_t: DMatrix<f32>,
pub phi: DVector<f32>,
pub v_n: DVector<f32>,
pub mu: DVector<f32>,
pub restitution: DVector<f32>,
pub num_contacts: usize,
pub total_dofs: usize,
pub body_dof_offsets: Vec<usize>,
pub body_dof_counts: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct InterBodyContact {
pub body_a: usize,
pub link_a: usize,
pub body_b: usize,
pub link_b: usize,
pub point_world: Vector3<f32>,
pub normal: Vector3<f32>,
pub penetration: f32,
pub friction: f32,
pub restitution: f32,
}
impl GlobalContactConstraints {
pub fn build(
bodies: &[&ArticulatedBody],
dof_offsets: &[usize],
dof_counts: &[usize],
ground_contacts: &[(usize, &ContactManifold)],
inter_contacts: &[InterBodyContact],
) -> Self {
let total_nv: usize = dof_counts.iter().sum();
let ground_nc: usize = ground_contacts.iter().map(|(_, m)| m.active_count()).sum();
let inter_nc = inter_contacts.iter().filter(|c| c.penetration > 0.0).count();
let total_nc = ground_nc + inter_nc;
if total_nc == 0 || total_nv == 0 {
return Self {
j_n: DMatrix::zeros(0, total_nv),
j_t: DMatrix::zeros(0, total_nv),
phi: DVector::zeros(0),
v_n: DVector::zeros(0),
mu: DVector::zeros(0),
restitution: DVector::zeros(0),
num_contacts: 0,
total_dofs: total_nv,
body_dof_offsets: dof_offsets.to_vec(),
body_dof_counts: dof_counts.to_vec(),
};
}
let mut j_n = DMatrix::zeros(total_nc, total_nv);
let mut j_t = DMatrix::zeros(2 * total_nc, total_nv);
let mut phi = DVector::zeros(total_nc);
let mut v_n = DVector::zeros(total_nc);
let mut mu_vec = DVector::zeros(total_nc);
let mut rest_vec = DVector::zeros(total_nc);
let mut row = 0;
for &(body_idx, manifold) in ground_contacts {
if body_idx >= bodies.len() { continue; }
let body = bodies[body_idx];
let offset = dof_offsets[body_idx];
let nv = dof_counts[body_idx];
let fk = forward_kinematics(body);
for cp in manifold.active_contacts() {
let j_point = contact_point_jacobian(body, &fk, cp.body_id, &cp.point_world);
let (t1, t2) = cp.tangent_frame();
for j in 0..nv {
let col = offset + j;
j_n[(row, col)] = cp.normal.x * j_point[(0, j)]
+ cp.normal.y * j_point[(1, j)]
+ cp.normal.z * j_point[(2, j)];
j_t[(2 * row, col)] = t1.x * j_point[(0, j)]
+ t1.y * j_point[(1, j)]
+ t1.z * j_point[(2, j)];
j_t[(2 * row + 1, col)] = t2.x * j_point[(0, j)]
+ t2.y * j_point[(1, j)]
+ t2.z * j_point[(2, j)];
}
let v_contact = &j_point * &body.qd;
let vn = cp.normal.x * v_contact[0]
+ cp.normal.y * v_contact[1]
+ cp.normal.z * v_contact[2];
phi[row] = cp.penetration;
v_n[row] = vn;
mu_vec[row] = cp.friction;
rest_vec[row] = cp.restitution;
row += 1;
}
}
for ic in inter_contacts {
if ic.penetration <= 0.0 { continue; }
if ic.body_a >= bodies.len() || ic.body_b >= bodies.len() { continue; }
let body_a = bodies[ic.body_a];
let body_b = bodies[ic.body_b];
let fk_a = forward_kinematics(body_a);
let fk_b = forward_kinematics(body_b);
let j_a = contact_point_jacobian(body_a, &fk_a, ic.link_a, &ic.point_world);
let j_b = contact_point_jacobian(body_b, &fk_b, ic.link_b, &ic.point_world);
let normal = if ic.normal.norm_squared() > 1e-12 { ic.normal.normalize() } else { Vector3::y() };
let (t1, t2) = super::contact::compute_tangent_frame_pub(&normal);
let offset_a = dof_offsets[ic.body_a];
let nv_a = dof_counts[ic.body_a];
let offset_b = dof_offsets[ic.body_b];
let nv_b = dof_counts[ic.body_b];
for j in 0..nv_a {
let col = offset_a + j;
j_n[(row, col)] = -(normal.x * j_a[(0, j)]
+ normal.y * j_a[(1, j)]
+ normal.z * j_a[(2, j)]);
j_t[(2 * row, col)] = -(t1.x * j_a[(0, j)]
+ t1.y * j_a[(1, j)]
+ t1.z * j_a[(2, j)]);
j_t[(2 * row + 1, col)] = -(t2.x * j_a[(0, j)]
+ t2.y * j_a[(1, j)]
+ t2.z * j_a[(2, j)]);
}
for j in 0..nv_b {
let col = offset_b + j;
j_n[(row, col)] += normal.x * j_b[(0, j)]
+ normal.y * j_b[(1, j)]
+ normal.z * j_b[(2, j)];
j_t[(2 * row, col)] += t1.x * j_b[(0, j)]
+ t1.y * j_b[(1, j)]
+ t1.z * j_b[(2, j)];
j_t[(2 * row + 1, col)] += t2.x * j_b[(0, j)]
+ t2.y * j_b[(1, j)]
+ t2.z * j_b[(2, j)];
}
let v_a = &j_a * &body_a.qd;
let v_b = &j_b * &body_b.qd;
let vn = normal.x * (v_b[0] - v_a[0])
+ normal.y * (v_b[1] - v_a[1])
+ normal.z * (v_b[2] - v_a[2]);
phi[row] = ic.penetration;
v_n[row] = vn;
mu_vec[row] = ic.friction;
rest_vec[row] = ic.restitution;
row += 1;
}
Self {
j_n,
j_t,
phi,
v_n,
mu: mu_vec,
restitution: rest_vec,
num_contacts: row, total_dofs: total_nv,
body_dof_offsets: dof_offsets.to_vec(),
body_dof_counts: dof_counts.to_vec(),
}
}
}
fn contact_point_jacobian(
body: &ArticulatedBody,
fk: &super::kinematics::FKResult,
body_id: usize,
point_world: &Vector3<f32>,
) -> DMatrix<f32> {
let nv = body.dof_count();
let mut jac = DMatrix::zeros(3, nv);
let mut current = body_id as i32;
while current >= 0 {
let idx = current as usize;
let bd = &body.bodies[idx];
let dof = bd.joint_type.dof();
if dof > 0 {
let s = bd.joint_type.motion_subspace(body.joint_q(idx));
let r_joint = &fk.transforms[idx].rotation;
let p_joint = &fk.transforms[idx].translation;
for (j, sj) in s.iter().enumerate().take(dof) {
let col_idx = bd.v_index + j;
let angular_world = r_joint * sj.angular();
let linear_world = r_joint * sj.linear();
let lever = point_world - p_joint;
let v = linear_world + angular_world.cross(&lever);
jac[(0, col_idx)] = v.x;
jac[(1, col_idx)] = v.y;
jac[(2, col_idx)] = v.z;
}
}
current = bd.parent;
}
jac
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::body::GenJointType;
use super::super::contact::ContactPoint;
use super::super::spatial::{SpatialInertia, SpatialTransform};
use approx::assert_relative_eq;
use nalgebra::Matrix3;
fn make_inertia(mass: f32) -> SpatialInertia {
SpatialInertia::from_mass_inertia_at_com(mass, Matrix3::identity() * 0.01 * mass)
}
#[test]
fn test_friction_cone_creation() {
let cone = FrictionCone::new(0.5, 4);
assert_eq!(cone.num_sides, 4);
assert_relative_eq!(cone.mu, 0.5);
assert_eq!(cone.directions.len(), 4);
}
#[test]
fn test_friction_cone_box() {
let cone = FrictionCone::box_friction(0.8);
assert_eq!(cone.num_sides, 4);
assert!(cone.is_within_cone([0.5, 0.5], 1.0));
assert!(!cone.is_within_cone([1.0, 1.0], 1.0));
}
#[test]
fn test_friction_cone_project() {
let cone = FrictionCone::new(0.5, 8);
let inside = [0.1, 0.1];
let projected = cone.project_to_cone(inside, 1.0);
assert_relative_eq!(projected[0], 0.1, epsilon = 1e-6);
assert_relative_eq!(projected[1], 0.1, epsilon = 1e-6);
let outside = [1.0, 0.0];
let projected = cone.project_to_cone(outside, 1.0);
assert_relative_eq!(projected[0], 0.5, epsilon = 1e-6);
assert_relative_eq!(projected[1], 0.0, epsilon = 1e-6);
}
#[test]
fn test_friction_cone_zero_normal() {
let cone = FrictionCone::new(0.5, 4);
let projected = cone.project_to_cone([1.0, 1.0], 0.0);
assert_relative_eq!(projected[0], 0.0, epsilon = 1e-6);
assert_relative_eq!(projected[1], 0.0, epsilon = 1e-6);
}
#[test]
fn test_contact_jacobian_single_revolute() {
let mut body = ArticulatedBody::new();
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.set_joint_qd(0, &[2.0]);
let fk = forward_kinematics(&body);
let point = Vector3::new(0.3, 0.0, 0.0);
let jac = contact_point_jacobian(&body, &fk, 0, &point);
let v = &jac * &body.qd;
assert_relative_eq!(v[0], 0.0, epsilon = 1e-5);
assert_relative_eq!(v[1], 0.6, epsilon = 1e-5);
assert_relative_eq!(v[2], 0.0, epsilon = 1e-5);
}
#[test]
fn test_contact_jacobian_finite_difference() {
let mut body = ArticulatedBody::new();
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_q(0, &[0.4]);
body.set_joint_q(1, &[-0.2]);
body.set_joint_qd(0, &[1.5]);
body.set_joint_qd(1, &[-0.7]);
let fk = forward_kinematics(&body);
let point = fk.transforms[1].translation;
let jac = contact_point_jacobian(&body, &fk, 1, &point);
let v_jac = &jac * &body.qd;
let dt = 1e-5;
let q0_0 = body.joint_q(0)[0];
let q0_1 = body.joint_q(1)[0];
body.set_joint_q(0, &[q0_0 + body.joint_qd(0)[0] * dt]);
body.set_joint_q(1, &[q0_1 + body.joint_qd(1)[0] * dt]);
let fk2 = forward_kinematics(&body);
let point2 = fk2.transforms[1].translation;
let v_fd = (point2 - point) / dt;
assert_relative_eq!(v_jac[0], v_fd.x, epsilon = 0.25);
assert_relative_eq!(v_jac[1], v_fd.y, epsilon = 0.25);
assert_relative_eq!(v_jac[2], v_fd.z, epsilon = 0.25);
}
#[test]
fn test_contact_constraints_empty() {
let body = ArticulatedBody::new();
let manifold = ContactManifold::new();
let constraints = ContactConstraints::from_manifold(&body, &manifold);
assert!(!constraints.has_contacts());
assert_eq!(constraints.num_contacts, 0);
}
#[test]
fn test_contact_constraints_from_manifold() {
let mut body = ArticulatedBody::new();
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_q(0, &[0.0]);
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(
1,
Vector3::new(0.3, 0.0, 0.0),
Vector3::zeros(),
Vector3::y(),
0.001,
)
.with_friction(0.5),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
assert!(constraints.has_contacts());
assert_eq!(constraints.num_contacts, 1);
assert_eq!(constraints.j_n.nrows(), 1);
assert_eq!(constraints.j_n.ncols(), 2);
assert_eq!(constraints.j_t.nrows(), 2);
assert_eq!(constraints.j_t.ncols(), 2);
assert_relative_eq!(constraints.mu[0], 0.5);
assert_relative_eq!(constraints.phi[0], 0.001);
}
#[test]
fn test_normal_jacobian_correct_direction() {
let mut body = ArticulatedBody::new();
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.set_joint_qd(0, &[1.0]);
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(
0,
Vector3::new(0.3, 0.0, 0.0),
Vector3::new(0.3, 0.0, 0.0),
Vector3::y(),
0.001,
));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
assert_relative_eq!(constraints.v_n[0], 0.3, epsilon = 1e-4);
}
#[test]
fn test_tangent_jacobian_orthogonal() {
let mut body = ArticulatedBody::new();
body.add_body(
"link",
-1,
GenJointType::Revolute { axis: Vector3::z() },
make_inertia(1.0),
SpatialTransform::identity(),
);
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(
0,
Vector3::new(0.3, 0.0, 0.0),
Vector3::new(0.3, 0.0, 0.0),
Vector3::y(),
0.001,
));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let cd = &constraints.per_contact[0];
assert_relative_eq!(cd.normal.dot(&cd.tangent1), 0.0, epsilon = 1e-5);
assert_relative_eq!(cd.normal.dot(&cd.tangent2), 0.0, epsilon = 1e-5);
assert_relative_eq!(cd.tangent1.dot(&cd.tangent2), 0.0, epsilon = 1e-5);
}
#[test]
fn test_delassus_matrix_dimensions() {
let mut body = ArticulatedBody::new();
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)),
);
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(
0, Vector3::zeros(), Vector3::zeros(), Vector3::y(), 0.001,
));
manifold.add_contact(ContactPoint::new(
1, Vector3::new(0.3, 0.0, 0.0), Vector3::zeros(), Vector3::y(), 0.002,
));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let m_inv = DMatrix::identity(2, 2);
let w = constraints.delassus_matrix(&m_inv);
assert_eq!(w.nrows(), 2); assert_eq!(w.ncols(), 2);
let w_full = constraints.delassus_matrix_full(&m_inv);
assert_eq!(w_full.nrows(), 6); assert_eq!(w_full.ncols(), 6);
}
#[test]
fn test_multiple_contacts_assembly() {
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)),
);
}
body.set_joint_q(0, &[0.3]);
body.set_joint_qd(0, &[1.0]);
let mut manifold = ContactManifold::new();
for i in 0..3 {
manifold.add_contact(ContactPoint::new(
i,
Vector3::new(i as f32 * 0.2, 0.0, 0.0),
Vector3::zeros(),
Vector3::y(),
0.001,
));
}
let constraints = ContactConstraints::from_manifold(&body, &manifold);
assert_eq!(constraints.num_contacts, 3);
assert_eq!(constraints.j_n.nrows(), 3);
assert_eq!(constraints.j_n.ncols(), 3);
assert_eq!(constraints.j_t.nrows(), 6);
assert_eq!(constraints.j_t.ncols(), 3);
assert_eq!(constraints.per_contact.len(), 3);
}
#[test]
fn intent_jacobian_normal_rows_match_contact_count() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(0,
Vector3::new(0.0, -0.1, 0.0), Vector3::zeros(), Vector3::y(), 0.01));
manifold.add_contact(ContactPoint::new(0,
Vector3::new(0.1, -0.1, 0.0), Vector3::zeros(), Vector3::y(), 0.02));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
assert_eq!(constraints.j_n.nrows(), 2, "J_n rows should match contact count");
assert_eq!(constraints.j_t.nrows(), 4, "J_t rows should be 2 * contact count");
}
#[test]
fn intent_jacobian_cols_match_dof() {
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(1.0, 0.1),
SpatialTransform::from_translation(Vector3::new(0.0, -1.0, 0.0)));
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(1,
Vector3::new(0.0, -2.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
assert_eq!(constraints.j_n.ncols(), body.dof_count(),
"J_n cols should match DOF count");
}
#[test]
fn intent_delassus_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(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.set_joint_q(0, &[0.3]);
body.set_joint_q(1, &[-0.2]);
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(1,
Vector3::new(0.0, -1.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.5));
manifold.add_contact(ContactPoint::new(0,
Vector3::new(0.0, -0.5, 0.0), Vector3::zeros(), Vector3::y(), 0.005)
.with_friction(0.3));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let m_inv = {
use super::super::crba::crba_mass_matrix;
let m = crba_mass_matrix(&body);
let n = m.nrows();
m.try_inverse().unwrap_or_else(|| nalgebra::DMatrix::identity(n, n))
};
let w = constraints.delassus_matrix(&m_inv);
for i in 0..w.nrows() {
for j in 0..w.ncols() {
assert!((w[(i, j)] - w[(j, i)]).abs() < 1e-5,
"Delassus W[{i},{j}]={} != W[{j},{i}]={}", w[(i, j)], w[(j, i)]);
}
}
}
#[test]
fn intent_delassus_diagonal_positive() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(0,
Vector3::new(0.0, -0.1, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.5));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let m_inv = {
use super::super::crba::crba_mass_matrix;
let m = crba_mass_matrix(&body);
let n = m.nrows();
m.try_inverse().unwrap_or_else(|| nalgebra::DMatrix::identity(n, n))
};
let w = constraints.delassus_matrix(&m_inv);
for i in 0..w.nrows() {
assert!(w[(i, i)] > 0.0, "Delassus diagonal W[{i},{i}]={} must be positive", w[(i, i)]);
}
}
#[test]
fn intent_global_constraints_correct_dimensions() {
use super::GlobalContactConstraints;
use super::InterBodyContact;
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(0,
Vector3::new(0.0, -0.1, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.5));
manifold.add_contact(ContactPoint::new(0,
Vector3::new(0.1, -0.1, 0.1), Vector3::zeros(), Vector3::y(), 0.02)
.with_friction(0.3));
let bodies: Vec<&ArticulatedBody> = vec![&body];
let dof_offsets = vec![0usize];
let dof_counts = vec![body.dof_count()];
let ground_contacts: Vec<(usize, &ContactManifold)> = vec![(0, &manifold)];
let inter_contacts: Vec<InterBodyContact> = vec![];
let gc = GlobalContactConstraints::build(
&bodies, &dof_offsets, &dof_counts, &ground_contacts, &inter_contacts);
let nc = gc.num_contacts;
let nv = body.dof_count();
assert_eq!(nc, 2, "should have 2 contacts");
assert_eq!(gc.j_n.nrows(), nc, "J_n rows = num_contacts");
assert_eq!(gc.j_n.ncols(), nv, "J_n cols = total_nv");
assert_eq!(gc.j_t.nrows(), 2 * nc, "J_t rows = 2*num_contacts");
assert_eq!(gc.j_t.ncols(), nv, "J_t cols = total_nv");
assert_eq!(gc.phi.len(), nc, "phi len = num_contacts");
assert_eq!(gc.mu.len(), nc, "mu len = num_contacts");
}
#[test]
fn property_multi_body_jacobian_cols_equal_total_dof() {
use super::GlobalContactConstraints;
use super::InterBodyContact;
let mut body_a = ArticulatedBody::new();
body_a.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body_a.add_body("a", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let mut body_b = ArticulatedBody::new();
body_b.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body_b.add_body("b", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let manifold_a = ContactManifold::new();
let manifold_b = ContactManifold::new();
let bodies: Vec<&ArticulatedBody> = vec![&body_a, &body_b];
let dof_offsets = vec![0usize, 6];
let dof_counts = vec![6, 6];
let ground_contacts: Vec<(usize, &ContactManifold)> = vec![(0, &manifold_a), (1, &manifold_b)];
let inter = vec![InterBodyContact {
body_a: 0, link_a: 0, body_b: 1, link_b: 0,
point_world: Vector3::new(0.0, 0.5, 0.0),
normal: Vector3::y(),
penetration: 0.01,
friction: 0.5,
restitution: 0.0,
}];
let gc = GlobalContactConstraints::build(
&bodies, &dof_offsets, &dof_counts, &ground_contacts, &inter);
let total_nv = 12; assert_eq!(gc.j_n.ncols(), total_nv,
"Global J_n cols should equal total DOFs across all bodies");
}
}