forked from 1b-agent/fieldcraft-upstream
Temperatures now strain the material in nonlinear steps instead of being refused. At finite strain the deformation gradient splits multiplicatively, F = F_m F_theta with F_theta = diag(1 + alpha_i dT) at each point: the material sees F_m and the loops run on the thermally expanded volume J_theta dV0 (mixed elements project det F_m), so the tangent stays consistent. At small strain the split is additive, and elements on their linear stiffness take the equivalent nodal forces as a dead load. The step's temperatures are its temperature loads, or the steady conduction of its temperature conditions and heat loads (one-way coupling, as the linear coupled step); both at once are refused. They ramp with the load factor from each material's reference temperature. Johnson-Cook plasticity softens at the point's temperature. Verified: free expansion and a fully constrained neo-Hookean block at 20 % thermal stretch, full and hybrid (exact); a bar held at its ends heated past yield (exact); the linear limit and the conducted temperatures against the linear solvers; tangents by central differences with a temperature field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
517 lines
28 KiB
Rust
517 lines
28 KiB
Rust
//! Nonlinear static analysis against closed forms: uniaxial stretch of neo-Hookean and
|
||
//! St Venant–Kirchhoff blocks (3D, plane strain, axisymmetric), the nonlinear patch test, a
|
||
//! large rigid rotation, Newton's quadratic convergence, the linear limit, and what nonlinear
|
||
//! steps refuse or stop at. Run with `--nocapture` to see the residuals.
|
||
|
||
mod common;
|
||
|
||
use common::*;
|
||
use fieldcraft_model::*;
|
||
use fieldcraft_solver::{Limits, StaticResult, solve_nonlinear, solve_static};
|
||
|
||
fn nonlinear() -> Procedure {
|
||
Procedure::NonlinearStatic(NonlinearStatic::default())
|
||
}
|
||
|
||
fn neo_hookean(g: &mut Grid, e: f64, nu: f64) {
|
||
let m = Material { elastic: Some(iso(e, nu)), hyperelastic: Some(Hyperelastic::NeoHookean {}), ..Material::default() };
|
||
g.model.replace_material("Mat", m).unwrap();
|
||
}
|
||
|
||
/// Prescribe every displacement component of each node where `f` holds from its position:
|
||
/// one set and one boundary condition per node.
|
||
fn prescribe(g: &mut Grid, f: impl Fn([f64; 3]) -> bool, u: impl Fn([f64; 3]) -> [f64; 3]) {
|
||
for l in g.nodes_where(f) {
|
||
let x = g.model.node(l).unwrap().xyz;
|
||
let v = u(x);
|
||
let name = format!("n{l}");
|
||
g.model.add_set(&name, Set { nodes: vec![l], ..Set::default() }).unwrap();
|
||
let values = [Some(v[0]), Some(v[1]), Some(v[2]), None, None, None];
|
||
g.bc(&name, BoundaryCondition::Displacement { target: Target::Set(name.clone()), values });
|
||
}
|
||
}
|
||
|
||
fn at(r: &StaticResult, label: u32) -> usize {
|
||
r.nodes.binary_search(&label).unwrap()
|
||
}
|
||
|
||
fn solve(g: &Grid) -> StaticResult {
|
||
solve_nonlinear(&g.model, Some("Step"), &Limits::default()).unwrap()
|
||
}
|
||
|
||
/// The lateral stretch of a neo-Hookean bar stretched by λ in uniaxial stress (σ_yy = σ_zz = 0)
|
||
/// and its axial Cauchy stress, by bisection on σ_yy(l) = μ J^(−5/3) (l² − I₁/3) + κ (J − 1).
|
||
fn neo_hookean_uniaxial(mu: f64, kappa: f64, lam: f64) -> (f64, f64) {
|
||
let stress = |l: f64| {
|
||
let (j, i1) = (lam * l * l, lam * lam + 2.0 * l * l);
|
||
let c = mu * j.powf(-5.0 / 3.0);
|
||
(c * (lam * lam - i1 / 3.0) + kappa * (j - 1.0), c * (l * l - i1 / 3.0) + kappa * (j - 1.0))
|
||
};
|
||
let (mut lo, mut hi) = (0.1, 1.5);
|
||
for _ in 0..200 {
|
||
let mid = 0.5 * (lo + hi);
|
||
if stress(mid).1 > 0.0 { hi = mid } else { lo = mid }
|
||
}
|
||
let l = 0.5 * (lo + hi);
|
||
(l, stress(l).0)
|
||
}
|
||
|
||
/// A unit block stretched to twice its length (x), free laterally (symmetry planes), in each
|
||
/// element kind: the homogeneous closed form at every node, and the logarithmic strain ln 2.
|
||
#[test]
|
||
fn neo_hookean_block_in_uniaxial_stretch() {
|
||
let (e, nu, lam) = (10.0, 0.3, 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);
|
||
for (kind, formulation, name) in variants().into_iter().filter(|v| v.1 != incompatible()) {
|
||
let mut g = grid([2, 2, 2], kind, |x| x);
|
||
g.define(iso(e, nu), None, formulation, 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);
|
||
for (i, label) in r.nodes.iter().enumerate() {
|
||
let x = g.model.node(*label).unwrap().xyz;
|
||
let want = [(lam - 1.0) * x[0], (l - 1.0) * x[1], (l - 1.0) * x[2]];
|
||
for k in 0..3 {
|
||
assert!((r.displacement[i][k] - want[k]).abs() < 1e-8, "{name} node {label}: {:?} vs {want:?}", r.displacement[i]);
|
||
}
|
||
}
|
||
for s in &r.stress {
|
||
assert!((s[0] - sigma).abs() < 1e-8 * sigma && s[1].abs() < 1e-8 * sigma && s[3].abs() < 1e-8 * sigma, "{name}: {s:?} vs {sigma}");
|
||
}
|
||
let strain = r.elements[0].points[0].strain;
|
||
assert!((strain[0] - lam.ln()).abs() < 1e-9 && (strain[1] - l.ln()).abs() < 1e-9, "{name}: {strain:?}");
|
||
// The reactions on the pulled face balance the axial force σ times the deformed area l².
|
||
let pulled: f64 = g.nodes_where(|x| near(x[0], 1.0)).iter().map(|n| r.reaction[at(&r, *n)][0]).sum();
|
||
assert!((pulled - sigma * l * l).abs() < 1e-8 * sigma, "{name}: {pulled} vs {}", sigma * l * l);
|
||
let h = r.nonlinear.as_ref().unwrap();
|
||
assert!(h.increments.last().is_some_and(|i| i.converged && i.load_factor == 1.0), "{name}: {:?}", h.increments);
|
||
assert_eq!(h.frames.first().map(|f| f.load_factor), Some(0.0));
|
||
assert_eq!(h.frames.last().map(|f| f.load_factor), Some(1.0));
|
||
eprintln!("{name}: {} increments, {} iterations", h.increments.len(), h.iterations);
|
||
}
|
||
}
|
||
|
||
/// St Venant–Kirchhoff in uniaxial stress: S₁₁ = E E₁₁ with E₁₁ = (λ² − 1)/2, lateral
|
||
/// E₂₂ = −ν E₁₁; the Cauchy stress σ = λ² S₁₁ / J.
|
||
#[test]
|
||
fn st_venant_kirchhoff_uniaxial_stretch() {
|
||
let (e, nu, lam): (f64, f64, f64) = (1000.0, 0.25, 1.3);
|
||
let e11 = 0.5 * (lam * lam - 1.0);
|
||
let l = (1.0 - 2.0 * nu * e11).sqrt();
|
||
let sigma = lam * lam * e * e11 / (lam * l * l);
|
||
let mut g = grid([1, 1, 1], ElementKind::Hex20, |x| x);
|
||
g.define(iso(e, nu), None, full(), nonlinear());
|
||
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-9, "{:?}", r.displacement[at(&r, corner)]);
|
||
assert!(r.stress.iter().all(|s| (s[0] - sigma).abs() < 1e-8 * sigma), "{:?} vs {sigma}", r.stress[0]);
|
||
}
|
||
|
||
/// The nonlinear patch test: a distorted mesh whose boundary nodes follow a homogeneous finite
|
||
/// deformation x = F X (stretch, shear and rotation) reproduces it at the inner nodes and gives
|
||
/// the uniform stress of F, for every element kind.
|
||
#[test]
|
||
fn nonlinear_patch_test() {
|
||
let f = [[1.3, 0.2, -0.1], [-0.15, 0.9, 0.25], [0.05, -0.2, 1.1]];
|
||
let affine = |x: [f64; 3]| -> [f64; 3] { std::array::from_fn(|i| (0..3).map(|j| f[i][j] * x[j]).sum::<f64>() - x[i]) };
|
||
let distort = |q: [f64; 3]| {
|
||
let bump = (std::f64::consts::PI * q[0]).sin() * (std::f64::consts::PI * q[1]).sin() * (std::f64::consts::PI * q[2]).sin();
|
||
[q[0] + 0.12 * bump, q[1] - 0.08 * bump, q[2] + 0.1 * bump]
|
||
};
|
||
let (e, nu) = (100.0, 0.3);
|
||
let neo = fieldcraft_fem::constitutive::MaterialModel::from_model(&iso(e, nu), Some(&Hyperelastic::NeoHookean {})).unwrap();
|
||
let tau = neo.response(&f, None, &[], &mut []).unwrap().stress;
|
||
let j = fieldcraft_fem::dense::det3(&f);
|
||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||
let mut cases: Vec<(ElementKind, Formulation, &str)> = variants().into_iter().filter(|v| v.1 != incompatible()).collect();
|
||
cases.extend([
|
||
(ElementKind::Hex8, hybrid, "hex8 hybrid"),
|
||
(ElementKind::Hex20, hybrid, "hex20 hybrid"),
|
||
(ElementKind::Tet10, hybrid, "tet10 hybrid"),
|
||
]);
|
||
for (kind, formulation, name) in cases {
|
||
let mut g = grid([2, 2, 2], kind, distort);
|
||
g.define(iso(e, nu), None, formulation, nonlinear());
|
||
neo_hookean(&mut g, e, nu);
|
||
let boundary = |q: [f64; 3]| q.iter().any(|c| near(*c, 0.0) || near(*c, 1.0));
|
||
prescribe(&mut g, boundary, affine);
|
||
let r = solve(&g);
|
||
for (i, label) in r.nodes.iter().enumerate() {
|
||
let want = affine(g.model.node(*label).unwrap().xyz);
|
||
assert!((0..3).all(|k| (r.displacement[i][k] - want[k]).abs() < 1e-9), "{name} node {label}: {:?} vs {want:?}", r.displacement[i]);
|
||
}
|
||
for el in &r.elements {
|
||
for p in &el.points {
|
||
assert!((0..6).all(|k| (p.stress[k] - tau[k] / j).abs() < 1e-8 * e), "{name}: {:?} vs {:?}", p.stress, tau.map(|t| t / j));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A block turned rigidly by 90° about an inclined axis, its boundary prescribed: no strain, no
|
||
/// stress, no reaction, whatever the path (objectivity of the formulation).
|
||
#[test]
|
||
fn rigid_rotation_strains_and_stresses_nothing() {
|
||
let axis = [1.0 / 3f64.sqrt(); 3];
|
||
let k = fieldcraft_fem::rotation::skew(&axis);
|
||
let k2 = fieldcraft_fem::rotation::mul(&k, &k);
|
||
let rot: [[f64; 3]; 3] = std::array::from_fn(|i| std::array::from_fn(|j| if i == j { 1.0 } else { 0.0 } + k[i][j] + k2[i][j]));
|
||
for (kind, formulation, name) in
|
||
[(ElementKind::Hex20, full(), "hex20"), (ElementKind::Tet10, full(), "tet10"), (ElementKind::Hex8, full(), "hex8")]
|
||
{
|
||
let mut g = grid([2, 2, 2], kind, |x| x);
|
||
g.define(iso(1000.0, 0.3), None, formulation, nonlinear());
|
||
neo_hookean(&mut g, 1000.0, 0.3);
|
||
let boundary = |q: [f64; 3]| q.iter().any(|c| near(*c, 0.0) || near(*c, 1.0));
|
||
prescribe(&mut g, boundary, |x| std::array::from_fn(|i| (0..3).map(|j| rot[i][j] * x[j]).sum::<f64>() - x[i]));
|
||
let r = solve(&g);
|
||
let worst = r.elements.iter().flat_map(|e| &e.points).flat_map(|p| p.stress.iter().chain(&p.strain)).fold(0.0f64, |m, v| m.max(v.abs()));
|
||
assert!(worst < 1e-10 * 1000.0, "{name}: largest stress or strain {worst:e}");
|
||
assert!(r.reaction.iter().flatten().all(|v| v.abs() < 1e-9), "{name}");
|
||
}
|
||
}
|
||
|
||
/// Plane strain and axisymmetric 2D elements reproduce the closed forms: a plane strain block
|
||
/// stretched in x (σ_yy = 0, λ_z = 1) and a solid cylinder stretched along its axis (the 3D
|
||
/// uniaxial case).
|
||
#[test]
|
||
fn plane_strain_and_axisymmetric_stretch() {
|
||
let (e, nu, lam) = (10.0, 0.3, 1.6);
|
||
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
|
||
// Plane strain: σ_yy(l) = μ J^(−5/3) (l² − I₁/3) + κ (J − 1) = 0 with J = λ l, I₁ = λ² + l² + 1.
|
||
let syy = |l: f64| {
|
||
let (j, i1) = (lam * l, lam * lam + l * l + 1.0);
|
||
mu * j.powf(-5.0 / 3.0) * (l * l - i1 / 3.0) + kappa * (j - 1.0)
|
||
};
|
||
let (mut lo, mut hi) = (0.1, 1.5);
|
||
for _ in 0..200 {
|
||
let mid = 0.5 * (lo + hi);
|
||
if syy(mid) > 0.0 { hi = mid } else { lo = mid }
|
||
}
|
||
let l_plane = 0.5 * (lo + hi);
|
||
let (l_axi, sigma_axi) = neo_hookean_uniaxial(mu, kappa, lam);
|
||
for (section, kind) in [
|
||
(SectionKind::PlaneStrain { thickness: 1.0 }, ElementKind::Quad8),
|
||
(SectionKind::PlaneStrain { thickness: 1.0 }, ElementKind::Tri3),
|
||
(SectionKind::Axisymmetric {}, ElementKind::Quad8),
|
||
(SectionKind::Axisymmetric {}, ElementKind::Quad4),
|
||
] {
|
||
let mut m = Model::default();
|
||
let n = 3usize;
|
||
let label = |i: usize, j: usize| (1 + i + (2 * n + 1) * j) as u32;
|
||
for j in 0..=2 * n {
|
||
for i in 0..=2 * n {
|
||
m.set_node(label(i, j), [i as f64 / (2 * n) as f64, j as f64 / (2 * n) as f64, 0.0]).unwrap();
|
||
}
|
||
}
|
||
let mut next = 1;
|
||
for cj in 0..n {
|
||
for ci in 0..n {
|
||
let (i, j) = (2 * ci, 2 * cj);
|
||
let c = [label(i, j), label(i + 2, j), label(i + 2, j + 2), label(i, j + 2)];
|
||
match kind {
|
||
ElementKind::Quad8 => {
|
||
let mid = [label(i + 1, j), label(i + 2, j + 1), label(i + 1, j + 2), label(i, j + 1)];
|
||
m.set_element(next, kind, c.into_iter().chain(mid).collect()).unwrap();
|
||
next += 1;
|
||
}
|
||
ElementKind::Quad4 => {
|
||
m.set_element(next, kind, c.to_vec()).unwrap();
|
||
next += 1;
|
||
}
|
||
_ => {
|
||
m.set_element(next, kind, vec![c[0], c[1], c[2]]).unwrap();
|
||
m.set_element(next + 1, kind, vec![c[0], c[2], c[3]]).unwrap();
|
||
next += 2;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let all: Vec<u32> = m.elements().keys().copied().collect();
|
||
m.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||
let mat = Material { elastic: Some(iso(e, nu)), hyperelastic: Some(Hyperelastic::NeoHookean {}), ..Material::default() };
|
||
m.add_material("Mat", mat).unwrap();
|
||
m.add_section("s", Section { set: "all".into(), material: "Mat".into(), kind: section, formulation: Formulation::default() }).unwrap();
|
||
let axisymmetric = matches!(section, SectionKind::Axisymmetric {});
|
||
// Stretch along x (plane strain) or along the axis y (axisymmetric).
|
||
let (pulled, axis) = if axisymmetric { (1, 1) } else { (0, 0) };
|
||
let on = |k: usize, v: f64| -> Vec<u32> { m.nodes().iter().filter(|(_, n)| near(n.xyz[k], v)).map(|(l, _)| *l).collect() };
|
||
let (low, high, side) = (on(pulled, 0.0), on(pulled, 1.0), on(1 - pulled, 0.0));
|
||
m.add_set("low", Set { nodes: low, ..Set::default() }).unwrap();
|
||
m.add_set("high", Set { nodes: high, ..Set::default() }).unwrap();
|
||
m.add_set("side", Set { nodes: side, ..Set::default() }).unwrap();
|
||
let hold = |k: usize, v: Option<f64>| {
|
||
let mut values = [None; 6];
|
||
values[k] = v;
|
||
values
|
||
};
|
||
m.add_boundary_condition("low", BoundaryCondition::Displacement { target: Target::Set("low".into()), values: hold(axis, Some(0.0)) })
|
||
.unwrap();
|
||
m.add_boundary_condition("high", BoundaryCondition::Displacement { target: Target::Set("high".into()), values: hold(axis, Some(lam - 1.0)) })
|
||
.unwrap();
|
||
m.add_boundary_condition("side", BoundaryCondition::Displacement { target: Target::Set("side".into()), values: hold(1 - axis, Some(0.0)) })
|
||
.unwrap();
|
||
let mut step = Step::new("Step", nonlinear());
|
||
step.boundary_conditions = ["low", "high", "side"].iter().map(|s| s.to_string()).collect();
|
||
m.add_step(step, None).unwrap();
|
||
let r = solve_nonlinear(&m, None, &Limits::default()).unwrap();
|
||
let lateral = if axisymmetric { l_axi } else { l_plane };
|
||
for (i, label) in r.nodes.iter().enumerate() {
|
||
let x = m.node(*label).unwrap().xyz;
|
||
let want = [(lateral - 1.0) * x[1 - pulled], (lam - 1.0) * x[pulled]];
|
||
let (got_axial, got_lateral) = (r.displacement[i][pulled], r.displacement[i][1 - pulled]);
|
||
assert!(
|
||
(got_axial - want[1]).abs() < 1e-8 && (got_lateral - want[0]).abs() < 1e-8,
|
||
"{section:?} {kind:?} node {label}: {:?}",
|
||
r.displacement[i]
|
||
);
|
||
}
|
||
if axisymmetric {
|
||
assert!(r.stress.iter().all(|s| (s[1] - sigma_axi).abs() < 1e-7 * sigma_axi && s[0].abs() < 1e-7 * sigma_axi), "{:?}", r.stress[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Newton converges quadratically near the solution: in the last iterations of each increment
|
||
/// the residual falls at least like the square of the previous one.
|
||
#[test]
|
||
fn newton_converges_quadratically() {
|
||
let mut g = grid([8, 1, 2], ElementKind::Hex20, |q| [10.0 * q[0], q[1], q[2]]);
|
||
let mut step = NonlinearStatic { initial_increment: 0.5, line_search: false, ..NonlinearStatic::default() };
|
||
step.tolerances.force = 1e-12;
|
||
step.tolerances.displacement = 1e-12;
|
||
step.tolerances.energy = 1e-20;
|
||
g.define(iso(1000.0, 0.0), None, full(), Procedure::NonlinearStatic(step));
|
||
g.fixed("root", |q| near(q[0], 0.0));
|
||
g.face_set("tip", |q| near(q[0], 1.0));
|
||
// P L² / (E I) = 1: a tip rotation of about 0.46 rad.
|
||
let p = 1000.0 * (2.0f64.powi(3) / 12.0) / 100.0;
|
||
g.load("push", Load::Force { target: Target::Set("tip".into()), vector: [0.0, 0.0, -p], distribution: Distribution::Total });
|
||
let r = solve(&g);
|
||
let h = r.nonlinear.unwrap();
|
||
// The order of convergence ln(r₂/r₁) / ln(r₁/r₀) over successive residuals in the
|
||
// asymptotic range (below 10⁻³, above round-off): 2 for Newton with the consistent tangent.
|
||
let mut best = 0.0f64;
|
||
for inc in h.increments.iter().filter(|i| i.converged) {
|
||
let res: Vec<f64> = inc.iterations.iter().map(|i| i.force).collect();
|
||
let orders: Vec<f64> = res.windows(3).filter(|w| w[0] < 1e-3 && w[2] > 1e-15).map(|w| (w[2] / w[1]).ln() / (w[1] / w[0]).ln()).collect();
|
||
eprintln!("increment {} (λ = {}): residuals {res:?}, orders {orders:?}", inc.number, inc.load_factor);
|
||
best = orders.iter().fold(best, |m, o| m.max(*o));
|
||
}
|
||
assert!(best > 1.8, "no quadratic convergence (best order {best})");
|
||
}
|
||
|
||
/// Without nonlinear geometry, and with a small load with it, the step is the linear solution.
|
||
#[test]
|
||
fn small_loads_and_small_kinematics_give_the_linear_solution() {
|
||
let build = |procedure: Procedure, scale: f64| {
|
||
let mut g = grid([6, 1, 2], ElementKind::Hex20, |q| [6.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(1000.0, 0.3), None, full(), procedure);
|
||
g.fixed("root", |q| near(q[0], 0.0));
|
||
g.face_set("tip", |q| near(q[0], 1.0));
|
||
g.load("push", Load::Force { target: Target::Set("tip".into()), vector: [0.0, 0.0, -scale], distribution: Distribution::Total });
|
||
g.face_set("top", |q| near(q[2], 1.0));
|
||
g.load("press", Load::Pressure { set: "top".into(), magnitude: 0.1 * scale });
|
||
g
|
||
};
|
||
let linear = solve_static(&build(Procedure::Static {}, 1e-3).model, None, &Limits::default()).unwrap();
|
||
let small = NonlinearStatic { nonlinear_geometry: false, ..NonlinearStatic::default() };
|
||
let r = solve(&build(Procedure::NonlinearStatic(small), 1e-3));
|
||
let tip = linear.displacement.iter().fold(0.0f64, |m, u| m.max(u[2].abs()));
|
||
for (a, b) in r.displacement.iter().zip(&linear.displacement) {
|
||
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-9 * tip), "{a:?} vs {b:?}");
|
||
}
|
||
let r = solve(&build(nonlinear(), 1e-3));
|
||
for (a, b) in r.displacement.iter().zip(&linear.displacement) {
|
||
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 1e-3 * tip), "geometric effects of order u/L: {a:?} vs {b:?}");
|
||
}
|
||
}
|
||
|
||
/// Hostile and impossible input: the step's controls are refused when defined; an element
|
||
/// that inverts makes the step cut back and stop at the minimum increment with the element
|
||
/// named; the caps stop a step; unsupported elements and loads are refused by name.
|
||
#[test]
|
||
fn nonlinear_steps_fail_cleanly() {
|
||
let base = || {
|
||
let mut g = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||
let step = NonlinearStatic { min_increment: 1e-3, ..NonlinearStatic::default() };
|
||
g.define(iso(100.0, 0.3), None, full(), Procedure::NonlinearStatic(step));
|
||
neo_hookean(&mut g, 100.0, 0.3);
|
||
g.fixed("root", |q| near(q[0], 0.0));
|
||
g
|
||
};
|
||
// Squash the bar through itself: past −100 % every element inverts.
|
||
let mut g = base();
|
||
g.node_set("x1", |q| near(q[0], 1.0));
|
||
g.bc("crush", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(-1.5), Some(0.0), Some(0.0), None, None, None] });
|
||
let err = solve_nonlinear(&g.model, None, &Limits::default()).unwrap_err().to_string();
|
||
assert!(err.contains("stopped at load factor") && err.contains("element"), "{err}");
|
||
// A tight cap on the Newton iterations.
|
||
let err = solve_nonlinear(&g.model, None, &Limits { max_newton_iterations: 3, ..Limits::default() }).unwrap_err().to_string();
|
||
assert!(err.contains("Newton iterations"), "{err}");
|
||
// The increment cap.
|
||
let mut capped = base();
|
||
capped.node_set("x1", |q| near(q[0], 1.0));
|
||
capped.bc("pull", BoundaryCondition::Displacement { target: Target::Set("x1".into()), values: [Some(0.5), None, None, None, None, None] });
|
||
let mut step = capped.model.step("Step").unwrap().clone();
|
||
step.set_procedure(Procedure::NonlinearStatic(NonlinearStatic {
|
||
initial_increment: 0.01,
|
||
max_increment: 0.01,
|
||
min_increment: 0.01,
|
||
max_increments: 3,
|
||
..NonlinearStatic::default()
|
||
}));
|
||
capped.model.replace_step(step).unwrap();
|
||
let err = solve_nonlinear(&capped.model, None, &Limits::default()).unwrap_err().to_string();
|
||
assert!(err.contains("increments reached the step's cap"), "{err}");
|
||
// Hostile controls never reach the solver.
|
||
let d = NonlinearStatic::default();
|
||
for bad in [
|
||
NonlinearStatic { initial_increment: f64::NAN, ..d.clone() },
|
||
NonlinearStatic { initial_increment: 0.0, ..d.clone() },
|
||
NonlinearStatic { max_iterations: u32::MAX, ..d.clone() },
|
||
NonlinearStatic { max_increments: 0, ..d.clone() },
|
||
NonlinearStatic { tolerances: Tolerances { force: -1.0, ..Tolerances::default() }, ..d.clone() },
|
||
] {
|
||
let mut step = capped.model.step("Step").unwrap().clone();
|
||
step.set_procedure(Procedure::NonlinearStatic(bad.clone()));
|
||
assert!(capped.model.replace_step(step).is_err(), "{bad:?}");
|
||
}
|
||
// Refused by name: incompatible modes, a hybrid St Venant–Kirchhoff element, centrifugal loads.
|
||
let mut im = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||
im.define(iso(100.0, 0.3), None, incompatible(), nonlinear());
|
||
im.fixed("root", |q| near(q[0], 0.0));
|
||
im.face_set("end", |q| near(q[0], 1.0));
|
||
im.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||
let err = solve_nonlinear(&im.model, None, &Limits::default()).unwrap_err().to_string();
|
||
assert!(err.contains("incompatible modes in nonlinear steps"), "{err}");
|
||
let mut hy = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||
hy.define(iso(100.0, 0.3), None, Formulation { hybrid: true, ..Formulation::default() }, nonlinear());
|
||
hy.fixed("root", |q| near(q[0], 0.0));
|
||
hy.face_set("end", |q| near(q[0], 1.0));
|
||
hy.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
|
||
assert!(err.contains("hybrid elements of a St Venant–Kirchhoff material"), "{err}");
|
||
hy.load("spin", Load::Centrifugal { angular_velocity: 10.0, point: [0.0; 3], axis: [0.0, 0.0, 1.0], set: None });
|
||
neo_hookean(&mut hy, 100.0, 0.3);
|
||
let mut m = hy.model.material("Mat").unwrap().clone();
|
||
m.density = Some(1.0);
|
||
hy.model.replace_material("Mat", m).unwrap();
|
||
let err = solve_nonlinear(&hy.model, None, &Limits::default()).unwrap_err().to_string();
|
||
assert!(err.contains("centrifugal loads"), "{err}");
|
||
// A model that can move freely: the first tangent is singular, named like the linear solver.
|
||
let mut free = grid([2, 1, 1], ElementKind::Hex8, |q| q);
|
||
free.define(iso(100.0, 0.3), None, full(), nonlinear());
|
||
free.face_set("end", |q| near(q[0], 1.0));
|
||
free.load("p", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||
let err = solve_nonlinear(&free.model, None, &Limits::default()).unwrap_err().to_string();
|
||
assert!(err.contains("singular at node"), "{err}");
|
||
// A linear static step is not a nonlinear one.
|
||
let mut lin = grid([1, 1, 1], ElementKind::Hex8, |q| q);
|
||
lin.define(iso(100.0, 0.3), None, full(), Procedure::Static {});
|
||
assert!(solve_nonlinear(&lin.model, None, &Limits::default()).is_err());
|
||
}
|
||
|
||
/// The same model gives the same numbers bit for bit, whatever the thread count.
|
||
#[test]
|
||
fn nonlinear_results_are_deterministic() {
|
||
let mut g = grid([3, 1, 1], ElementKind::Tet10, |q| [4.0 * q[0], q[1], q[2]]);
|
||
g.define(iso(100.0, 0.3), None, full(), nonlinear());
|
||
neo_hookean(&mut g, 100.0, 0.3);
|
||
g.fixed("root", |q| near(q[0], 0.0));
|
||
g.face_set("top", |q| near(q[2], 1.0));
|
||
g.load("press", Load::Pressure { set: "top".into(), magnitude: 0.2 });
|
||
let a = fieldcraft_parallel::with_threads(1, || solve(&g));
|
||
for threads in [2, 7] {
|
||
assert_eq!(fieldcraft_parallel::with_threads(threads, || solve(&g)), a, "{threads} threads");
|
||
}
|
||
}
|
||
|
||
/// The principal Cauchy stresses of a decoupled polynomial energy at principal stretches `l`:
|
||
/// σᵢ = (λᵢ/J) ∂W/∂λᵢ by central differences of W itself (independent of the solver's
|
||
/// stress and tangent), W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + C₂₀ (Ī₁ − 3)² + C₃₀ (Ī₁ − 3)³ + κ/2 (J − 1)².
|
||
fn polynomial_stress(c: [f64; 4], kappa: f64, l: [f64; 3]) -> [f64; 3] {
|
||
let w = |l: [f64; 3]| {
|
||
let j = l[0] * l[1] * l[2];
|
||
let b: Vec<f64> = l.iter().map(|x| x * x * j.powf(-2.0 / 3.0)).collect();
|
||
let i1 = b[0] + b[1] + b[2];
|
||
let i2 = b[0] * b[1] + b[1] * b[2] + b[2] * b[0];
|
||
let x = i1 - 3.0;
|
||
c[0] * x + c[1] * (i2 - 3.0) + c[2] * x * x + c[3] * x * x * x + 0.5 * kappa * (j - 1.0) * (j - 1.0)
|
||
};
|
||
let j = l[0] * l[1] * l[2];
|
||
std::array::from_fn(|i| {
|
||
let h = 1e-6 * l[i];
|
||
let (mut p, mut m) = (l, l);
|
||
p[i] += h;
|
||
m[i] -= h;
|
||
l[i] / j * (w(p) - w(m)) / (2.0 * h)
|
||
})
|
||
}
|
||
|
||
/// Mooney–Rivlin and Yeoh blocks in the homogeneous modes of rubber testing (L. R. G. Treloar,
|
||
/// *The Physics of Rubber Elasticity*, 3rd ed., Oxford 1975, ch. 10): uniaxial and equibiaxial
|
||
/// tension and pure shear, against the closed form of the compressible decoupled energy (the
|
||
/// free stretches found by bisection so the free faces carry no stress).
|
||
#[test]
|
||
fn mooney_rivlin_and_yeoh_in_the_homogeneous_modes() {
|
||
let (e, nu) = (3.0, 0.49);
|
||
let (mu, kappa) = (e / (2.0 * (1.0 + nu)), e / (3.0 * (1.0 - 2.0 * nu)));
|
||
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
|
||
let (c01, c20, c30) = (0.2, -0.02, 0.004);
|
||
for option in [Hyperelastic::MooneyRivlin { c01 }, Hyperelastic::Yeoh { c20, c30 }] {
|
||
let c = option.constants(mu);
|
||
for (mode, lam) in [("uniaxial", 2.0), ("equibiaxial", 1.5), ("pure shear", 2.0)] {
|
||
// The stretches with the free one(s) l from σ_free(l) = 0.
|
||
let stretches = |l: f64| match mode {
|
||
"uniaxial" => [lam, l, l],
|
||
"equibiaxial" => [lam, lam, l],
|
||
_ => [lam, 1.0, l],
|
||
};
|
||
let (mut lo, mut hi) = (0.05, 1.5);
|
||
for _ in 0..200 {
|
||
let mid = 0.5 * (lo + hi);
|
||
if polynomial_stress(c, kappa, stretches(mid))[2] > 0.0 { hi = mid } else { lo = mid }
|
||
}
|
||
let want = polynomial_stress(c, kappa, stretches(0.5 * (lo + hi)));
|
||
let mut g = grid([1, 1, 1], ElementKind::Hex20, |x| x);
|
||
g.define(iso(e, nu), None, hybrid, nonlinear());
|
||
g.model.replace_material("Mat", Material { elastic: Some(iso(e, nu)), hyperelastic: Some(option), ..Material::default() }).unwrap();
|
||
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] },
|
||
);
|
||
g.node_set("y1", |x| near(x[1], 1.0));
|
||
let y = match mode {
|
||
"equibiaxial" => Some(lam - 1.0),
|
||
"pure shear" => Some(0.0),
|
||
_ => None,
|
||
};
|
||
if let Some(v) = y {
|
||
g.bc("y1", BoundaryCondition::Displacement { target: Target::Set("y1".into()), values: [None, Some(v), None, None, None, None] });
|
||
}
|
||
let r = solve(&g);
|
||
for s in &r.stress {
|
||
for k in 0..3 {
|
||
assert!((s[k] - want[k]).abs() < 1e-7 * want[0], "{} {mode}: {s:?} vs {want:?}", option.name());
|
||
}
|
||
}
|
||
eprintln!("{} {mode} λ = {lam}: σ = {:.6?} (closed form {want:.6?})", option.name(), &r.stress[0][..3]);
|
||
}
|
||
}
|
||
}
|