solver: Newton on indefinite tangents by LDLᵀ

A tangent that is not positive definite is factored by LDLᵀ (Cholesky first, one symbolic
factorization) instead of cutting the increment back; each iteration records the tangent's
negative eigenvalues and the summary warns when a converged state keeps some. A model that can
move freely is still an error at its first tangent, located by the undeformed Cholesky factor;
a singular tangent later is a cutback. Nearly incompressible hybrid hex8, hex20 and tet10 at
ν = 0.4999 under prescribed displacement now converge to the closed form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 12:42:12 -04:00
1 parent 908dae180b
commit c09ccc8a59
4 files changed
+127 -26

No files matched your search

+7
View File
@@ -14,6 +14,7 @@ use fieldcraft_parallel as parallel;
use crate::cholesky::{Factor, FactorError, UpperColumns}; use crate::cholesky::{Factor, FactorError, UpperColumns};
use crate::constraints::{Budget, Reduction, coupling_rows}; use crate::constraints::{Budget, Reduction, coupling_rows};
use crate::eigen::{Operator, Wanted, lanczos}; use crate::eigen::{Operator, Wanted, lanczos};
use crate::ldlt::{Kind, Symmetric};
use crate::ordering::nested_dissection; use crate::ordering::nested_dissection;
use crate::sparse::{Csr, NodeGraph, add_block, add_diagonal, add_scalar}; use crate::sparse::{Csr, NodeGraph, add_block, add_diagonal, add_scalar};
use crate::system::{ use crate::system::{
@@ -250,6 +251,12 @@ pub(crate) fn factor_raw(a: &Csr, limits: &Limits) -> std::result::Result<Factor
Factor::new(&UpperColumns::from_symmetric(a), limits.max_factor_entries) Factor::new(&UpperColumns::from_symmetric(a), limits.max_factor_entries)
} }
/// Factor a free-dof matrix by Cholesky, LDLᵀ, or Cholesky with LDLᵀ when it is not positive
/// definite ([`Kind`]).
pub(crate) fn factor_symmetric(a: &Csr, limits: &Limits, kind: Kind) -> std::result::Result<Symmetric, FactorError> {
Symmetric::new(&UpperColumns::from_symmetric(a), limits.max_factor_entries, kind)
}
/// A factorization failure in the user's terms: the node (or reference point) and direction of /// A factorization failure in the user's terms: the node (or reference point) and direction of
/// a singular pivot. /// a singular pivot.
pub(crate) fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) -> Error { pub(crate) fn factor_error(e: FactorError, mesh: &Mesh, reduction: &Reduction) -> Error {
+79 -25
View File
@@ -36,12 +36,21 @@
//! increments in a row that converge in at most four iterations let the next grow by half (up //! increments in a row that converge in at most four iterations let the next grow by half (up
//! to `max_increment`), one that needs more than eight shrinks it by a quarter. An increment is //! to `max_increment`), one that needs more than eight shrinks it by a quarter. An increment is
//! cut back to a quarter of its size when it fails: an element inverts, the residual is not //! cut back to a quarter of its size when it fails: an element inverts, the residual is not
//! finite, the residual grows in two successive iterations, the tangent is not positive definite //! finite, the residual grows in two successive iterations, the tangent is singular, or it does
//! (a limit point or an unstable material), or it does not converge in `max_iterations`. The //! not converge in `max_iterations`. The
//! step fails cleanly, naming the load factor and the last reason, when an increment falls below //! step fails cleanly, naming the load factor and the last reason, when an increment falls below
//! `min_increment`, when it has taken `max_increments` increments, or when the Newton iterations //! `min_increment`, when it has taken `max_increments` increments, or when the Newton iterations
//! of the whole step reach [`crate::Limits::max_newton_iterations`]. //! of the whole step reach [`crate::Limits::max_newton_iterations`].
//! //!
//! **Indefinite tangents.** A tangent that is not positive definite — a strained mixed element
//! of a nearly incompressible material, an unstable material, a state past a limit or
//! bifurcation point — is factored by the symmetric indefinite LDLᵀ ([`crate::ldlt`]) and Newton
//! goes on: the residual decides convergence, and every iteration records the tangent's negative
//! eigenvalues (its inertia), which the step summary reports when a converged state keeps some
//! (an unstable equilibrium reached under load control). Only the first tangent of a model that
//! can move without straining is an error (the undeformed model's Cholesky factor names where);
//! a singular tangent later is a cutback.
//!
//! **Follower pressure** and the Cholesky factorization: the load stiffness of a pressure on the //! **Follower pressure** and the Cholesky factorization: the load stiffness of a pressure on the
//! deformed surface is not symmetric (Schweizerhof & Ramm 1984); its skew part is left out of the //! deformed surface is not symmetric (Schweizerhof & Ramm 1984); its skew part is left out of the
//! tangent (the symmetric part is kept), so the factorization stays the supernodal Cholesky of the //! tangent (the symmetric part is kept), so the factorization stays the supernodal Cholesky of the
@@ -93,12 +102,14 @@ use fieldcraft_model::{BoundaryCondition, Extent, Hardening, Load, Model, Nonlin
use fieldcraft_parallel as parallel; use fieldcraft_parallel as parallel;
use serde::Serialize; use serde::Serialize;
use crate::analysis::factor_symmetric;
use crate::analysis::{ use crate::analysis::{
ElementStress, GRAIN, PointResult, ROUND, Setup, StaticResult, Stats, budget, factor_error, factor_raw, ignored_dashpots, pattern, reduction, ElementStress, GRAIN, PointResult, ROUND, Setup, StaticResult, Stats, budget, factor_error, factor_raw, ignored_dashpots, pattern, reduction,
split, split,
}; };
use crate::cholesky::FactorError; use crate::cholesky::FactorError;
use crate::constraints::Reduction; use crate::constraints::Reduction;
use crate::ldlt::{Kind, Symmetric};
use crate::sparse::{Csr, add_block}; use crate::sparse::{Csr, add_block};
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translations}; use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translations};
use crate::{Error, Limits, Result}; use crate::{Error, Limits, Result};
@@ -114,6 +125,14 @@ pub struct Iteration {
pub energy: f64, pub energy: f64,
/// The line search's step along the correction (1: the full Newton step). /// The line search's step along the correction (1: the full Newton step).
pub line_search: f64, pub line_search: f64,
/// Negative eigenvalues of the tangent the correction was solved with (an indefinite
/// tangent is factored by LDLᵀ, [`crate::ldlt`]).
#[serde(skip_serializing_if = "is_zero")]
pub negative_pivots: usize,
}
fn is_zero(n: &usize) -> bool {
*n == 0
} }
/// One increment (converged, or cut back). /// One increment (converged, or cut back).
@@ -435,8 +454,8 @@ struct Run<'a> {
committed: Vec<Vec<f64>>, committed: Vec<Vec<f64>>,
/// Whether a material keeps history (the tangent predictor and its convergence rules). /// Whether a material keeps history (the tangent predictor and its convergence rules).
path_dependent: bool, path_dependent: bool,
/// Whether the undeformed model's tangent factors, once asked. /// How the undeformed model's tangent factors (Cholesky), once asked.
undeformed_stable: Option<bool>, undeformed: Option<std::result::Result<(), FactorError>>,
} }
/// The outcome of a converged increment. /// The outcome of a converged increment.
@@ -460,19 +479,50 @@ impl Run<'_> {
Ok(self.problem.evaluate(&self.problem.red.expand_scaled(q, lambda), lambda, tangent, &self.committed)?) Ok(self.problem.evaluate(&self.problem.red.expand_scaled(q, lambda), lambda, tangent, &self.committed)?)
} }
/// Whether the tangent of the undeformed, unloaded model is positive definite: a first /// How the tangent of the undeformed, unloaded model factors (Cholesky): a first tangent
/// tangent that is not, at a state the first increment has already strained, is then the /// that is not positive definite, at a state the first increment has already strained, is
/// material or the geometry (a cutback), not a model that can move freely (an error). /// then the material or the geometry, not a model that can move freely (this error).
fn undeformed_is_stable(&mut self) -> std::result::Result<bool, Failure> { fn undeformed(&mut self) -> std::result::Result<std::result::Result<(), FactorError>, Failure> {
if let Some(stable) = self.undeformed_stable { if let Some(u) = &self.undeformed {
return Ok(stable); return Ok(u.clone());
} }
let mut e = self.problem.evaluate(&vec![0.0; self.problem.mesh.dofs()], 0.0, true, &self.committed)?; 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 k = e.tangent.take().ok_or_else(|| Failure::Fatal(Error::Internal("tangent".into())))?;
let [kff] = self.problem.red.reduce([&k])?; let [kff] = self.problem.red.reduce([&k])?;
let stable = factor_raw(&kff, self.limits).is_ok(); let u = factor_raw(&kff, self.limits).map(|_| ());
self.undeformed_stable = Some(stable); self.undeformed = Some(u.clone());
Ok(stable) Ok(u)
}
/// Factor a tangent: Cholesky, or LDLᵀ when it is not positive definite (an indefinite
/// tangent: a strained mixed element, an unstable material, a state past a limit or
/// bifurcation point; Newton proceeds on it and the result counts its negative pivots). A
/// model that can move freely is an error at its first tangent; a singular tangent later
/// is a cutback.
fn factor(&mut self, kff: &Csr, first: bool) -> std::result::Result<Symmetric, Failure> {
let free_motion = |run: &mut Self| -> std::result::Result<Option<Error>, Failure> {
if !first {
return Ok(None);
}
Ok(match run.undeformed()? {
Err(e @ (FactorError::NotPositiveDefinite { .. } | FactorError::Singular { .. })) => {
Some(factor_error(e, run.problem.mesh, run.problem.red))
}
_ => None,
})
};
match factor_symmetric(kff, self.limits, Kind::Auto) {
Ok(f @ Symmetric::Cholesky(_)) => Ok(f),
Ok(f) => match free_motion(self)? {
Some(e) => Err(Failure::Fatal(e)),
None => Ok(f),
},
Err(FactorError::NotPositiveDefinite { .. } | FactorError::Singular { .. }) => match free_motion(self)? {
Some(e) => Err(Failure::Fatal(e)),
None => Err(Failure::Cutback("the tangent stiffness is singular (a limit point or bifurcation, or an unstable material)".into())),
},
Err(e) => Err(Failure::Fatal(factor_error(e, self.problem.mesh, self.problem.red))),
}
} }
/// The tangent predictor of an increment from the converged `q0` at load factor `from` to /// The tangent predictor of an increment from the converged `q0` at load factor `from` to
@@ -553,19 +603,11 @@ impl Run<'_> {
self.force_floor = Some(1e-8 * mean * self.length); self.force_floor = Some(1e-8 * mean * self.length);
} }
drop(k); drop(k);
let chol = match factor_raw(&kff, self.limits) { let factor = self.factor(&kff, first)?;
Ok(c) => c, self.factor_entries = factor.entries();
Err(FactorError::NotPositiveDefinite { .. }) if !first || self.undeformed_is_stable()? => {
return Err(Failure::Cutback(
"the tangent stiffness is not positive definite (a limit point, a bifurcation or an unstable material)".into(),
));
}
Err(e) => return Err(Failure::Fatal(factor_error(e, self.problem.mesh, red))),
};
self.factor_entries = chol.entries();
drop(kff); drop(kff);
let mut dq = r.clone(); let mut dq = r.clone();
chol.solve(&mut dq)?; factor.solve(&mut dq)?;
if dq.iter().any(|v| !v.is_finite()) { if dq.iter().any(|v| !v.is_finite()) {
return Err(Failure::Cutback("the Newton correction is not finite".into())); return Err(Failure::Cutback("the Newton correction is not finite".into()));
} }
@@ -585,6 +627,7 @@ impl Run<'_> {
displacement: s * norm_inf(&dq) / reference, displacement: s * norm_inf(&dq) / reference,
energy: if first > 0.0 { energy / first } else { 0.0 }, energy: if first > 0.0 { energy / first } else { 0.0 },
line_search: s, line_search: s,
negative_pivots: factor.inertia().negative,
}; };
iterations.push(it); iterations.push(it);
if !it.force.is_finite() { if !it.force.is_finite() {
@@ -882,7 +925,7 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
factor_entries: 0, factor_entries: 0,
committed, committed,
path_dependent, path_dependent,
undeformed_stable: None, undeformed: None,
}; };
let mut history = History::default(); let mut history = History::default();
@@ -985,6 +1028,17 @@ pub fn solve_nonlinear(model: &Model, step: Option<&str>, limits: &Limits) -> Re
Some(e) => e, Some(e) => e,
None => problem.evaluate(&u, lambda, false, &run.committed)?, None => problem.evaluate(&u, lambda, false, &run.committed)?,
}; };
if let Some((at, negative)) = history
.increments
.iter()
.filter(|i| i.converged)
.filter_map(|i| i.iterations.last().map(|it| (i.load_factor, it.negative_pivots)))
.find(|(_, n)| *n > 0)
{
warnings.push(format!(
"the converged tangent had {negative} negative eigenvalue(s) from load factor {at:.6} on: an unstable equilibrium under load control (past a limit or bifurcation point, or an unstable material)"
));
}
if let Some(a) = problem.follower_asymmetry(&u, lambda)? if let Some(a) = problem.follower_asymmetry(&u, lambda)?
&& a > 1e-6 && a > 1e-6
{ {
+31
View File
@@ -514,3 +514,34 @@ fn mooney_rivlin_and_yeoh_in_the_homogeneous_modes() {
} }
} }
} }
/// Nearly incompressible hybrid blocks (ν = 0.4999) stretched to λ = 2 by a prescribed
/// displacement: the first states of an increment strain the boundary elements alone, so their
/// mixed tangents are indefinite; Newton goes on with the LDLᵀ factor and converges to the
/// compressible neo-Hookean closed form.
#[test]
fn nearly_incompressible_hybrids_under_prescribed_displacement() {
let (e, nu, lam) = (3.0, 0.4999, 2.0);
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
let (l, sigma) = neo_hookean_uniaxial(mu, kappa, lam);
let mut indefinite = 0;
for (kind, n) in [(ElementKind::Hex8, 3), (ElementKind::Hex20, 2), (ElementKind::Tet10, 2)] {
let mut g = grid([n, n, n], kind, |x| x);
g.define(iso(e, nu), None, Formulation { hybrid: true, ..Formulation::default() }, nonlinear());
neo_hookean(&mut g, e, nu);
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
g.node_set("x1", |x| near(x[0], 1.0));
g.bc("pull", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(lam - 1.0), None, None, None, None, None] });
let r = solve(&g);
let corner = g.nodes_where(|x| near(x[0], 1.0) && near(x[1], 1.0) && near(x[2], 1.0))[0];
assert!((r.displacement[at(&r, corner)][1] - (l - 1.0)).abs() < 1e-7, "{kind:?}: {:?}", r.displacement[at(&r, corner)]);
for s in &r.stress {
assert!((s[0] - sigma).abs() < 1e-5 * sigma, "{kind:?}: {s:?} vs {sigma}");
}
let h = r.nonlinear.as_ref().unwrap();
indefinite += h.increments.iter().flat_map(|i| &i.iterations).filter(|i| i.negative_pivots > 0).count();
}
assert!(indefinite > 0, "the LDLᵀ path was exercised");
}
+10 -1
View File
@@ -274,9 +274,18 @@ first iteration's) are all below their tolerances, or the residual is round-off.
starts from the linear extrapolation of the last one. Automatic incrementation grows an starts from the linear extrapolation of the last one. Automatic incrementation grows an
increment by half after two fast ones (≤ 4 iterations), shrinks it after a slow one (> 8), and increment by half after two fast ones (≤ 4 iterations), shrinks it after a slow one (> 8), and
cuts it to a quarter when an element inverts, the residual is not finite or grows twice, the cuts it to a quarter when an element inverts, the residual is not finite or grows twice, the
tangent is not positive definite, or the iterations run out; the step stops with the load factor tangent is singular, or the iterations run out; the step stops with the load factor
and the reason at the minimum increment, the increment cap, or `Limits::max_newton_iterations`. and the reason at the minimum increment, the increment cap, or `Limits::max_newton_iterations`.
**Indefinite tangents.** A tangent that is not positive definite (a strained nearly
incompressible mixed element, an unstable material, a state past a limit or bifurcation point)
is factored by LDLᵀ and Newton goes on; each iteration records the tangent's negative
eigenvalues, and the summary warns when a converged state keeps some (an unstable equilibrium
under load control). This lifts the old limit on hybrid elements near ν = ½ under prescribed
displacement (at ν = 0.4999 every increment used to be cut back to the cap). Only the first
tangent of a model that can move freely is an error. The tangent predictor of path-dependent
materials keeps Cholesky and is skipped when the tangent is indefinite.
**Results**: the final displacements and reactions, the **Cauchy stress** and the **Results**: the final displacements and reactions, the **Cauchy stress** and the
**logarithmic strain** (ln V, engineering shears; the small strain without nonlinear geometry) at **logarithmic strain** (ln V, engineering shears; the small strain without nonlinear geometry) at
the integration points and averaged at the nodes, the **equivalent plastic strain** at the the integration points and averaged at the nodes, the **equivalent plastic strain** at the