use nalgebra::{DMatrix, DVector};
use super::contact_jacobian::ContactConstraints;
use super::crba::crba_mass_matrix;
use super::body::ArticulatedBody;
#[derive(Clone, Debug)]
pub struct LcpSolverConfig {
pub max_iterations: usize,
pub tolerance: f32,
pub baumgarte: f32,
pub restitution_threshold: f32,
pub regularization: f32,
pub warm_start: f32,
pub max_impulse: f32,
pub sor: f32,
pub cfm: f32,
pub min_iterations: usize,
pub solref: [f32; 2],
pub solimp: [f32; 3],
pub bounce_threshold: f32,
pub contact_damping: f32,
}
impl Default for LcpSolverConfig {
fn default() -> Self {
Self {
max_iterations: 100,
tolerance: 1e-6,
baumgarte: 0.1,
restitution_threshold: 0.01,
regularization: 1e-4,
warm_start: 0.9,
max_impulse: 50.0,
sor: 1.3,
cfm: 1e-5,
min_iterations: 5,
solref: [0.02, 1.0], solimp: [0.9, 0.95, 0.001], bounce_threshold: 0.01, contact_damping: 0.5, }
}
}
#[derive(Clone, Debug)]
pub struct LcpSolverResult {
pub lambda_n: DVector<f32>,
pub lambda_t: DVector<f32>,
pub tau_contact: DVector<f32>,
pub iterations: usize,
pub residual: f32,
pub converged: bool,
}
pub fn solve_contact_lcp(
body: &ArticulatedBody,
constraints: &ContactConstraints,
dt: f32,
config: &LcpSolverConfig,
) -> LcpSolverResult {
let nc = constraints.num_contacts;
let nv = constraints.num_dofs;
if nc == 0 {
return LcpSolverResult {
lambda_n: DVector::zeros(0),
lambda_t: DVector::zeros(0),
tau_contact: DVector::zeros(nv),
iterations: 0,
residual: 0.0,
converged: true,
};
}
let m = crba_mass_matrix(body);
let m_inv = match m.clone().try_inverse() {
Some(inv) => inv,
None => {
let mut inv = DMatrix::zeros(nv, nv);
for i in 0..nv {
let d = m[(i, i)];
inv[(i, i)] = if d.abs() > 1e-10 { 1.0 / d } else { 0.0 };
}
inv
}
};
let j_n_m_inv = &constraints.j_n * &m_inv;
let w_nn = &j_n_m_inv * constraints.j_n.transpose();
let w_nt = &j_n_m_inv * constraints.j_t.transpose();
let j_t_m_inv = &constraints.j_t * &m_inv;
let w_tn = &j_t_m_inv * constraints.j_n.transpose();
let w_tt = &j_t_m_inv * constraints.j_t.transpose();
let mut b_n = DVector::zeros(nc);
let b_t = &constraints.j_t * &body.qd;
for k in 0..nc {
let vn = constraints.v_n[k];
let phi = constraints.phi[k];
let e = constraints.restitution[k];
let stabilization = if phi > 0.0 && dt > 1e-10 {
config.baumgarte * phi / dt
} else {
0.0
};
let restitution_bias = if vn < -config.restitution_threshold {
e * vn
} else {
0.0
};
b_n[k] = vn - stabilization + restitution_bias;
}
let mut lambda_n = DVector::zeros(nc);
let mut lambda_t = DVector::zeros(2 * nc);
let mut iterations = 0;
let mut residual = f32::MAX;
for iter in 0..config.max_iterations {
let mut max_change = 0.0_f32;
for k in 0..nc {
let diag = w_nn[(k, k)] + config.regularization + config.cfm;
if diag.abs() < 1e-12 {
continue;
}
let mut r: f32 = b_n[k];
for j in 0..nc {
r += w_nn[(k, j)] * lambda_n[j];
}
for j in 0..2 * nc {
r += w_nt[(k, j)] * lambda_t[j];
}
let old: f32 = lambda_n[k];
let updated: f32 = old - r / diag;
let new_val = updated.max(0.0).min(config.max_impulse);
lambda_n[k] = new_val;
max_change = max_change.max((new_val - old).abs());
}
for k in 0..nc {
let mu = constraints.mu[k];
let fn_k = lambda_n[k];
for d in 0..2 {
let idx = 2 * k + d;
let diag = w_tt[(idx, idx)] + config.regularization;
if diag.abs() < 1e-12 {
continue;
}
let mut r: f32 = b_t[idx];
for j in 0..nc {
r += w_tn[(idx, j)] * lambda_n[j];
}
for j in 0..2 * nc {
r += w_tt[(idx, j)] * lambda_t[j];
}
let old: f32 = lambda_t[idx];
let max_friction: f32 = mu * fn_k;
let updated: f32 = old - r / diag;
let new_val = updated.clamp(-max_friction, max_friction);
lambda_t[idx] = new_val;
max_change = max_change.max((new_val - old).abs());
}
}
iterations = iter + 1;
residual = max_change;
if max_change < config.tolerance && iter >= config.min_iterations {
break;
}
}
let mut tau_contact = DVector::zeros(nv);
for k in 0..nc {
for j in 0..nv {
tau_contact[j] += constraints.j_n[(k, j)] * lambda_n[k];
}
}
for k in 0..2 * nc {
for j in 0..nv {
tau_contact[j] += constraints.j_t[(k, j)] * lambda_t[k];
}
}
LcpSolverResult {
lambda_n,
lambda_t,
tau_contact,
iterations,
residual,
converged: residual < config.tolerance,
}
}
pub fn compute_contact_impulse(
body: &ArticulatedBody,
constraints: &ContactConstraints,
dt: f32,
) -> DVector<f32> {
let config = LcpSolverConfig::default();
let result = solve_contact_lcp(body, constraints, dt, &config);
result.tau_contact
}
use super::contact_jacobian::GlobalContactConstraints;
#[derive(Clone, Debug)]
pub struct GlobalLcpResult {
pub delta_qd: DVector<f32>,
pub position_correction: DVector<f32>,
pub lambda_n: DVector<f32>,
pub lambda_t: DVector<f32>,
pub iterations: usize,
pub residual: f32,
pub converged: bool,
}
pub fn solve_global_contact_lcp(
bodies: &[&ArticulatedBody],
constraints: &GlobalContactConstraints,
_dt: f32,
config: &LcpSolverConfig,
initial_lambda_n: Option<&DVector<f32>>,
initial_lambda_t: Option<&DVector<f32>>,
) -> GlobalLcpResult {
let nc = constraints.num_contacts;
let total_nv = constraints.total_dofs;
if nc == 0 {
return GlobalLcpResult {
delta_qd: DVector::zeros(total_nv),
position_correction: DVector::zeros(total_nv),
lambda_n: DVector::zeros(0),
lambda_t: DVector::zeros(0),
iterations: 0,
residual: 0.0,
converged: true,
};
}
let m_inv = super::newton_solver::build_block_diagonal_m_inv(
bodies, &constraints.body_dof_offsets, &constraints.body_dof_counts, total_nv,
);
let j_n_m_inv = &constraints.j_n * &m_inv;
let w_nn = &j_n_m_inv * constraints.j_n.transpose();
let w_nt = &j_n_m_inv * constraints.j_t.transpose();
let j_t_m_inv = &constraints.j_t * &m_inv;
let w_tn = &j_t_m_inv * constraints.j_n.transpose();
let w_tt = &j_t_m_inv * constraints.j_t.transpose();
let mut b_n = DVector::zeros(nc);
let b_t = &constraints.j_t * &build_global_qd(bodies, &constraints.body_dof_offsets, &constraints.body_dof_counts, total_nv);
let mut phi_correction = DVector::zeros(nc);
for k in 0..nc {
let vn = constraints.v_n[k];
let phi_k = constraints.phi[k];
let e = constraints.restitution[k];
if phi_k > 0.0 {
phi_correction[k] = (config.baumgarte * phi_k).min(0.01);
}
let restitution_bias = if vn < -config.restitution_threshold {
e * vn
} else {
0.0
};
b_n[k] = vn + restitution_bias;
if config.contact_damping > 0.0 && vn < 0.0 {
b_n[k] -= config.contact_damping * vn;
}
}
let mut lambda_n = match initial_lambda_n {
Some(prev) if prev.len() == nc => prev.clone(),
_ => DVector::zeros(nc),
};
let mut lambda_t = match initial_lambda_t {
Some(prev) if prev.len() == 2 * nc => prev.clone(),
_ => DVector::zeros(2 * nc),
};
let mut iterations = 0;
let mut residual = f32::MAX;
for iter in 0..config.max_iterations {
let mut max_change = 0.0_f32;
for k in 0..nc {
let diag = w_nn[(k, k)] + config.regularization + config.cfm;
if diag.abs() < 1e-12 { continue; }
let mut r: f32 = b_n[k];
for j in 0..nc {
r += w_nn[(k, j)] * lambda_n[j];
}
for j in 0..2 * nc {
r += w_nt[(k, j)] * lambda_t[j];
}
let old: f32 = lambda_n[k];
let gs_update: f32 = old - r / diag;
let sor_update = old + config.sor * (gs_update - old);
let new_val = if sor_update.is_finite() {
sor_update.max(0.0).min(config.max_impulse)
} else {
0.0
};
lambda_n[k] = new_val;
max_change = max_change.max((new_val - old).abs());
}
for k in 0..nc {
let mu = constraints.mu[k];
let fn_k = lambda_n[k];
for d in 0..2 {
let idx = 2 * k + d;
let diag = w_tt[(idx, idx)] + config.regularization;
if diag.abs() < 1e-12 { continue; }
let mut r: f32 = b_t[idx];
for j in 0..nc {
r += w_tn[(idx, j)] * lambda_n[j];
}
for j in 0..2 * nc {
r += w_tt[(idx, j)] * lambda_t[j];
}
let old: f32 = lambda_t[idx];
let max_friction: f32 = mu * fn_k;
let gs_update: f32 = old - r / diag;
let sor_update = old + config.sor * (gs_update - old);
let new_val = if sor_update.is_finite() {
sor_update.clamp(-max_friction, max_friction)
} else {
0.0
};
lambda_t[idx] = new_val;
max_change = max_change.max((new_val - old).abs());
}
}
iterations = iter + 1;
residual = max_change;
if max_change < config.tolerance && iter >= config.min_iterations {
break;
}
}
let tau_global = constraints.j_n.transpose() * &lambda_n
+ constraints.j_t.transpose() * &lambda_t;
let mut delta_qd = &m_inv * &tau_global;
for i in 0..delta_qd.len() {
if !delta_qd[i].is_finite() {
delta_qd[i] = 0.0;
}
}
let tau_pos = constraints.j_n.transpose() * &phi_correction;
let mut position_correction = &m_inv * &tau_pos;
for i in 0..position_correction.len() {
if !position_correction[i].is_finite() {
position_correction[i] = 0.0;
}
}
GlobalLcpResult {
delta_qd,
position_correction,
lambda_n,
lambda_t,
iterations,
residual,
converged: residual < config.tolerance,
}
}
fn build_global_qd(
bodies: &[&ArticulatedBody],
offsets: &[usize],
counts: &[usize],
total_nv: usize,
) -> DVector<f32> {
let mut qd = DVector::zeros(total_nv);
for (k, body) in bodies.iter().enumerate() {
let offset = offsets[k];
let nv = counts[k];
for i in 0..nv {
qd[offset + i] = body.qd[i];
}
}
qd
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::body::GenJointType;
use super::super::contact::{ContactManifold, ContactPoint};
use super::super::contact_jacobian::ContactConstraints;
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_lcp_no_contacts() {
let body = ArticulatedBody::new();
let constraints = ContactConstraints::empty(0);
let result = solve_contact_lcp(&body, &constraints, 0.001, &LcpSolverConfig::default());
assert!(result.converged);
assert_eq!(result.iterations, 0);
assert_eq!(result.tau_contact.len(), 0);
}
#[test]
fn test_lcp_single_contact_normal_force() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"link",
-1,
GenJointType::Prismatic { axis: Vector3::y() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.set_joint_qd(0, &[-0.5]);
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(
0,
Vector3::new(0.0, 0.0, 0.0),
Vector3::zeros(),
Vector3::y(),
0.001,
)
.with_friction(0.5),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let result = solve_contact_lcp(&body, &constraints, 0.001, &LcpSolverConfig::default());
assert!(result.lambda_n[0] > 0.0, "Normal impulse should be positive: {}", result.lambda_n[0]);
assert!(result.tau_contact[0] > 0.0, "Contact impulse should be upward: {}", result.tau_contact[0]);
}
#[test]
fn test_lcp_no_force_when_separating() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body(
"link",
-1,
GenJointType::Prismatic { axis: Vector3::y() },
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::zeros(),
Vector3::zeros(),
Vector3::y(),
0.0001, ));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let result = solve_contact_lcp(&body, &constraints, 0.001, &LcpSolverConfig::default());
assert_relative_eq!(result.lambda_n[0], 0.0, epsilon = 1e-3);
}
#[test]
fn test_lcp_friction_bounds() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"vertical",
-1,
GenJointType::Prismatic { axis: Vector3::y() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.add_body(
"horizontal",
0,
GenJointType::Prismatic { axis: Vector3::x() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.set_joint_qd(1, &[5.0]); body.set_joint_qd(0, &[-0.1]);
let mu = 0.3;
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(
1,
Vector3::zeros(),
Vector3::zeros(),
Vector3::y(),
0.001,
)
.with_friction(mu),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let result = solve_contact_lcp(&body, &constraints, 0.001, &LcpSolverConfig::default());
let fn_val = result.lambda_n[0];
let ft1 = result.lambda_t[0];
let ft2 = result.lambda_t[1];
let ft_mag = (ft1 * ft1 + ft2 * ft2).sqrt();
if fn_val > 1e-6 {
assert!(
ft_mag <= mu * fn_val + 1e-4,
"Friction should be bounded: |ft|={ft_mag} > μ·fn={}",
mu * fn_val
);
}
}
#[test]
fn test_lcp_convergence() {
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_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);
let config = LcpSolverConfig {
max_iterations: 100,
tolerance: 1e-8,
..Default::default()
};
let result = solve_contact_lcp(&body, &constraints, 0.001, &config);
assert!(result.converged, "Solver should converge. Residual: {}, iterations: {}",
result.residual, result.iterations);
assert_eq!(result.tau_contact.len(), 3);
}
#[test]
fn test_lcp_result_finite() {
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.set_joint_q(0, &[0.5]);
body.set_joint_qd(0, &[2.0]);
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(
0,
Vector3::new(0.3, -0.1, 0.0),
Vector3::new(0.3, -0.1, 0.0),
Vector3::y(),
0.005,
)
.with_friction(0.8),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let result = solve_contact_lcp(&body, &constraints, 0.001, &LcpSolverConfig::default());
for i in 0..result.lambda_n.len() {
assert!(result.lambda_n[i].is_finite(), "lambda_n[{i}] is not finite");
}
for i in 0..result.lambda_t.len() {
assert!(result.lambda_t[i].is_finite(), "lambda_t[{i}] is not finite");
}
for i in 0..result.tau_contact.len() {
assert!(result.tau_contact[i].is_finite(), "tau_contact[{i}] is not finite");
}
}
#[test]
fn test_lcp_complementarity() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"link",
-1,
GenJointType::Prismatic { axis: Vector3::y() },
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::zeros(),
Vector3::zeros(),
Vector3::y(),
0.001,
));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let config = LcpSolverConfig {
max_iterations: 200,
tolerance: 1e-10,
baumgarte: 0.0, ..Default::default()
};
let result = solve_contact_lcp(&body, &constraints, 0.001, &config);
let fn_val = result.lambda_n[0];
if fn_val > 1e-6 {
let m = crba_mass_matrix(&body);
let m_inv = m.try_inverse().expect("Mass matrix must be invertible — check for degenerate bodies");
let dqd = &m_inv * &result.tau_contact;
let new_qd = &body.qd + dqd;
let vn_post = (&constraints.j_n * &new_qd)[0];
assert!(
vn_post.abs() < 0.1 || vn_post > -1e-4,
"Post-contact normal velocity should be ~0 or positive: {vn_post}"
);
}
}
#[test]
fn test_compute_contact_impulse_convenience() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body(
"link",
-1,
GenJointType::Prismatic { axis: Vector3::y() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.set_joint_qd(0, &[-0.5]);
let mut manifold = ContactManifold::new();
manifold.add_contact(ContactPoint::new(
0,
Vector3::zeros(),
Vector3::zeros(),
Vector3::y(),
0.001,
));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let tau = compute_contact_impulse(&body, &constraints, 0.001);
assert_eq!(tau.len(), 1);
assert!(tau[0].is_finite());
}
#[test]
fn test_lcp_restitution() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::zeros());
body.add_body(
"link",
-1,
GenJointType::Prismatic { axis: Vector3::y() },
make_inertia(1.0),
SpatialTransform::identity(),
);
body.set_joint_qd(0, &[-2.0]);
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(
0,
Vector3::zeros(),
Vector3::zeros(),
Vector3::y(),
0.001,
)
.with_restitution(0.8),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let config = LcpSolverConfig {
baumgarte: 0.0,
restitution_threshold: 0.1,
..Default::default()
};
let result = solve_contact_lcp(&body, &constraints, 0.001, &config);
assert!(result.lambda_n[0] > 0.0, "Should have contact impulse with restitution");
}
#[test]
fn intent_lcp_impulses_non_negative() {
let mut body = ArticulatedBody::new();
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.05)
.with_friction(0.5));
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let config = LcpSolverConfig::default();
let result = solve_contact_lcp(&body, &constraints, 0.004, &config);
for i in 0..result.lambda_n.len() {
assert!(result.lambda_n[i] >= 0.0,
"contact impulse must be non-negative: lambda_n[{}]={}", i, result.lambda_n[i]);
}
}
#[test]
fn intent_lcp_empty_manifold_zero_impulse() {
let mut body = ArticulatedBody::new();
body.add_body("link", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
let manifold = ContactManifold::new();
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let config = LcpSolverConfig::default();
let result = solve_contact_lcp(&body, &constraints, 0.004, &config);
assert_eq!(result.lambda_n.len(), 0);
}
#[test]
fn global_lcp_single_body_non_negative_impulses() {
use super::super::contact_jacobian::GlobalContactConstraints;
use super::super::contact_jacobian::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));
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 config = LcpSolverConfig::default();
let result = solve_global_contact_lcp(&bodies, &gc, 0.004, &config, None, None);
for i in 0..result.lambda_n.len() {
assert!(result.lambda_n[i] >= -1e-8,
"global LCP normal impulse must be non-negative: lambda_n[{i}]={}", result.lambda_n[i]);
}
assert!(result.delta_qd.len() == body.dof_count(),
"delta_qd should have total_nv elements");
}
#[test]
fn global_lcp_empty_contacts_converges_immediately() {
use super::super::contact_jacobian::GlobalContactConstraints;
use super::super::contact_jacobian::InterBodyContact;
let mut body = ArticulatedBody::new();
body.add_body("link", -1, GenJointType::Floating,
SpatialInertia::sphere(1.0, 0.1), SpatialTransform::identity());
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![];
let inter_contacts: Vec<InterBodyContact> = vec![];
let gc = GlobalContactConstraints::build(
&bodies, &dof_offsets, &dof_counts, &ground_contacts, &inter_contacts);
let result = solve_global_contact_lcp(&bodies, &gc, 0.004, &LcpSolverConfig::default(), None, None);
assert!(result.converged, "empty contacts should converge immediately");
assert_eq!(result.iterations, 0);
assert_eq!(result.lambda_n.len(), 0);
}
#[test]
fn global_lcp_warm_start_does_not_panic() {
use super::super::contact_jacobian::GlobalContactConstraints;
use super::super::contact_jacobian::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.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 config = LcpSolverConfig::default();
let r1 = solve_global_contact_lcp(&bodies, &gc, 0.004, &config, None, None);
let r2 = solve_global_contact_lcp(
&bodies, &gc, 0.004, &config,
Some(&r1.lambda_n), Some(&r1.lambda_t));
assert!(r2.iterations <= r1.iterations + 1,
"warm start should not increase iterations: cold={}, warm={}", r1.iterations, r2.iterations);
}
#[test]
fn lcp_config_default_has_reasonable_values() {
let cfg = LcpSolverConfig::default();
assert!(cfg.max_iterations > 0, "must have at least 1 iteration");
assert!(cfg.tolerance > 0.0, "tolerance must be positive");
}
#[test]
fn stress_lcp_eight_contacts_all_finite() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Floating,
make_inertia(1.0), SpatialTransform::identity());
body.set_joint_q(0, &[0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.0]);
let mut manifold = ContactManifold::new();
let offsets: [(f32, f32); 8] = [
(-0.3, -0.3), (0.3, -0.3), (-0.3, 0.3), (0.3, 0.3),
(-0.1, -0.1), (0.1, -0.1), (-0.1, 0.1), (0.1, 0.1),
];
for &(dx, dz) in &offsets {
manifold.add_contact(
ContactPoint::new(0, Vector3::new(dx, 0.0, dz), Vector3::zeros(), Vector3::y(), 0.005)
.with_friction(0.5),
);
}
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let config = LcpSolverConfig::default();
let result = solve_contact_lcp(&body, &constraints, 0.001, &config);
assert!(result.tau_contact.iter().all(|v| v.is_finite()), "all tau_contact must be finite");
assert!(result.lambda_n.iter().all(|v| v.is_finite()), "all lambda_n must be finite");
assert!(result.residual.is_finite(), "residual must be finite");
assert_eq!(result.lambda_n.len(), 8, "should have 8 contact impulses");
}
#[test]
fn property_lcp_different_sor_all_converge() {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Floating,
make_inertia(1.0), SpatialTransform::identity());
body.set_joint_q(0, &[0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.0]);
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(0, Vector3::new(0.0, 0.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.5),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
for sor in [0.8_f32, 1.0, 1.3, 1.5] {
let mut config = LcpSolverConfig::default();
config.sor = sor;
let result = solve_contact_lcp(&body, &constraints, 0.001, &config);
assert!(
result.residual.is_finite(),
"SOR={sor}: residual must be finite, got {}", result.residual
);
assert!(
result.lambda_n[0] >= -1e-8,
"SOR={sor}: impulse must be non-negative, got {}", result.lambda_n[0]
);
}
}
#[test]
fn intent_lcp_restitution_increases_bounce_velocity() {
let mut body_low = ArticulatedBody::new();
body_low.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body_low.add_body("link", -1, GenJointType::Floating,
make_inertia(1.0), SpatialTransform::identity());
body_low.set_joint_q(0, &[0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.0]);
body_low.set_joint_qd(0, &[0.0, -5.0, 0.0, 0.0, 0.0, 0.0]);
let body_high = body_low.clone();
let mut manifold_low = ContactManifold::new();
manifold_low.add_contact(
ContactPoint::new(0, Vector3::new(0.0, 0.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.3).with_restitution(0.1),
);
let mut manifold_high = ContactManifold::new();
manifold_high.add_contact(
ContactPoint::new(0, Vector3::new(0.0, 0.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.3).with_restitution(0.9),
);
let constraints_low = ContactConstraints::from_manifold(&body_low, &manifold_low);
let constraints_high = ContactConstraints::from_manifold(&body_high, &manifold_high);
let config = LcpSolverConfig::default();
let result_low = solve_contact_lcp(&body_low, &constraints_low, 0.001, &config);
let result_high = solve_contact_lcp(&body_high, &constraints_high, 0.001, &config);
assert!(
result_high.lambda_n[0] >= result_low.lambda_n[0] - 1e-4,
"High restitution impulse ({}) should >= low restitution impulse ({})",
result_high.lambda_n[0], result_low.lambda_n[0]
);
}
#[test]
fn intent_lcp_heavier_body_same_impulse_less_velocity_change() {
let mut body_light = ArticulatedBody::new();
body_light.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body_light.add_body("link", -1, GenJointType::Floating,
make_inertia(1.0), SpatialTransform::identity());
body_light.set_joint_q(0, &[0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.0]);
body_light.set_joint_qd(0, &[0.0, -2.0, 0.0, 0.0, 0.0, 0.0]);
let mut body_heavy = ArticulatedBody::new();
body_heavy.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body_heavy.add_body("link", -1, GenJointType::Floating,
make_inertia(10.0), SpatialTransform::identity());
body_heavy.set_joint_q(0, &[0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.0]);
body_heavy.set_joint_qd(0, &[0.0, -2.0, 0.0, 0.0, 0.0, 0.0]);
let mut m_light = ContactManifold::new();
m_light.add_contact(
ContactPoint::new(0, Vector3::new(0.0, 0.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.3),
);
let mut m_heavy = ContactManifold::new();
m_heavy.add_contact(
ContactPoint::new(0, Vector3::new(0.0, 0.0, 0.0), Vector3::zeros(), Vector3::y(), 0.01)
.with_friction(0.3),
);
let c_light = ContactConstraints::from_manifold(&body_light, &m_light);
let c_heavy = ContactConstraints::from_manifold(&body_heavy, &m_heavy);
let config = LcpSolverConfig::default();
let r_light = solve_contact_lcp(&body_light, &c_light, 0.001, &config);
let r_heavy = solve_contact_lcp(&body_heavy, &c_heavy, 0.001, &config);
let vy_change_light = r_light.tau_contact.iter().map(|v| v.abs()).sum::<f32>();
let vy_change_heavy = r_heavy.tau_contact.iter().map(|v| v.abs()).sum::<f32>();
assert!(vy_change_light.is_finite() && vy_change_heavy.is_finite());
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_lcp_impulses_finite(
pen in 0.001f32..0.02,
mu in 0.1f32..1.0,
vy in -5.0f32..0.0,
) {
let mut body = ArticulatedBody::new();
body.set_gravity(Vector3::new(0.0, -9.81, 0.0));
body.add_body("link", -1, GenJointType::Floating,
make_inertia(1.0), SpatialTransform::identity());
body.set_joint_q(0, &[0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.0]);
body.set_joint_qd(0, &[0.0, vy, 0.0, 0.0, 0.0, 0.0]);
let mut manifold = ContactManifold::new();
manifold.add_contact(
ContactPoint::new(0, Vector3::zeros(), Vector3::zeros(), Vector3::y(), pen)
.with_friction(mu),
);
let constraints = ContactConstraints::from_manifold(&body, &manifold);
let result = solve_contact_lcp(&body, &constraints, 0.001, &LcpSolverConfig::default());
for &l in &result.lambda_n {
prop_assert!(l.is_finite(), "lambda_n must be finite");
prop_assert!(l >= -1e-6, "lambda_n must be non-negative: {l}");
}
for &t in result.tau_contact.as_slice() {
prop_assert!(t.is_finite(), "tau_contact must be finite");
}
}
}
}