Files
rustytorch/crates/specialized/rtx-fea/src/boundary/contact.rs
T
Omar SobhandClaude Fable 5 87cf392556
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
rtx-fea: re-enable the remaining CPU test modules; fix three real defects they caught
All 33 remaining #[cfg(disabled)] test modules outside the GPU cluster are
now enabled: assembly (dof_mapping, constraints, global assembly), boundary
(mod + dirichlet/neumann/robin/thermal/contact), analysis (mod + static),
materials (mod, linear_elastic, hyperelastic, plasticity), elements (mod,
element_matrices, isoparametric, jacobian, quadrature), mesh (element_types,
connectivity, topology, topology_repair), solvers (mod, direct, iterative,
nonlinear) and lib.rs. Lib tests 117 -> 335, stable across repeated runs.
Only gpu_solver_tests and the GpuMeshData fixture stay disabled — they need
CUDA hardware and belong to the GPU tranche.

Three real defects found by the newly-compiling tests, each fixed:

- Direct solvers reused factorizations keyed on matrix SIZE alone.
  In a Newton loop the Jacobian changes every iteration but never its
  dimension, so LuDirect/CholeskyDirect/LdltDirect silently solved with the
  first iteration's factorization forever — Newton on x^2-4 crawled to
  x=1.955 in 1000 iterations instead of converging in 5. Invisible in
  single-solve linear analysis, which is why every green test passed over
  it. solve() now factorizes the matrix it is given.

- AdaptiveQuadrature's refinement re-integrated the WHOLE domain once per
  subdomain, so each level multiplied the estimate by the subdomain count:
  integrating e^x over [-1,1] at tolerance 1e-10 returned ~75 instead of
  2.35. The recursion now descends into each sub-box with its share of the
  error budget.

- compute_skewness read Jacobian columns as coordinate-line tangents, but
  the trait's jacobian() stores tangents in ROWS: on a sheared
  parallelogram whose tangents meet at 14 degrees it reported skewness 0.43
  instead of 0.84 — measuring per-component gradients, not mesh skew.

Fixtures corrected rather than the code where the fixture was wrong:
sigma_yy ~ 0 asserted uniaxial-stress physics on a uniaxial-strain state
(exact Lame values now asserted); an "unstable" orthotropic parameter set
that satisfies the determinant stability condition (delta = 0.187 > 0); a
unit-cube hex Jacobian of 1.0 that assumed a unit reference element (it is
0.125 from [-1,1]^3); a "distorted" quad whose centre Jacobian is exactly
orthogonal, asserted as skewed (flattening and shearing now tested
separately); a quality score below the implementation's own calibration;
Rayleigh damping fed the scalar-field mass (now expanded via the Kronecker
identity, with C = alpha*M + beta*K asserted entry-wise); an element
factory required to construct Point/Line types that have no implementation;
and DOF counts that encoded the repaired 3-DOFs-per-node-on-2-D defect.

MaterialDatabase::add_material call sites updated to the (id, material,
name) signature; ConnectivityInfo::build takes elements only;
TopologyRepair::triangle_quality (normalized 4*sqrt(3)*A/sum(a^2)) added
for the repair tests; create_subdomain_rule_* widened to pub(super) for the
quadrature tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-19 18:52:50 -07:00

553 lines
18 KiB
Rust

