forked from 1b-agent/fieldcraft-upstream
WIP (parked): folding robustness, stabilization, mesh.revolve, boot-seal profile
Parked work for #182/#185 (not for merge as is): - contact: held active set with primal-dual updates replaces the inconsistent ENGAGED_BAND tangent after chattering (moves the nl-friction-sliding-blocks fixture by 4.5e-8; not re-blessed) - nonlinear: opt-in viscous stabilization (step 'stabilization', calibrated on the first increment), Abaqus STABILIZE, LS-DYNA refusal - ordering: graph (level-structure) nested dissection, chosen when it fills less: half the fill on the revolved boot, factor time 62 s -> 25 s - engine: mesh.revolve command (2D profile -> hex8/wedge6, tags kept) - engine: sample/boot.rs boot-seal profile, axisymmetric and 3D scripts Scratch still in the code: FC_TRACE/TIMES timing prints, NO_BAND/STAB/ SQUEEZE env hooks in tests, boot_axisymmetric_scratch test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
227c4544ed
commit
98eaede2dd
18 files changed
+1111
-57
No files matched your search
@@ -15,7 +15,7 @@ use crate::cholesky::{Factor, FactorError, UpperColumns};
|
||||
use crate::constraints::{Budget, Reduction, coupling_rows};
|
||||
use crate::eigen::{Operator, Wanted, lanczos};
|
||||
use crate::ldlt::{Kind, Symmetric};
|
||||
use crate::ordering::nested_dissection;
|
||||
use crate::ordering::{fill, graph_dissection, nested_dissection};
|
||||
use crate::sparse::{Csr, NodeGraph, add_block, add_diagonal, add_scalar};
|
||||
use crate::system::{
|
||||
Mesh, Structural, StructuralKernel, add_thermal_loads, inelastic_warnings, loads, prescribed, structural_body_forces, temperature_loads,
|
||||
@@ -311,12 +311,12 @@ pub(crate) fn pattern(mesh: &Mesh) -> (NodeGraph, Vec<usize>) {
|
||||
let graph = NodeGraph::new(mesh.block_count(), mesh.blocks());
|
||||
// The order covers the mesh nodes; reference point equations follow them.
|
||||
let n = mesh.labels.len();
|
||||
let order = if graph.adjacency.len() == n {
|
||||
nested_dissection(&mesh.xyz, &graph.adjacency)
|
||||
} else {
|
||||
let mesh_part: Vec<Vec<usize>> = graph.adjacency.iter().take(n).map(|row| row.iter().copied().filter(|b| *b < n).collect()).collect();
|
||||
nested_dissection(&mesh.xyz, &mesh_part)
|
||||
};
|
||||
let mesh_part: Vec<Vec<usize>> = graph.adjacency.iter().take(n).map(|row| row.iter().copied().filter(|b| *b < n).collect()).collect();
|
||||
// The dissection that fills the factor less: by coordinate planes, or by the graph's own
|
||||
// level structures (better on thin curved walls).
|
||||
let by_planes = nested_dissection(&mesh.xyz, &mesh_part);
|
||||
let by_graph = graph_dissection(&mesh_part);
|
||||
let order = if fill(&mesh_part, &by_graph) < fill(&mesh_part, &by_planes) { by_graph } else { by_planes };
|
||||
(graph, order)
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,11 @@ fn ereach(a: &UpperColumns, k: usize, parent: &[usize], stack: &mut [usize], mar
|
||||
}
|
||||
|
||||
/// The elimination tree and the entry count of each column of L, without computing values.
|
||||
/// The entries of the Cholesky factor of `a`'s pattern (in its own order).
|
||||
pub(crate) fn factor_entries(a: &UpperColumns) -> usize {
|
||||
symbolic(a).1.iter().sum()
|
||||
}
|
||||
|
||||
fn symbolic(a: &UpperColumns) -> (Vec<usize>, Vec<usize>) {
|
||||
let parent = etree(a);
|
||||
let mut counts = vec![1usize; a.n];
|
||||
|
||||
+135
-19
@@ -119,19 +119,14 @@ pub const PENALTY_FACTOR: f64 = 10.0;
|
||||
/// the element under it (and no open node keeps a multiplier).
|
||||
pub const GAP_TOLERANCE: f64 = 1e-5;
|
||||
|
||||
/// Once an increment's active set has chattered, a node open by less than this fraction of its
|
||||
/// element's thickness keeps its penalty in the tangent for the rest of the step (its
|
||||
/// pressure, and so the residual, is still zero): a node that touches with no pressure, the edge
|
||||
/// of a contact that lifts, otherwise flips between open and closed from one Newton iteration to
|
||||
/// the next, each state's correction pushing it into the other, and a smaller increment does not
|
||||
/// help under proportional loading.
|
||||
pub const ENGAGED_BAND: f64 = 1e-3;
|
||||
/// Most updates of a held active set in one increment.
|
||||
pub const MAX_ACTIVE_SET_UPDATES: usize = 8;
|
||||
|
||||
/// Most augmentations of one increment.
|
||||
pub const MAX_AUGMENTATIONS: usize = 25;
|
||||
|
||||
/// An increment is cut back when the active set alternates between two states this many times
|
||||
/// (and the step takes [`ENGAGED_BAND`] from then on).
|
||||
/// An increment whose active set alternates between two states this many times holds it fixed
|
||||
/// ([`Committed::active`]); if it still alternates once released, the increment is cut back.
|
||||
pub const CHATTER_LIMIT: usize = 3;
|
||||
|
||||
/// Most faces over the contacts of a model.
|
||||
@@ -187,6 +182,10 @@ pub struct Committed {
|
||||
pub multipliers: Vec<f64>,
|
||||
pub traction: Vec<Vec3>,
|
||||
pub slipping: Vec<bool>,
|
||||
/// An active set held fixed while an increment that chattered converges: per secondary
|
||||
/// node, closed (its pressure λ − ε ĝ, tension allowed) or open (none). `None`: each
|
||||
/// evaluation decides, p = ⟨λ − ε ĝ⟩.
|
||||
pub active: Option<Vec<bool>>,
|
||||
}
|
||||
|
||||
impl Committed {
|
||||
@@ -793,13 +792,13 @@ impl Interface {
|
||||
multipliers: vec![0.0; n],
|
||||
traction: if friction { vec![[0.0; 3]; n] } else { Vec::new() },
|
||||
slipping: vec![false; if friction { n } else { 0 }],
|
||||
active: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The contact's forces at displacements `u` (added to `internal`), its tangent blocks when
|
||||
/// `tangent`, and its state, from the last converged increment's state `committed` and
|
||||
/// displacements `reference`; nodes open by less than `band` of their element's thickness
|
||||
/// keep their penalty in the tangent ([`ENGAGED_BAND`]; 0: only closed nodes).
|
||||
/// `tangent`, and its state, from the last converged increment's state `committed` (with
|
||||
/// its active set when one is held fixed) and displacements `reference`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
@@ -807,7 +806,6 @@ impl Interface {
|
||||
u: &[f64],
|
||||
reference: &[f64],
|
||||
committed: &Committed,
|
||||
band: f64,
|
||||
tangent: bool,
|
||||
internal: &mut [f64],
|
||||
blocks: &mut Blocks,
|
||||
@@ -837,17 +835,21 @@ impl Interface {
|
||||
for v in &mut state.normal {
|
||||
*v = unit(*v).unwrap_or(*v);
|
||||
}
|
||||
// Nodes closed, or open by less than `band` of their element's thickness: their penalty
|
||||
// stays in the tangent.
|
||||
// The closed nodes (their penalty in the tangent): those pressing, or those an active
|
||||
// set held fixed closes.
|
||||
let mut engaged = vec![false; n];
|
||||
for i in 0..self.nodes.len() {
|
||||
let (Some(Some(g)), Some(a), Some(e)) = (weighted.get(i), self.area.get(i), self.penalty.get(i)) else { continue };
|
||||
let gap = g / a;
|
||||
let lambda = multipliers.get(i).copied().unwrap_or(0.0);
|
||||
if let (Some(slot), Some(p), Some(h)) = (state.gap.get_mut(i), state.pressure.get_mut(i), self.thickness.get(i)) {
|
||||
let trial = multipliers.get(i).copied().unwrap_or(0.0) - e * gap;
|
||||
let closed = match committed.active.as_ref().and_then(|s| s.get(i)) {
|
||||
Some(held) => *held,
|
||||
None => trial > 0.0,
|
||||
};
|
||||
if let (Some(slot), Some(p)) = (state.gap.get_mut(i), state.pressure.get_mut(i)) {
|
||||
*slot = Some(gap);
|
||||
*p = (lambda - e * gap).max(0.0);
|
||||
engaged[i] = lambda - e * gap > -e * band * h;
|
||||
*p = if closed { trial } else { 0.0 };
|
||||
engaged[i] = closed;
|
||||
}
|
||||
}
|
||||
// The pairs with a closed secondary corner, linearised.
|
||||
@@ -1172,6 +1174,29 @@ impl Interface {
|
||||
}
|
||||
}
|
||||
|
||||
/// The active set held at `committed`, checked at `state` (computed with it): `None` when it
|
||||
/// is consistent there (no held-closed node pulls with more than ε_A times
|
||||
/// [`GAP_TOLERANCE`] of its element's thickness, and no held-open node presses by more: a
|
||||
/// node within that band touches with no pressure, and either status is the solution to the
|
||||
/// tolerance), else the set with the offending nodes switched (the primal–dual active set
|
||||
/// update; S. Hüeber, B. I. Wohlmuth, *A primal–dual active set strategy for non-linear
|
||||
/// multibody contact problems*, CMAME 194:3147–3166, 2005).
|
||||
pub fn update_held(&self, committed: &Committed, state: &State) -> Option<Vec<bool>> {
|
||||
let held = committed.active.as_ref()?;
|
||||
let mut next = held.clone();
|
||||
let mut changed = false;
|
||||
for (i, closed) in next.iter_mut().enumerate() {
|
||||
let (Some(Some(gap)), Some(e), Some(h)) = (state.gap.get(i), self.penalty.get(i), self.thickness.get(i)) else { continue };
|
||||
let trial = committed.multipliers.get(i).copied().unwrap_or(0.0) - e * gap;
|
||||
let limit = e * GAP_TOLERANCE * h;
|
||||
if (*closed && trial < -limit) || (!*closed && trial > limit) {
|
||||
*closed = !*closed;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed.then_some(next)
|
||||
}
|
||||
|
||||
/// How far the augmentation is from converged at `state`, computed with `multipliers`: the
|
||||
/// largest gap of a closed node, or the gap equivalent λ_A / ε_A of the multiplier an open
|
||||
/// node keeps, over the thickness of the element under the node.
|
||||
@@ -1186,3 +1211,94 @@ impl Interface {
|
||||
worst
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fieldcraft_model::{Elastic, Material, Section, SectionKind, Set};
|
||||
|
||||
/// Two separate quads facing each other, the upper one tilted and pressed into the lower
|
||||
/// one, with general contact; `axisymmetric` off the axis.
|
||||
fn pressed_quads(axisymmetric: bool) -> Model {
|
||||
let mut m = Model::default();
|
||||
let x0 = if axisymmetric { 2.0 } else { 0.0 };
|
||||
let lower = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
||||
let upper = [[0.25, 1.03], [1.2, 1.01], [1.25, 1.9], [0.3, 1.95]];
|
||||
for (i, p) in lower.iter().chain(&upper).enumerate() {
|
||||
m.set_node(i as u32 + 1, [x0 + p[0], p[1], 0.0]).unwrap();
|
||||
}
|
||||
m.set_element(1, ElementKind::Quad4, vec![1, 2, 3, 4]).unwrap();
|
||||
m.set_element(2, ElementKind::Quad4, vec![5, 6, 7, 8]).unwrap();
|
||||
m.add_set("all", Set { elements: vec![1, 2], ..Set::default() }).unwrap();
|
||||
m.add_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: 10.0, poisson: 0.3 }), ..Material::default() }).unwrap();
|
||||
let kind = if axisymmetric { SectionKind::Axisymmetric {} } else { SectionKind::PlaneStrain { thickness: 1.5 } };
|
||||
m.add_section("s", Section { set: "all".into(), material: "Mat".into(), kind, formulation: Default::default() }).unwrap();
|
||||
m.add_contact("c", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
|
||||
let step = fieldcraft_model::Step::new("Step", fieldcraft_model::Procedure::NonlinearStatic(Default::default()));
|
||||
m.add_step(step, None).unwrap();
|
||||
m
|
||||
}
|
||||
|
||||
/// The contact's tangent blocks are the derivatives of its forces (central differences), in
|
||||
/// plane strain and axisymmetric models.
|
||||
#[test]
|
||||
fn the_2d_contact_tangent_is_the_derivative_of_its_forces() {
|
||||
for axisymmetric in [false, true] {
|
||||
let m = pressed_quads(axisymmetric);
|
||||
let resolved = m.resolve().unwrap();
|
||||
let mesh = Mesh::new(&m, &resolved, 1_000_000).unwrap();
|
||||
let interfaces = Interface::all(&m, &resolved, &mesh, true, &mut Vec::new()).unwrap();
|
||||
let n = mesh.dofs();
|
||||
// A small motion of every node, so no pair is symmetric by accident, and the upper
|
||||
// quad pressed into the lower one.
|
||||
let mut u: Vec<f64> = (0..n).map(|i| 0.01 * ((i * 7 % 5) as f64 - 2.0)).collect();
|
||||
for node in 4..8 {
|
||||
if let Some(d) = translations(&mesh, node).ok().and_then(|d| d.get(1).copied()) {
|
||||
u[d] -= 0.06;
|
||||
}
|
||||
}
|
||||
let forces = |u: &[f64], blocks: &mut Blocks| {
|
||||
let mut f = vec![0.0; n];
|
||||
for c in &interfaces {
|
||||
let committed = c.committed();
|
||||
c.evaluate(&mesh, u, &vec![0.0; n], &committed, true, &mut f, blocks).unwrap();
|
||||
}
|
||||
f
|
||||
};
|
||||
let mut blocks = Blocks::new();
|
||||
forces(&u, &mut blocks);
|
||||
// The dense tangent over (node, x/y) from the 3-per-node blocks.
|
||||
let dof = |node: usize, k: usize| translations(&mesh, node).ok().and_then(|d| d.get(k).copied());
|
||||
let mut k = vec![0.0; n * n];
|
||||
for (nodes, data) in &blocks {
|
||||
let w = 3 * nodes.len();
|
||||
for (a, na) in nodes.iter().enumerate() {
|
||||
for (b, nb) in nodes.iter().enumerate() {
|
||||
for r in 0..2 {
|
||||
for c in 0..2 {
|
||||
if let (Some(i), Some(j)) = (dof(*na, r), dof(*nb, c)) {
|
||||
k[i * n + j] += data[(3 * a + r) * w + 3 * b + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let scale = k.iter().fold(0.0f64, |a, v| a.max(v.abs()));
|
||||
assert!(scale > 0.0, "the quads are in contact");
|
||||
let h = 1e-7;
|
||||
for j in 0..n {
|
||||
let shifted = |s: f64| {
|
||||
let mut v = u.clone();
|
||||
v[j] += s;
|
||||
forces(&v, &mut Blocks::new())
|
||||
};
|
||||
let (p, q) = (shifted(h), shifted(-h));
|
||||
for i in 0..n {
|
||||
let fd = (p[i] - q[i]) / (2.0 * h);
|
||||
assert!((fd - k[i * n + j]).abs() < 1e-5 * scale, "axisymmetric {axisymmetric}: ({i}, {j}) {fd} vs {}", k[i * n + j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,7 +527,9 @@ impl Symmetric {
|
||||
Kind::Definite => Factor::new(a, max_entries).map(Symmetric::Cholesky),
|
||||
Kind::Indefinite => LdltFactor::new(a, max_entries).map(Symmetric::Indefinite),
|
||||
Kind::Auto => {
|
||||
let t0 = std::time::Instant::now();
|
||||
let symbolic = Symbolic::new(a, max_entries)?;
|
||||
if std::env::var("FC_TRACE").is_ok() { eprintln!("TRACE symbolic {:?}", t0.elapsed()); }
|
||||
match Factor::from_symbolic(&symbolic) {
|
||||
Ok(f) => Ok(Symmetric::Cholesky(f)),
|
||||
Err(FactorError::NotPositiveDefinite { .. }) => LdltFactor::from_symbolic(&symbolic).map(Symmetric::Indefinite),
|
||||
|
||||
+212
-26
@@ -217,6 +217,16 @@ pub struct History {
|
||||
pub iterations: usize,
|
||||
/// Arc-length steps: where the tangent's inertia changes along the path.
|
||||
pub critical_points: Vec<CriticalPoint>,
|
||||
/// Steps with stabilization: the energy it dissipated.
|
||||
pub stabilization: Option<Stabilization>,
|
||||
}
|
||||
|
||||
/// What a step's viscous stabilization did: the energy its damping dissipated, and the work of
|
||||
/// the internal forces over the step (the energy the model stores, contact's included).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
|
||||
pub struct Stabilization {
|
||||
pub dissipated: f64,
|
||||
pub stored: f64,
|
||||
}
|
||||
|
||||
impl History {
|
||||
@@ -229,6 +239,11 @@ impl History {
|
||||
/// Most displacement values the frames of one step keep (3 per node per frame).
|
||||
pub const MAX_FRAME_VALUES: usize = 30_000_000;
|
||||
|
||||
|
||||
/// A stabilization that dissipates more than this fraction of the work of the internal forces is
|
||||
/// reported.
|
||||
const STABILIZATION_WARNING: f64 = 0.05;
|
||||
|
||||
/// Line search: accept a step whose residual work is at most this fraction of the first.
|
||||
const LINE_SEARCH_ACCEPT: f64 = 0.8;
|
||||
/// Line search: most trial steps, and the range of the step.
|
||||
@@ -425,7 +440,6 @@ impl Problem<'_> {
|
||||
committed: &[Vec<f64>],
|
||||
contact: &[contact::Committed],
|
||||
reference: &[f64],
|
||||
band: f64,
|
||||
) -> Result<Evaluation> {
|
||||
let layout = &self.mesh.layout;
|
||||
let n = u.len();
|
||||
@@ -433,10 +447,13 @@ impl Problem<'_> {
|
||||
// Contact first: its face pairs extend the tangent's pattern.
|
||||
let mut blocks = contact::Blocks::new();
|
||||
let mut states = Vec::with_capacity(self.contacts.len());
|
||||
let t0 = std::time::Instant::now();
|
||||
for (i, c) in self.contacts.iter().enumerate() {
|
||||
let empty = contact::Committed::default();
|
||||
states.push(c.evaluate(self.mesh, u, reference, contact.get(i).unwrap_or(&empty), band, tangent, &mut internal, &mut blocks)?);
|
||||
states.push(c.evaluate(self.mesh, u, reference, contact.get(i).unwrap_or(&empty), tangent, &mut internal, &mut blocks)?);
|
||||
}
|
||||
TIMES[0].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut k = tangent.then(|| {
|
||||
if blocks.is_empty() {
|
||||
self.pattern.clone()
|
||||
@@ -451,6 +468,8 @@ impl Problem<'_> {
|
||||
add_block(k, layout, nodes, kinds, &translation_block(data, nodes.len(), kinds))?;
|
||||
}
|
||||
}
|
||||
TIMES[1].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut history = Vec::with_capacity(self.mesh.elements.len());
|
||||
for round in parallel::blocks(self.mesh.elements.len(), ROUND) {
|
||||
let start = round.start;
|
||||
@@ -473,6 +492,7 @@ impl Problem<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
TIMES[2].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(s) = &self.springs {
|
||||
let mut ku = vec![0.0; s.n];
|
||||
s.mul(u.get(..s.n).unwrap_or_default(), &mut ku);
|
||||
@@ -585,14 +605,19 @@ struct Run<'a> {
|
||||
contact: Vec<contact::Committed>,
|
||||
/// The displacements of the last converged increment (all degrees of freedom).
|
||||
reference: Vec<f64>,
|
||||
/// The band of the engaged contact nodes ([`contact::ENGAGED_BAND`] once an increment's
|
||||
/// active set has chattered, for the rest of the step; else 0).
|
||||
band: f64,
|
||||
augmentations: usize,
|
||||
/// The augmentation being solved in the current increment.
|
||||
augmentation: usize,
|
||||
/// The largest force of the converged states (contact's force scale, [`Run::residual`]).
|
||||
converged_scale: f64,
|
||||
/// Viscous stabilization: the volume each free equation's node carries, and the damping
|
||||
/// c V per free equation once the first increment has calibrated c; the current
|
||||
/// increment's size as a fraction of the step (the pseudo-time step).
|
||||
volumes: Option<Vec<f64>>,
|
||||
damping: Option<Vec<f64>>,
|
||||
pseudo_time: f64,
|
||||
/// The energy the stabilization has dissipated over the converged increments.
|
||||
dissipated: f64,
|
||||
}
|
||||
|
||||
/// The outcome of a converged increment.
|
||||
@@ -624,6 +649,28 @@ impl Run<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the stabilization's damping forces (D/Δt)(q − q₀) out of the free residual `r` at
|
||||
/// `q`, the increment starting from `q0`.
|
||||
fn damp(&self, r: &mut [f64], q: &[f64], q0: &[f64]) {
|
||||
if let Some(d) = &self.damping {
|
||||
let c = 1.0 / self.pseudo_time;
|
||||
for (((ri, di), a), b) in r.iter_mut().zip(d).zip(q).zip(q0) {
|
||||
*ri -= c * di * (a - b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the stabilization's D/Δt to the free tangent.
|
||||
fn damp_tangent(&self, kff: &mut Csr) -> std::result::Result<(), Failure> {
|
||||
if let Some(d) = &self.damping {
|
||||
let c = 1.0 / self.pseudo_time;
|
||||
for (i, di) in d.iter().enumerate() {
|
||||
add_entry(kff, i, i, c * di).map_err(Failure::Fatal)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scale(&self) -> f64 {
|
||||
self.force_scale.max(self.force_floor.unwrap_or(0.0)).max(f64::MIN_POSITIVE)
|
||||
}
|
||||
@@ -636,7 +683,6 @@ impl Run<'_> {
|
||||
&self.committed,
|
||||
&self.contact,
|
||||
&self.reference,
|
||||
self.band,
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -647,7 +693,7 @@ impl Run<'_> {
|
||||
if let Some(u) = &self.undeformed {
|
||||
return Ok(u.clone());
|
||||
}
|
||||
let mut e = self.problem.evaluate(&vec![0.0; self.problem.mesh.dofs()], 0.0, true, &self.committed, &[], &[], 0.0)?;
|
||||
let mut e = self.problem.evaluate(&vec![0.0; self.problem.mesh.dofs()], 0.0, true, &self.committed, &[], &[])?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let [kff] = self.problem.red.reduce([&k])?;
|
||||
let u = factor_raw(&kff, self.limits).map(|_| ());
|
||||
@@ -696,7 +742,7 @@ impl Run<'_> {
|
||||
/// factored (the increment then starts from `q0`).
|
||||
fn tangent_predictor(&mut self, q0: &[f64], from: f64, to: f64) -> std::result::Result<Option<(Vec<f64>, f64)>, Failure> {
|
||||
let red = self.problem.red;
|
||||
let mut e = self.problem.evaluate(&red.expand_scaled(q0, from), to, true, &self.committed, &self.contact, &self.reference, self.band)?;
|
||||
let mut e = self.problem.evaluate(&red.expand_scaled(q0, from), to, true, &self.committed, &self.contact, &self.reference)?;
|
||||
let k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let rf: Vec<f64> = free_residual(red, &e).iter().zip(prescribed_force(red, &k, to - from)).map(|(a, b)| a - b).collect();
|
||||
let mut dq = rf.clone();
|
||||
@@ -721,6 +767,12 @@ impl Run<'_> {
|
||||
) -> std::result::Result<Converged, Failure> {
|
||||
let red = self.problem.red;
|
||||
let tol = self.settings.tolerances;
|
||||
// A held active set is the increment's own (`Committed::active`).
|
||||
for c in &mut self.contact {
|
||||
c.active = None;
|
||||
}
|
||||
let mut held = false;
|
||||
let mut updates = 0usize;
|
||||
// Start from the extrapolated predictor when it is a valid state, else from q0.
|
||||
let start = predictor.map(|p| self.state(p, lambda, true).map(|e| (p.to_vec(), e)));
|
||||
let (mut q, mut eval) = match start {
|
||||
@@ -729,6 +781,7 @@ impl Run<'_> {
|
||||
Some(Err(Failure::Cutback(_))) | None => (q0.to_vec(), self.state(q0, lambda, true)?),
|
||||
};
|
||||
let mut r = self.residual(&eval);
|
||||
self.damp(&mut r, &q, q0);
|
||||
// With path-dependent materials, an increment that starts within the force tolerance (a
|
||||
// predictor that already solved it) has only round-off left for the energy criterion to
|
||||
// measure against: it is waived.
|
||||
@@ -743,9 +796,18 @@ impl Run<'_> {
|
||||
let mut sets = (None, active_sets(&eval));
|
||||
let mut flips = 0usize;
|
||||
let max_iterations = usize::try_from(self.settings.max_iterations).unwrap_or(usize::MAX);
|
||||
for _ in 0..max_iterations {
|
||||
// Holding an active set starts the increment's iterations again (once).
|
||||
let mut remaining = max_iterations;
|
||||
while remaining > 0 {
|
||||
remaining -= 1;
|
||||
// The round-off rule: nothing left to correct.
|
||||
if norm_inf(&r) <= ROUND_OFF * tol.force * self.scale() && self.force_floor.is_some() {
|
||||
if self.release(&mut eval, &q, lambda, &mut updates)? {
|
||||
r = self.residual(&eval);
|
||||
self.damp(&mut r, &q, q0);
|
||||
remaining = max_iterations;
|
||||
continue;
|
||||
}
|
||||
return Ok(Converged { q, evaluation: eval, energy: first_energy });
|
||||
}
|
||||
if self.iterations >= self.limits.max_newton_iterations {
|
||||
@@ -756,14 +818,17 @@ impl Run<'_> {
|
||||
}
|
||||
self.iterations += 1;
|
||||
let k = eval.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
|
||||
let [kff] = red.reduce([&k])?;
|
||||
let [mut kff] = red.reduce([&k])?;
|
||||
// The first tangent of the step: a singular one is a modelling error, not a cutback.
|
||||
let first = self.force_floor.is_none();
|
||||
if first {
|
||||
self.seed_force_floor(&kff);
|
||||
}
|
||||
self.damp_tangent(&mut kff)?;
|
||||
drop(k);
|
||||
let t0 = std::time::Instant::now();
|
||||
let factor = self.factor(&kff, first)?;
|
||||
TIMES[3].fetch_add(t0.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
|
||||
self.factor_entries = factor.entries();
|
||||
drop(kff);
|
||||
let mut dq = r.clone();
|
||||
@@ -772,24 +837,38 @@ impl Run<'_> {
|
||||
return Err(Failure::Cutback("the Newton correction is not finite".into()));
|
||||
}
|
||||
let g0 = dot(&dq, &r);
|
||||
let (s, next) = self.line_search(&q, &dq, lambda, g0)?;
|
||||
let (s, next) = self.line_search(&q, q0, &dq, lambda, g0)?;
|
||||
let negative = factor.inertia().negative;
|
||||
for (x, d) in q.iter_mut().zip(&dq) {
|
||||
*x += s * d;
|
||||
}
|
||||
eval = next;
|
||||
if !self.problem.contacts.is_empty() {
|
||||
let holding = self.contact.iter().any(|c| c.active.is_some());
|
||||
if !self.problem.contacts.is_empty() && !holding {
|
||||
let set = active_sets(&eval);
|
||||
if sets.0 == Some(set) && set != sets.1 {
|
||||
flips += 1;
|
||||
if flips >= contact::CHATTER_LIMIT {
|
||||
// From now on the nodes about to touch stay in the tangent.
|
||||
self.band = contact::ENGAGED_BAND;
|
||||
return Err(Failure::Cutback("the contact status alternates between two states (chattering)".into()));
|
||||
if held {
|
||||
return Err(Failure::Cutback("the contact status alternates between two states (chattering)".into()));
|
||||
}
|
||||
// Hold the current active set until the increment converges (then
|
||||
// check it, `release`): the nodes touching with no pressure stop
|
||||
// flipping, and the tangent stays the derivative of the forces.
|
||||
held = true;
|
||||
for (c, st) in self.contact.iter_mut().zip(&eval.contact) {
|
||||
c.active = Some(st.pressure.iter().map(|p| *p > 0.0).collect());
|
||||
}
|
||||
eval = self.state(&q, lambda, true)?;
|
||||
flips = 0;
|
||||
remaining = max_iterations;
|
||||
}
|
||||
}
|
||||
sets = (Some(sets.1), set);
|
||||
sets = (Some(sets.1), active_sets(&eval));
|
||||
}
|
||||
let r_old = std::mem::replace(&mut r, self.residual(&eval));
|
||||
let mut r_new = self.residual(&eval);
|
||||
self.damp(&mut r_new, &q, q0);
|
||||
let r_old = std::mem::replace(&mut r, r_new);
|
||||
let energy = (s * dot(&dq, &r_old)).abs();
|
||||
let first = *first_energy.get_or_insert(energy);
|
||||
let moved = q.iter().zip(q0).fold(0.0f64, |m, (a, b)| m.max((a - b).abs()));
|
||||
@@ -799,7 +878,7 @@ impl Run<'_> {
|
||||
displacement: s * norm_inf(&dq) / reference,
|
||||
energy: if first > 0.0 { energy / first } else { 0.0 },
|
||||
line_search: s,
|
||||
negative_pivots: factor.inertia().negative,
|
||||
negative_pivots: negative,
|
||||
augmentation: self.augmentation,
|
||||
};
|
||||
iterations.push(it);
|
||||
@@ -808,6 +887,13 @@ impl Run<'_> {
|
||||
}
|
||||
let energy_met = it.energy <= tol.energy || started_converged;
|
||||
if (it.force <= tol.force && it.displacement <= tol.displacement && energy_met) || it.force <= ROUND_OFF * tol.force {
|
||||
if self.release(&mut eval, &q, lambda, &mut updates)? {
|
||||
r = self.residual(&eval);
|
||||
self.damp(&mut r, &q, q0);
|
||||
history.clear();
|
||||
remaining = max_iterations;
|
||||
continue;
|
||||
}
|
||||
return Ok(Converged { q, evaluation: eval, energy: first_energy });
|
||||
}
|
||||
history.push(it.force);
|
||||
@@ -861,7 +947,8 @@ impl Run<'_> {
|
||||
self.augmentations += 1;
|
||||
self.augmentation = *count;
|
||||
for (l, s) in self.contact.iter_mut().zip(&c.evaluation.contact) {
|
||||
l.multipliers.clone_from(&s.pressure);
|
||||
// A held-closed node may have pulled (within the tolerance); multipliers press.
|
||||
l.multipliers = s.pressure.iter().map(|p| p.max(0.0)).collect();
|
||||
}
|
||||
// From the converged state, measured against the increment's start and its first
|
||||
// correction like the increment itself.
|
||||
@@ -872,7 +959,37 @@ impl Run<'_> {
|
||||
|
||||
/// The line search along `dq` from `q`: the accepted step and the state there (with the
|
||||
/// tangent).
|
||||
fn line_search(&mut self, q: &[f64], dq: &[f64], lambda: f64, g0: f64) -> std::result::Result<(f64, Evaluation), Failure> {
|
||||
/// At a converged state `eval` (at `q`, load factor `lambda`) with active sets held: when
|
||||
/// every one is consistent there ([`Interface::update_held`]) they are released and the
|
||||
/// state is the solution (false); otherwise the offending nodes switch, at most
|
||||
/// [`contact::MAX_ACTIVE_SET_UPDATES`] times, `eval` is evaluated again and the iterations
|
||||
/// go on (true).
|
||||
fn release(&mut self, eval: &mut Evaluation, q: &[f64], lambda: f64, updates: &mut usize) -> std::result::Result<bool, Failure> {
|
||||
if self.contact.iter().all(|c| c.active.is_none()) {
|
||||
return Ok(false);
|
||||
}
|
||||
let next: Vec<Option<Vec<bool>>> =
|
||||
self.problem.contacts.iter().zip(&self.contact).zip(&eval.contact).map(|((i, c), s)| i.update_held(c, s)).collect();
|
||||
if next.iter().all(Option::is_none) {
|
||||
for c in &mut self.contact {
|
||||
c.active = None;
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
*updates += 1;
|
||||
if *updates > contact::MAX_ACTIVE_SET_UPDATES {
|
||||
return Err(Failure::Cutback("the contact's active set does not settle".into()));
|
||||
}
|
||||
for (c, n) in self.contact.iter_mut().zip(next) {
|
||||
if n.is_some() {
|
||||
c.active = n;
|
||||
}
|
||||
}
|
||||
*eval = self.state(q, lambda, true)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn line_search(&mut self, q: &[f64], q0: &[f64], dq: &[f64], lambda: f64, g0: f64) -> std::result::Result<(f64, Evaluation), Failure> {
|
||||
let at = |s: f64| -> Vec<f64> { q.iter().zip(dq).map(|(a, b)| a + s * b).collect() };
|
||||
// The full step, with its tangent (taken in most iterations).
|
||||
let full = self.state(&at(1.0), lambda, true);
|
||||
@@ -880,11 +997,15 @@ impl Run<'_> {
|
||||
return full.map(|e| (1.0, e));
|
||||
}
|
||||
let red = self.problem.red;
|
||||
let work = |e: &Evaluation| dot(dq, &free_residual(red, e));
|
||||
let work = |e: &Evaluation, s: f64| {
|
||||
let mut r = free_residual(red, e);
|
||||
self.damp(&mut r, &at(s), q0);
|
||||
dot(dq, &r)
|
||||
};
|
||||
let mut s = 1.0;
|
||||
let mut g = match &full {
|
||||
Ok(e) => {
|
||||
let g1 = work(e);
|
||||
let g1 = work(e, 1.0);
|
||||
if g1.abs() <= LINE_SEARCH_ACCEPT * g0.abs() {
|
||||
return full.map(|e| (1.0, e));
|
||||
}
|
||||
@@ -906,7 +1027,7 @@ impl Run<'_> {
|
||||
}
|
||||
s = next;
|
||||
g = match self.state(&at(s), lambda, false) {
|
||||
Ok(e) => Some(work(&e)),
|
||||
Ok(e) => Some(work(&e, s)),
|
||||
Err(Failure::Cutback(_)) => None,
|
||||
Err(f) => return Err(f),
|
||||
};
|
||||
@@ -929,6 +1050,26 @@ impl Run<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per free equation, the volume its node carries (the lumped mass of a unit density): the
|
||||
/// stabilization's mass matrix.
|
||||
fn nodal_volumes(problem: &Problem) -> Result<Vec<f64>> {
|
||||
let mut v = vec![0.0; problem.mesh.dofs()];
|
||||
for (e, dofs) in problem.mesh.elements.iter().zip(&problem.element_dofs) {
|
||||
let lumped = e.kernel()?.lumped_mass(1.0).map_err(|err| Error::element(e.label, err))?;
|
||||
let per = dofs.len() / lumped.len().max(1);
|
||||
for (n, m) in lumped.iter().enumerate() {
|
||||
for d in dofs.iter().skip(n * per).take(per) {
|
||||
if let Some(x) = v.get_mut(*d) {
|
||||
*x += m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(problem.red.restrict(&v))
|
||||
}
|
||||
|
||||
pub static TIMES: [std::sync::atomic::AtomicU64; 4] = [const { std::sync::atomic::AtomicU64::new(0) }; 4];
|
||||
|
||||
/// Refuse what nonlinear steps cannot take yet, and warn about what they simplify.
|
||||
fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &NonlinearStatic, warnings: &mut Vec<String>) -> Result<()> {
|
||||
let at = |what: String| Error::NotSupported(format!("{what} in nonlinear steps (step `{}`)", step.name));
|
||||
@@ -1178,7 +1319,7 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// The undeformed state first: an element that is bad before anything moves is an error in
|
||||
// the model, not a reason to cut increments back.
|
||||
problem.evaluate(&vec![0.0; mesh.dofs()], 0.0, false, &committed, &[], &[], 0.0)?;
|
||||
problem.evaluate(&vec![0.0; mesh.dofs()], 0.0, false, &committed, &[], &[])?;
|
||||
let path_dependent = problem.materials.iter().any(MaterialModel::is_path_dependent);
|
||||
let mut run = Run {
|
||||
problem: &problem,
|
||||
@@ -1194,10 +1335,13 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
undeformed: None,
|
||||
contact: problem.contacts.iter().map(Interface::committed).collect(),
|
||||
reference: vec![0.0; mesh.dofs()],
|
||||
band: 0.0,
|
||||
augmentations: 0,
|
||||
augmentation: 0,
|
||||
converged_scale: 0.0,
|
||||
volumes: settings.stabilization.map(|_| nodal_volumes(&problem)).transpose()?,
|
||||
damping: None,
|
||||
pseudo_time: 1.0,
|
||||
dissipated: 0.0,
|
||||
};
|
||||
|
||||
let mut history = History::default();
|
||||
@@ -1228,6 +1372,9 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
let mut size = settings.initial_increment;
|
||||
let mut fast = 0usize;
|
||||
let mut last: Option<Evaluation> = None;
|
||||
// The work of the internal forces over the step (the energy the model stores, with
|
||||
// contact's), the stabilization's measure.
|
||||
let mut stored = 0.0f64;
|
||||
// The last converged increment's free motion and the change of the load factor over it.
|
||||
let mut previous: Option<(Vec<f64>, f64)> = None;
|
||||
let max_increments = usize::try_from(settings.max_increments).unwrap_or(usize::MAX);
|
||||
@@ -1262,6 +1409,7 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
}
|
||||
}
|
||||
let saved = run.contact.clone();
|
||||
run.pseudo_time = this;
|
||||
let mut augmentations = 0;
|
||||
run.augmentation = 0;
|
||||
let solved = run
|
||||
@@ -1270,8 +1418,36 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
if solved.is_err() {
|
||||
run.contact = saved;
|
||||
}
|
||||
if std::env::var("FC_TRACE").is_ok() {
|
||||
let f: Vec<String> = iterations.iter().map(|i| format!("{:.0e}{}", i.force, if i.line_search < 1.0 { format!("/s{:.2}", i.line_search) } else { String::new() })).collect();
|
||||
let closed: usize = match &solved { Ok(c) => c.evaluation.contact.iter().map(|s| s.pressure.iter().filter(|p| **p > 0.0).count()).sum(), Err(_) => 0 };
|
||||
eprintln!("TRACE times contact {} pattern {} elements {} factor {} ms", TIMES[0].load(std::sync::atomic::Ordering::Relaxed) / 1000, TIMES[1].load(std::sync::atomic::Ordering::Relaxed) / 1000, TIMES[2].load(std::sync::atomic::Ordering::Relaxed) / 1000, TIMES[3].load(std::sync::atomic::Ordering::Relaxed) / 1000);
|
||||
eprintln!("TRACE damp {:?} dissipated {:.3e} stored {stored:.3e}", run.damping.as_ref().map(|d| d.iter().fold(0.0f64, |m, v| m.max(*v))), run.dissipated);
|
||||
eprintln!("TRACE inc {number} lam {target_lambda:.5} size {:.2e} closed {closed} neg {:?} {} [{}] {}", change.abs(), iterations.last().map(|i| i.negative_pivots), if solved.is_ok() { "ok" } else { "CUT" }, f.join(" "), match &solved { Err(Failure::Cutback(r)) => r.clone(), _ => String::new() });
|
||||
}
|
||||
match solved {
|
||||
Ok(c) => {
|
||||
if let Some(d) = &run.damping {
|
||||
run.dissipated += c.q.iter().zip(&q).zip(d).map(|((a, b), di)| di * (a - b) * (a - b)).sum::<f64>() / this;
|
||||
}
|
||||
// The work of the internal forces over the increment (trapezoidal; zero forces
|
||||
// at the unloaded start).
|
||||
let (u0, u1) = (red.expand_scaled(&q, lambda), red.expand_scaled(&c.q, target_lambda));
|
||||
let zero = vec![0.0; u0.len()];
|
||||
let before = last.as_ref().map_or(zero.as_slice(), |e| e.internal.as_slice());
|
||||
let work = 0.5 * before.iter().zip(&c.evaluation.internal).zip(u0.iter().zip(&u1)).map(|((f0, f1), (a, b))| (f0 + f1) * (b - a)).sum::<f64>();
|
||||
stored += work;
|
||||
// The stabilization's damping, calibrated on the first increment (solved
|
||||
// without it): moving on at that increment's rate Δq/Δt, c V would dissipate
|
||||
// the fraction α of the work the step does at that rate, W₁/Δt² (the work
|
||||
// grows with the square of the motion), so c = α W₁ / (Δqᵀ V Δq).
|
||||
if let (Some(alpha), Some(v), None) = (settings.stabilization, &run.volumes, &run.damping) {
|
||||
let motion: f64 = c.q.iter().zip(&q).zip(v).map(|((a, b), vi)| vi * (a - b) * (a - b)).sum();
|
||||
if motion > 0.0 && work.abs() > 0.0 {
|
||||
let factor = alpha * work.abs() / motion;
|
||||
run.damping = Some(v.iter().map(|vi| factor * vi).collect());
|
||||
}
|
||||
}
|
||||
previous = Some((c.q.iter().zip(&q).map(|(a, b)| a - b).collect(), change));
|
||||
// The increment's own Newton iterations size the next one (augmentations
|
||||
// solve the same increment again).
|
||||
@@ -1327,13 +1503,23 @@ fn solve_on(model: &Model, step: &str, limits: &Limits, notes: Vec<String>) -> R
|
||||
}
|
||||
}
|
||||
}
|
||||
if run.damping.is_some() {
|
||||
history.stabilization = Some(Stabilization { dissipated: run.dissipated, stored });
|
||||
if run.dissipated > STABILIZATION_WARNING * stored.abs() {
|
||||
warnings.push(format!(
|
||||
"the stabilization dissipated {:.3e}, {:.1} % of the work of the internal forces ({stored:.3e}): lower it, or the result is not quasi-static",
|
||||
run.dissipated,
|
||||
100.0 * run.dissipated / stored.abs().max(f64::MIN_POSITIVE)
|
||||
));
|
||||
}
|
||||
}
|
||||
(q, lambda, last)
|
||||
};
|
||||
history.iterations = run.iterations;
|
||||
let u = red.expand_scaled(&q, lambda);
|
||||
let evaluation = match last {
|
||||
Some(e) => e,
|
||||
None => problem.evaluate(&u, lambda, false, &run.committed, &run.contact, &run.reference, 0.0)?,
|
||||
None => problem.evaluate(&u, lambda, false, &run.committed, &run.contact, &run.reference)?,
|
||||
};
|
||||
if let Some((at, negative)) = history
|
||||
.increments
|
||||
|
||||
@@ -86,6 +86,182 @@ pub fn nested_dissection(xyz: &[[f64; 3]], adjacency: &[Vec<usize>]) -> Vec<usiz
|
||||
order
|
||||
}
|
||||
|
||||
/// A permutation of the nodes by nested dissection on the graph alone: each part's separator is
|
||||
/// a middle level of the breadth-first level structure rooted at a pseudo-peripheral node, so
|
||||
/// separators follow the mesh rather than a coordinate plane (A. George, J. W. H. Liu, *An
|
||||
/// automatic nested dissection algorithm for irregular finite element problems*, SIAM J. Numer.
|
||||
/// Anal. 15(5):1053–1069, 1978; the pseudo-peripheral node of N. E. Gibbs, W. G. Poole,
|
||||
/// P. K. Stockmeyer, SIAM J. Numer. Anal. 13(2):236–250, 1976). A thin curved wall (a shell, a
|
||||
/// bellows) is cut across its surface instead of many times by a plane. Disconnected parts are
|
||||
/// ordered one after the other; ties go by node index, so the ordering is deterministic.
|
||||
pub fn graph_dissection(adjacency: &[Vec<usize>]) -> Vec<usize> {
|
||||
let n = adjacency.len();
|
||||
let mut order = Vec::with_capacity(n);
|
||||
// The part each node belongs to (a token per split), and its level in the current search.
|
||||
let mut owner = vec![0usize; n];
|
||||
let mut level = vec![usize::MAX; n];
|
||||
let mut token = 1usize;
|
||||
enum Task {
|
||||
Split(Vec<usize>),
|
||||
Emit(Vec<usize>),
|
||||
}
|
||||
let mut stack = vec![Task::Split((0..n).collect())];
|
||||
// The level structure of the nodes of part `token` reached from `root`.
|
||||
let levels = |root: usize, token: usize, owner: &[usize], level: &mut Vec<usize>| -> Vec<Vec<usize>> {
|
||||
let mut out: Vec<Vec<usize>> = vec![vec![root]];
|
||||
if let Some(l) = level.get_mut(root) {
|
||||
*l = 0;
|
||||
}
|
||||
loop {
|
||||
let depth = out.len();
|
||||
let mut next = Vec::new();
|
||||
for &v in out.last().into_iter().flatten() {
|
||||
for &w in adjacency.get(v).into_iter().flatten() {
|
||||
if owner.get(w) == Some(&token) && level.get(w) == Some(&usize::MAX) {
|
||||
if let Some(l) = level.get_mut(w) {
|
||||
*l = depth;
|
||||
}
|
||||
next.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
if next.is_empty() {
|
||||
break;
|
||||
}
|
||||
out.push(next);
|
||||
}
|
||||
out
|
||||
};
|
||||
let degree = |v: usize, token: usize, owner: &[usize]| adjacency.get(v).map_or(0, |row| row.iter().filter(|w| owner.get(**w) == Some(&token)).count());
|
||||
while let Some(task) = stack.pop() {
|
||||
let part = match task {
|
||||
Task::Emit(sep) => {
|
||||
order.extend(sep);
|
||||
continue;
|
||||
}
|
||||
Task::Split(p) => p,
|
||||
};
|
||||
if part.len() <= LEAF {
|
||||
order.extend(part);
|
||||
continue;
|
||||
}
|
||||
token += 1;
|
||||
for &v in &part {
|
||||
if let Some(o) = owner.get_mut(v) {
|
||||
*o = token;
|
||||
}
|
||||
}
|
||||
let reset = |level: &mut Vec<usize>, nodes: &[usize]| {
|
||||
for &v in nodes {
|
||||
if let Some(l) = level.get_mut(v) {
|
||||
*l = usize::MAX;
|
||||
}
|
||||
}
|
||||
};
|
||||
// A pseudo-peripheral root: from the first node, the end of the deepest level structure
|
||||
// with the fewest neighbours, while the depth grows.
|
||||
let mut root = part.first().copied().unwrap_or(0);
|
||||
let mut structure = levels(root, token, &owner, &mut level);
|
||||
for _ in 0..PERIPHERAL_SEARCHES {
|
||||
let reached: Vec<usize> = structure.iter().flatten().copied().collect();
|
||||
reset(&mut level, &reached);
|
||||
let Some(candidate) = structure.last().and_then(|last| last.iter().copied().min_by_key(|v| (degree(*v, token, &owner), *v))) else { break };
|
||||
let deeper = levels(candidate, token, &owner, &mut level);
|
||||
if deeper.len() <= structure.len() {
|
||||
let reached: Vec<usize> = deeper.iter().flatten().copied().collect();
|
||||
reset(&mut level, &reached);
|
||||
structure = levels(root, token, &owner, &mut level);
|
||||
break;
|
||||
}
|
||||
(root, structure) = (candidate, deeper);
|
||||
}
|
||||
let reached: Vec<usize> = structure.iter().flatten().copied().collect();
|
||||
reset(&mut level, &reached);
|
||||
if reached.len() < part.len() {
|
||||
// Disconnected: this component, then the rest.
|
||||
let mut inside = vec![false; 0];
|
||||
inside.resize(n, false);
|
||||
for &v in &reached {
|
||||
if let Some(x) = inside.get_mut(v) {
|
||||
*x = true;
|
||||
}
|
||||
}
|
||||
let rest: Vec<usize> = part.iter().copied().filter(|v| !inside.get(*v).copied().unwrap_or(false)).collect();
|
||||
let mut component = reached;
|
||||
component.sort_unstable();
|
||||
stack.push(Task::Split(rest));
|
||||
stack.push(Task::Split(component));
|
||||
continue;
|
||||
}
|
||||
if structure.len() < 3 {
|
||||
order.extend(part);
|
||||
continue;
|
||||
}
|
||||
// The middle level: the first whose levels up to it hold half the part.
|
||||
let mut count = 0;
|
||||
let mut middle = 1;
|
||||
for (i, l) in structure.iter().enumerate().skip(1) {
|
||||
count += l.len();
|
||||
if 2 * count >= part.len() {
|
||||
middle = i.min(structure.len() - 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Its nodes that touch the next level separate the levels before it from those after.
|
||||
for (i, l) in structure.iter().enumerate() {
|
||||
for &v in l {
|
||||
if let Some(x) = level.get_mut(v) {
|
||||
*x = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
let touches_next = |v: usize, level: &[usize]| adjacency.get(v).is_some_and(|row| row.iter().any(|w| owner.get(*w) == Some(&token) && level.get(*w) == Some(&(middle + 1))));
|
||||
let mut separator = Vec::new();
|
||||
let mut before: Vec<usize> = structure.iter().take(middle).flatten().copied().collect();
|
||||
for &v in structure.get(middle).into_iter().flatten() {
|
||||
if touches_next(v, &level) { separator.push(v) } else { before.push(v) }
|
||||
}
|
||||
let after: Vec<usize> = structure.iter().skip(middle + 1).flatten().copied().collect();
|
||||
reset(&mut level, &part);
|
||||
before.sort_unstable();
|
||||
separator.sort_unstable();
|
||||
let mut after = after;
|
||||
after.sort_unstable();
|
||||
stack.push(Task::Emit(separator));
|
||||
stack.push(Task::Split(after));
|
||||
stack.push(Task::Split(before));
|
||||
}
|
||||
order
|
||||
}
|
||||
|
||||
/// Searches for a pseudo-peripheral node.
|
||||
const PERIPHERAL_SEARCHES: usize = 8;
|
||||
|
||||
/// The entries of the Cholesky factor of the node graph `adjacency` eliminated in `order` (one
|
||||
/// per node pair): the fill an ordering gives.
|
||||
pub fn fill(adjacency: &[Vec<usize>], order: &[usize]) -> usize {
|
||||
let n = adjacency.len();
|
||||
let mut position = vec![0usize; n];
|
||||
for (k, &v) in order.iter().enumerate() {
|
||||
if let Some(p) = position.get_mut(v) {
|
||||
*p = k;
|
||||
}
|
||||
}
|
||||
let mut ptr = vec![0usize];
|
||||
let mut idx = Vec::new();
|
||||
for &v in order {
|
||||
let k = position.get(v).copied().unwrap_or(0);
|
||||
let mut rows: Vec<usize> = adjacency.get(v).into_iter().flatten().filter_map(|w| position.get(*w).copied()).filter(|r| *r <= k).collect();
|
||||
rows.push(k);
|
||||
rows.sort_unstable();
|
||||
rows.dedup();
|
||||
idx.extend(rows);
|
||||
ptr.push(idx.len());
|
||||
}
|
||||
let val = vec![1.0; idx.len()];
|
||||
crate::cholesky::factor_entries(&crate::cholesky::UpperColumns { n, ptr, idx, val })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -686,14 +686,19 @@ fn a_rubber_bellows_folds_onto_itself() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let settings = NonlinearStatic { initial_increment: 0.05, max_increment: 0.1, max_increments: 400, ..NonlinearStatic::default() };
|
||||
let stabilization = std::env::var("STAB").ok().and_then(|v| v.parse().ok());
|
||||
let settings = NonlinearStatic { initial_increment: 0.05, max_increment: 0.1, max_increments: 1000, line_search: stabilization.is_some(), stabilization, ..NonlinearStatic::default() };
|
||||
b.model.add_step(Step::new("Step", Procedure::NonlinearStatic(settings)), None).unwrap();
|
||||
b.model.add_contact("folds", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
|
||||
b.bc("x0", [Some(0.0), None, None], |x| near(x[0], 0.0));
|
||||
b.bc("y0", [None, Some(0.0), None], |x| near(x[1], 0.0));
|
||||
b.bc("base", [None, None, Some(0.0)], |x| near(x[2], 0.0));
|
||||
b.bc("squeeze", [None, None, Some(-0.8)], move |x| near(x[2], height));
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap();
|
||||
let squeeze: f64 = std::env::var("SQUEEZE").ok().and_then(|v| v.parse().ok()).unwrap_or(0.8);
|
||||
b.bc("squeeze", [None, None, Some(-squeeze)], move |x| near(x[2], height));
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = solve_nonlinear(&b.model, Some("Step"), &Limits::default());
|
||||
eprintln!("bellows squeeze {squeeze}: {:?} {}", t0.elapsed(), r.as_ref().err().map(ToString::to_string).unwrap_or_default());
|
||||
let r = r.unwrap();
|
||||
report(&r);
|
||||
let c = &r.contacts[0];
|
||||
eprintln!("closed {} penetration {:.2e} augmentations {}", c.closed(), c.penetration, c.augmentations);
|
||||
@@ -806,3 +811,11 @@ fn what_2d_contact_refuses() {
|
||||
let e = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap_err().to_string();
|
||||
assert!(e.contains("arc-length"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "scratch"]
|
||||
fn scratch_quadratic_2d() {
|
||||
let (_, r2) = planar_c_ring(0.15, 1);
|
||||
report(&r2);
|
||||
quadratic(&r2);
|
||||
}
|
||||
Reference in new issue
Block a user