forked from 1b-agent/fieldcraft-upstream
explicit: deformable contact and friction, verified by bars, sliding blocks, the patch test and Hertz
Segment-to-segment penalty contact between element faces: each secondary face and each main face facing it are projected onto the secondary face's plane, split into triangles, clipped (Sutherland–Hodgman) and integrated with a three-point rule per sub-triangle (Puso–Laursen segmentation, the penalty at the integration points as Fischer–Wriggers), so a uniform pressure gives the consistent nodal forces on both meshes and the contact patch test passes on non-matching meshes. Every contact runs both ways at half the penalty: pairs with each surface as the secondary, general and self contact face against face. A bucket grid of face boxes every increment, faces integrated in parallel and their forces added in face order: the same numbers on any thread count. The penalty per area ε = s γ min(m/A)/Δt² comes from the lumped masses, each node's directional area and the stable increment, bounded by Gershgorin's row sums so contact never shortens the increment. Coulomb friction with exponential decay by a regularised return map at the secondary nodes; rigid walls take Coulomb impulses. Contact work and friction dissipation join the energy balance; each contact reports its peak force, first and last contact, deepest penetration, friction and the nodes adjusted at the start (initial penetrations adjusted strain-free or refused with the node, face and depth); a cap on contact faces. Verified (crates/explicit/tests/contact.rs, tests/oracle/explicit-*): colliding bars (2L/c +1.2%, ρcAv₀/2, KE kept to 0.005%, contact energy back to 8e-5 of it), blocks sliding to a stop on a rigid wall and on a plate under constant and decaying friction (0.005–0.2%), the patch test (σ_zz uniform to 0.06%), two spheres colliding against Hertz (peak +2.3%, duration −0.4%; the oracle case waits for a graded sphere mesher), a free body untouched by general contact, determinism across threads. Oracle measures gain `over` (ratios). Existing fixtures do not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
ecf947c090
commit
a30d0b5c5b
37 files changed
+2459
-55
No files matched your search
@@ -8,12 +8,13 @@ use fieldcraft_fem::plasticity::J2;
|
||||
use fieldcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
|
||||
use fieldcraft_fem::{DofKind, DofLayout, DofSet, face};
|
||||
use fieldcraft_model::{
|
||||
Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
|
||||
Elastic, ElementKind, ExplicitDynamic, Friction, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
|
||||
};
|
||||
|
||||
use crate::contact::Interface;
|
||||
use crate::discrete::{Discrete, Rod};
|
||||
use crate::elements::{HexScales, add_at, frequency_bound, hex_scales};
|
||||
use crate::{Error, Result};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// Degrees of freedom per node: the three translations, node-major (see [`System`]).
|
||||
pub const NODE_DOFS: usize = 3;
|
||||
@@ -81,13 +82,14 @@ pub struct Tet {
|
||||
pub thermal_rate: f64,
|
||||
}
|
||||
|
||||
/// A fixed, frictionless rigid plane.
|
||||
/// A fixed rigid plane.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Wall {
|
||||
pub point: [f64; 3],
|
||||
/// Unit normal, towards the side the nodes stay on.
|
||||
pub normal: [f64; 3],
|
||||
pub nodes: Vec<usize>,
|
||||
pub friction: Friction,
|
||||
}
|
||||
|
||||
/// The discrete system of an explicit dynamic step.
|
||||
@@ -117,6 +119,8 @@ pub struct System {
|
||||
/// Prescribed velocity per degree of freedom (`None` = free).
|
||||
pub prescribed: Vec<Option<f64>>,
|
||||
pub walls: Vec<Wall>,
|
||||
/// The model's contacts ([`crate::contact`]).
|
||||
pub contacts: Vec<Interface>,
|
||||
/// Velocity per degree of freedom at the start (prescribed velocities included).
|
||||
pub initial_velocity: Vec<f64>,
|
||||
/// Constant external force per degree of freedom.
|
||||
@@ -192,16 +196,12 @@ fn check_formulation(kind: ElementKind, section_name: &str, f: &fieldcraft_model
|
||||
}
|
||||
|
||||
impl System {
|
||||
pub fn new(model: &Model, resolved: &Resolved, step: &Step, settings: &ExplicitDynamic, max_dofs: usize) -> Result<System> {
|
||||
pub fn new(model: &Model, resolved: &Resolved, step: &Step, settings: &ExplicitDynamic, limits: &Limits) -> Result<System> {
|
||||
let max_dofs = limits.max_dofs;
|
||||
if let Some(name) = model.couplings().keys().next() {
|
||||
return Err(Error::NotSupported(format!("couplings (coupling `{name}`) in explicit dynamics")));
|
||||
}
|
||||
if let Some(name) = model.contacts().keys().next() {
|
||||
return Err(Error::NotSupported(format!("contact (`{name}`) in explicit dynamics")));
|
||||
}
|
||||
if let Some(w) = settings.rigid_walls.iter().find(|w| !w.friction.is_frictionless()) {
|
||||
return Err(Error::NotSupported(format!("friction on rigid walls (the wall on set `{}`)", w.set)));
|
||||
}
|
||||
|
||||
// Elements by label, with their section's material.
|
||||
let mut materials: Vec<Material> = Vec::new();
|
||||
let mut chosen: Vec<(u32, ElementKind, Vec<u32>, usize, Option<f64>)> = Vec::new();
|
||||
@@ -274,6 +274,7 @@ impl System {
|
||||
discrete_increment: None,
|
||||
prescribed: vec![None; dofs],
|
||||
walls: Vec::new(),
|
||||
contacts: Vec::new(),
|
||||
initial_velocity: vec![0.0; dofs],
|
||||
external: vec![0.0; dofs],
|
||||
history_nodes: Vec::new(),
|
||||
@@ -285,6 +286,20 @@ impl System {
|
||||
labels,
|
||||
xyz,
|
||||
};
|
||||
// Contact first: nodes that start inside a surface are moved before the elements are
|
||||
// built on the coordinates.
|
||||
sys.contacts = crate::contact::interfaces(model, resolved, &mut sys.xyz, &sys.labels, &sys.elements, limits.max_contact_faces)?;
|
||||
for c in &sys.contacts {
|
||||
if let Some(deepest) = c.adjusted.iter().max_by(|a, b| a.depth.total_cmp(&b.depth)) {
|
||||
sys.warnings.push(format!(
|
||||
"contact `{}`: {} nodes started inside the other surface and were moved onto it (the deepest, node {}, by {:.3e})",
|
||||
c.name,
|
||||
c.adjusted.len(),
|
||||
deepest.node,
|
||||
deepest.depth
|
||||
));
|
||||
}
|
||||
}
|
||||
for (index, (label, kind, nodes, mi, area)) in chosen.into_iter().enumerate() {
|
||||
let idx: Vec<usize> = nodes.iter().map(|l| sys.node(*l).ok_or_else(|| Error::Internal(format!("node {l}")))).collect::<Result<_>>()?;
|
||||
let mat = sys.materials.get(mi).ok_or_else(|| Error::Internal("material index".into()))?;
|
||||
@@ -590,7 +605,7 @@ impl System {
|
||||
return Err(invalid(format!("node {label} of set `{}` starts behind its rigid wall (gap {gap:e})", w.set)));
|
||||
}
|
||||
}
|
||||
self.walls.push(Wall { point: w.point, normal, nodes });
|
||||
self.walls.push(Wall { point: w.point, normal, nodes, friction: w.friction });
|
||||
}
|
||||
if let Some(set) = &settings.history_nodes {
|
||||
let nodes = self.nodes_of(resolved, set);
|
||||
|
||||
Reference in new issue
Block a user