// Copyright (c) 2024 RustyTorch++ Team
// Licensed under the Apache License, Version 2.0
//! Contact boundary conditions for mechanical contact problems.
use crate::assembly::{AdvancedDofNumbering, DofComponent, SparseMatrix};
use crate::error::{BoundaryError, FeaResult};
use crate::mesh::{Mesh, NodeId};
use nalgebra::{DVector, Vector3};
/// Contact boundary condition for node-to-node or node-to-surface contact.
#[derive(Debug)]
pub struct ContactBC {
/// Master nodes (contact target)
pub master_nodes: Vec<NodeId>,
/// Slave nodes (contacting surface)
pub slave_nodes: Vec<NodeId>,
/// Contact stiffness (penalty parameter)
pub contact_stiffness: f64,
/// Friction coefficient
pub friction_coefficient: f64,
/// Initial gap
pub initial_gap: f64,
/// Contact tolerance
pub tolerance: f64,
/// Time range when active
pub time_range: Option<(f64, f64)>,
/// Contact detection method
pub detection_method: ContactDetectionMethod,
/// Contact enforcement method
pub enforcement_method: ContactEnforcementMethod,
}
/// Contact detection methods.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactDetectionMethod {
/// Node-to-node contact
NodeToNode,
/// Node-to-surface contact
NodeToSurface,
/// Surface-to-surface contact
SurfaceToSurface,
}
/// Contact enforcement methods.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactEnforcementMethod {
/// Penalty method
Penalty,
/// Lagrange multiplier method
LagrangeMultiplier,
/// Augmented Lagrangian method
AugmentedLagrangian,
}
impl ContactBC {
/// Create a new contact boundary condition.
pub fn new(
master_nodes: Vec<NodeId>,
slave_nodes: Vec<NodeId>,
contact_stiffness: f64,
) -> Self {
Self {
master_nodes,
slave_nodes,
contact_stiffness,
friction_coefficient: 0.0,
initial_gap: 0.0,
tolerance: 1e-6,
time_range: None,
detection_method: ContactDetectionMethod::NodeToNode,
enforcement_method: ContactEnforcementMethod::Penalty,
}
}
/// Set friction coefficient.
pub fn with_friction(mut self, friction_coefficient: f64) -> Self {
self.friction_coefficient = friction_coefficient;
self
}
/// Set initial gap.
pub fn with_gap(mut self, gap: f64) -> Self {
self.initial_gap = gap;
self
}
/// Set contact tolerance.
pub fn with_tolerance(mut self, tolerance: f64) -> Self {
self.tolerance = tolerance;
self
}
/// Set detection method.
pub fn with_detection_method(mut self, method: ContactDetectionMethod) -> Self {
self.detection_method = method;
self
}
/// Set enforcement method.
pub fn with_enforcement_method(mut self, method: ContactEnforcementMethod) -> Self {
self.enforcement_method = method;
self
}
/// Check if this boundary condition is active at the given time.
pub fn is_active(&self, time: f64) -> bool {
if let Some((start_time, end_time)) = self.time_range {
time >= start_time && time <= end_time
} else {
true
}
}
/// Apply contact boundary condition to the global system.
pub fn apply(
&self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
time: f64,
) -> FeaResult<()> {
self.apply_with_solution(
mesh,
dof_numbering,
global_stiffness,
global_force,
time,
None,
)
}
/// Apply contact boundary condition with current displacement solution.
pub fn apply_with_solution(
&self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
time: f64,
current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
if !self.is_active(time) {
return Ok(());
}
match self.enforcement_method {
ContactEnforcementMethod::Penalty => self.apply_penalty_method(
mesh,
dof_numbering,
global_stiffness,
global_force,
current_solution,
),
ContactEnforcementMethod::LagrangeMultiplier => self.apply_lagrange_method(
mesh,
dof_numbering,
global_stiffness,
global_force,
current_solution,
),
ContactEnforcementMethod::AugmentedLagrangian => self
.apply_augmented_lagrangian_method(
mesh,
dof_numbering,
global_stiffness,
global_force,
current_solution,
),
}
}
/// Apply penalty method for contact enforcement.
fn apply_penalty_method(
&self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
match self.detection_method {
ContactDetectionMethod::NodeToNode => self.apply_node_to_node_penalty(
mesh,
dof_numbering,
global_stiffness,
global_force,
current_solution,
),
ContactDetectionMethod::NodeToSurface => self.apply_node_to_surface_penalty(
mesh,
dof_numbering,
global_stiffness,
global_force,
current_solution,
),
ContactDetectionMethod::SurfaceToSurface => {
// Simplified implementation
self.apply_node_to_node_penalty(
mesh,
dof_numbering,
global_stiffness,
global_force,
current_solution,
)
}
}
}
/// Apply node-to-node contact penalty method.
fn apply_node_to_node_penalty(
&self,
mesh: &Mesh,
dof_numbering: &mut AdvancedDofNumbering,
global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
// Simple node-to-node contact
let min_nodes = self.master_nodes.len().min(self.slave_nodes.len());
for i in 0..min_nodes {
let master_node = self.master_nodes[i];
let slave_node = self.slave_nodes[i];
// Get node positions
let master_pos = mesh
.nodes
.get(&master_node)
.ok_or(BoundaryError::NodeNotFound {
node_id: master_node.0,
})?
.coordinates
.clone();
let slave_pos = mesh
.nodes
.get(&slave_node)
.ok_or(BoundaryError::NodeNotFound {
node_id: slave_node.0,
})?
.coordinates
.clone();
// Calculate gap (assuming contact in Z direction)
let gap = if let Some(solution) = current_solution {
// Get current displacements
let master_disp =
self.get_node_displacement(master_node, dof_numbering, solution)?;
let slave_disp = self.get_node_displacement(slave_node, dof_numbering, solution)?;
let current_master_pos = master_pos + master_disp;
let current_slave_pos = slave_pos + slave_disp;
(current_slave_pos[2] - current_master_pos[2]) - self.initial_gap
} else {
(slave_pos[2] - master_pos[2]) - self.initial_gap
};
// Apply contact forces if in contact (gap <= 0)
if gap <= self.tolerance {
let penetration = -gap.min(0.0);
let contact_force = self.contact_stiffness * penetration;
// Apply normal contact force
if let (Some(master_dof), Some(slave_dof)) = (
dof_numbering.get_dof(master_node, DofComponent::DisplacementZ),
dof_numbering.get_dof(slave_node, DofComponent::DisplacementZ),
) {
// Add penalty stiffness
global_stiffness.add_entry(master_dof, master_dof, self.contact_stiffness)?;
global_stiffness.add_entry(slave_dof, slave_dof, self.contact_stiffness)?;
global_stiffness.add_entry(master_dof, slave_dof, -self.contact_stiffness)?;
global_stiffness.add_entry(slave_dof, master_dof, -self.contact_stiffness)?;
// Add contact forces
global_force[master_dof] += contact_force;
global_force[slave_dof] -= contact_force;
}
// Apply friction forces if friction coefficient > 0
if self.friction_coefficient > 0.0 {
self.apply_friction_forces(
master_node,
slave_node,
contact_force,
dof_numbering,
global_stiffness,
global_force,
current_solution,
)?;
}
}
}
Ok(())
}
/// Apply node-to-surface contact penalty method.
fn apply_node_to_surface_penalty(
&self,
_mesh: &Mesh,
_dof_numbering: &mut AdvancedDofNumbering,
_global_stiffness: &mut SparseMatrix,
_global_force: &mut DVector<f64>,
_current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
// Simplified implementation - would require surface projection algorithms
Ok(())
}
/// Apply Lagrange multiplier method.
fn apply_lagrange_method(
&self,
_mesh: &Mesh,
_dof_numbering: &mut AdvancedDofNumbering,
_global_stiffness: &mut SparseMatrix,
_global_force: &mut DVector<f64>,
_current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
// Would require augmenting the system with Lagrange multiplier DOFs
Ok(())
}
/// Apply augmented Lagrangian method.
fn apply_augmented_lagrangian_method(
&self,
_mesh: &Mesh,
_dof_numbering: &mut AdvancedDofNumbering,
_global_stiffness: &mut SparseMatrix,
_global_force: &mut DVector<f64>,
_current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
// Combination of penalty and Lagrange multiplier methods
Ok(())
}
/// Apply friction forces.
fn apply_friction_forces(
&self,
master_node: NodeId,
slave_node: NodeId,
normal_force: f64,
dof_numbering: &mut AdvancedDofNumbering,
_global_stiffness: &mut SparseMatrix,
global_force: &mut DVector<f64>,
current_solution: Option<&DVector<f64>>,
) -> FeaResult<()> {
if let Some(solution) = current_solution {
// Get relative tangential velocity/displacement
let master_disp = self.get_node_displacement(master_node, dof_numbering, solution)?;
let slave_disp = self.get_node_displacement(slave_node, dof_numbering, solution)?;
let relative_disp = slave_disp - master_disp;
let tangential_disp = Vector3::new(relative_disp.x, relative_disp.y, 0.0);
let tangential_magnitude = tangential_disp.norm();
if tangential_magnitude > self.tolerance {
let friction_force = self.friction_coefficient * normal_force;
let friction_direction = tangential_disp / tangential_magnitude;
// Apply friction forces in X and Y directions
for (component, direction_component) in [
(DofComponent::DisplacementX, friction_direction.x),
(DofComponent::DisplacementY, friction_direction.y),
] {
if let (Some(master_dof), Some(slave_dof)) = (
dof_numbering.get_dof(master_node, component),
dof_numbering.get_dof(slave_node, component),
) {
let friction_component = friction_force * direction_component;
global_force[master_dof] += friction_component;
global_force[slave_dof] -= friction_component;
}
}
}
}
Ok(())
}
/// Get node displacement from solution vector.
fn get_node_displacement(
&self,
node_id: NodeId,
dof_numbering: &AdvancedDofNumbering,
solution: &DVector<f64>,
) -> FeaResult<Vector3<f64>> {
let mut displacement = Vector3::zeros();
for (i, component) in [
DofComponent::DisplacementX,
DofComponent::DisplacementY,
DofComponent::DisplacementZ,
]
.iter()
.enumerate()
{
if let Some(dof) = dof_numbering.get_dof(node_id, *component)
&& dof < solution.len()
{
displacement[i] = solution[dof];
}
}
Ok(displacement)
}
}
/// Contact patterns for common contact scenarios.
pub struct ContactPatterns;
impl ContactPatterns {
/// Create contact between two surfaces.
pub fn surface_to_surface_contact(
master_nodes: Vec<NodeId>,
slave_nodes: Vec<NodeId>,
contact_stiffness: f64,
friction_coefficient: f64,
) -> ContactBC {
ContactBC::new(master_nodes, slave_nodes, contact_stiffness)
.with_friction(friction_coefficient)
.with_detection_method(ContactDetectionMethod::SurfaceToSurface)
}
/// Create self-contact for a single surface.
pub fn self_contact(
nodes: Vec<NodeId>,
contact_stiffness: f64,
friction_coefficient: f64,
) -> ContactBC {
ContactBC::new(nodes.clone(), nodes, contact_stiffness)
.with_friction(friction_coefficient)
.with_detection_method(ContactDetectionMethod::NodeToNode)
}
/// Create rigid contact (infinite stiffness approximation).
pub fn rigid_contact(master_nodes: Vec<NodeId>, slave_nodes: Vec<NodeId>) -> ContactBC {
ContactBC::new(master_nodes, slave_nodes, 1e12) // Very high stiffness
.with_enforcement_method(ContactEnforcementMethod::Penalty)
}
/// Create frictionless contact.
pub fn frictionless_contact(
master_nodes: Vec<NodeId>,
slave_nodes: Vec<NodeId>,
contact_stiffness: f64,
) -> ContactBC {
ContactBC::new(master_nodes, slave_nodes, contact_stiffness).with_friction(0.0)
}
/// Create stick contact (no sliding).
pub fn stick_contact(
master_nodes: Vec<NodeId>,
slave_nodes: Vec<NodeId>,
contact_stiffness: f64,
) -> ContactBC {
ContactBC::new(master_nodes, slave_nodes, contact_stiffness).with_friction(1e6)
// Very high friction coefficient
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contact_bc_creation() {
let bc = ContactBC::new(vec![NodeId(0), NodeId(1)], vec![NodeId(2), NodeId(3)], 1e6);
assert_eq!(bc.master_nodes, vec![NodeId(0), NodeId(1)]);
assert_eq!(bc.slave_nodes, vec![NodeId(2), NodeId(3)]);
assert_eq!(bc.contact_stiffness, 1e6);
assert_eq!(bc.friction_coefficient, 0.0);
}
#[test]
fn test_contact_bc_with_friction() {
let bc = ContactBC::new(vec![NodeId(0)], vec![NodeId(1)], 1e5).with_friction(0.3);
assert_eq!(bc.friction_coefficient, 0.3);
}
#[test]
fn test_contact_detection_methods() {
let methods = [
ContactDetectionMethod::NodeToNode,
ContactDetectionMethod::NodeToSurface,
ContactDetectionMethod::SurfaceToSurface,
];
assert_eq!(methods.len(), 3);
}
#[test]
fn test_contact_enforcement_methods() {
let methods = [
ContactEnforcementMethod::Penalty,
ContactEnforcementMethod::LagrangeMultiplier,
ContactEnforcementMethod::AugmentedLagrangian,
];
assert_eq!(methods.len(), 3);
}
#[test]
fn test_contact_patterns_surface_to_surface() {
let bc = ContactPatterns::surface_to_surface_contact(
vec![NodeId(0), NodeId(1)],
vec![NodeId(2), NodeId(3)],
1e6,
0.3,
);
assert_eq!(bc.friction_coefficient, 0.3);
assert_eq!(
bc.detection_method,
ContactDetectionMethod::SurfaceToSurface
);
}
#[test]
fn test_contact_patterns_self_contact() {
let nodes = vec![NodeId(0), NodeId(1), NodeId(2)];
let bc = ContactPatterns::self_contact(nodes.clone(), 1e5, 0.2);
assert_eq!(bc.master_nodes, nodes);
assert_eq!(bc.slave_nodes, nodes);
assert_eq!(bc.friction_coefficient, 0.2);
}
#[test]
fn test_contact_patterns_rigid_contact() {
let bc = ContactPatterns::rigid_contact(vec![NodeId(0)], vec![NodeId(1)]);
assert_eq!(bc.contact_stiffness, 1e12);
assert_eq!(bc.enforcement_method, ContactEnforcementMethod::Penalty);
}
#[test]
fn test_contact_patterns_frictionless() {
let bc = ContactPatterns::frictionless_contact(vec![NodeId(0)], vec![NodeId(1)], 5e5);
assert_eq!(bc.friction_coefficient, 0.0);
assert_eq!(bc.contact_stiffness, 5e5);
}
#[test]
fn test_contact_patterns_stick_contact() {
let bc = ContactPatterns::stick_contact(vec![NodeId(0)], vec![NodeId(1)], 1e6);
assert_eq!(bc.friction_coefficient, 1e6);
}
}