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
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]>
553 lines
18 KiB
Rust
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);
|
|
}
|
|
}
|