Files
OneCAD-Simulation/crates/solver/tests/hybrid.rs
T
5ebb9f16dc Hybrid (mixed u–p) solids: hex8 and tet10 with a constant pressure, hex20 with a linear one (#49)
The isotropic elasticity splits into D_dev + K m mᵀ; an element-wise
discontinuous pressure is condensed out (p̂ = K M⁻¹ Gᵀ u), leaving
K_dev + K G M⁻¹ Gᵀ (Hughes §4.3–4.5). Q2–P1disc and P2–P0 are inf-sup
stable; Q1–P0 does not lock; tet4 and wedge6 still lock and are refused, as
are orthotropic materials (named at the section). ElementDofs counts the
condensed pressures. Verified: element patch, rigid modes and totals for
every hybrid kind, a block in uniaxial strain at ν = 0.4999 (exact), Lamé's
cylinder in plane strain at ν = 0.4999 (hex20 1e-5, tet10 1.3e-3, hex8
2.5e-3 vs the locking displacement hex8 at −80 %). Every non-hybrid result
of the test suites hashes identically before and after.


Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 22:20:21 -04:00

141 lines
6.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Hybrid (mixed u–p) solids for nearly incompressible materials, against closed forms: a block
//! in uniaxial strain (exact for every element) and Lamé's thick cylinder in plane strain at
//! ν = 0.4999, where the displacement elements lock and the hybrid ones do not. Run with
//! `--nocapture` to see the errors.
mod common;
use common::*;
use modelcraft_model::*;
use modelcraft_solver::{Limits, solve_static};
const NU: f64 = 0.4999;
fn hybrid() -> Formulation {
Formulation { hybrid: true, ..Formulation::default() }
}
fn hybrids() -> Vec<(ElementKind, &'static str)> {
vec![(ElementKind::Hex8, "hex8 hybrid"), (ElementKind::Hex20, "hex20 hybrid"), (ElementKind::Tet10, "tet10 hybrid")]
}
#[test]
fn block_in_uniaxial_strain() {
// Held normal to the bottom and the four sides, pressed on top: ε_zz = −p/M with the
// constrained modulus M = E(1 − ν)/((1 + ν)(1 − 2ν)); σ_xx = σ_yy = −p ν/(1 − ν).
let (e, p) = (10.0, 1.0);
let m = e * (1.0 - NU) / ((1.0 + NU) * (1.0 - 2.0 * NU));
for (kind, name) in hybrids() {
let mut g = grid([2, 2, 2], kind, |x| x);
g.define(iso(e, NU), None, hybrid(), Procedure::Static {});
g.symmetry("x0", Symmetry::X, |x| near(x[0], 0.0));
g.symmetry("x1", Symmetry::X, |x| near(x[0], 1.0));
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
g.symmetry("y1", Symmetry::Y, |x| near(x[1], 1.0));
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
g.face_set("top", |x| near(x[2], 1.0));
g.load("press", Load::Pressure { set: "top".into(), magnitude: p });
let r = solve_static(&g.model, None, &Limits::default()).unwrap();
for (i, l) in r.nodes.iter().enumerate() {
let z = g.model.node(*l).unwrap().xyz[2];
assert!((r.displacement[i][2] + p * z / m).abs() < 1e-9 * p / m, "{name} node {l}: {:?}", r.displacement[i]);
}
for s in &r.stress {
assert!((s[2] + p).abs() < 1e-6 && (s[0] + p * NU / (1.0 - NU)).abs() < 1e-6 && (s[1] - s[0]).abs() < 1e-6, "{name}: {s:?}");
}
}
}
/// The inner radial displacement of a quarter of a thick cylinder a = 1, b = 2 in plane strain
/// under internal pressure 1, and Lamé's value.
fn cylinder(kind: ElementKind, formulation: Formulation, n: [usize; 3]) -> (f64, f64) {
let (e, p, a, b) = (1000.0, 1.0, 1.0, 2.0);
let mut g = grid(n, kind, |x| {
let (r, t) = (a + (b - a) * x[0], std::f64::consts::FRAC_PI_2 * x[1]);
[r * t.cos(), r * t.sin(), 0.2 * x[2]]
});
g.define(iso(e, NU), None, formulation, Procedure::Static {});
g.symmetry("y0", Symmetry::Y, |x| near(x[1], 0.0));
g.symmetry("x0", Symmetry::X, |x| near(x[1], 1.0));
g.symmetry("z0", Symmetry::Z, |x| near(x[2], 0.0));
g.symmetry("z1", Symmetry::Z, |x| near(x[2], 1.0));
g.face_set("bore", |x| near(x[0], 0.0));
g.load("pressure", Load::Pressure { set: "bore".into(), magnitude: p });
let r = solve_static(&g.model, None, &Limits::default()).unwrap();
let inner = g.nodes_where(|x| near(x[0], 0.0) && near(x[1], 0.0) && near(x[2], 0.0))[0];
let u = r.displacement[r.nodes.binary_search(&inner).unwrap()][0];
let exact = (1.0 + NU) * p * a * a / (e * (b * b - a * a)) * ((1.0 - 2.0 * NU) * a + b * b / a);
(u, exact)
}
#[test]
fn nearly_incompressible_thick_cylinder_does_not_lock() {
for (kind, name, n, tol) in [
(ElementKind::Hex8, "hex8 hybrid", [8, 12, 1], 0.01),
(ElementKind::Hex20, "hex20 hybrid", [4, 6, 1], 1e-4),
(ElementKind::Tet10, "tet10 hybrid", [4, 6, 1], 5e-3),
] {
let (u, exact) = cylinder(kind, hybrid(), n);
eprintln!("{name}: u(a) = {u:.6e}, Lamé {exact:.6e}, error {:.2e}", (u - exact) / exact);
assert!(((u - exact) / exact).abs() < tol, "{name}: {u} vs {exact}");
}
// The displacement hex8 locks: far too stiff on the same mesh.
let (u, exact) = cylinder(ElementKind::Hex8, Formulation::default(), [8, 12, 1]);
eprintln!("hex8 (displacement): u(a) = {u:.6e}, Lamé {exact:.6e}, error {:.2e}", (u - exact) / exact);
assert!(u < 0.7 * exact, "full hex8 should lock at ν = {NU}: {u} vs {exact}");
}
/// Why a one-cell hybrid model of `kind` and `elastic` cannot be solved.
fn refusal(kind: ElementKind, elastic: Elastic) -> String {
let mut g = grid([1, 1, 1], kind, |x| x);
g.define(elastic, None, hybrid(), Procedure::Static {});
g.fixed("base", |x| near(x[2], 0.0));
g.face_set("top", |x| near(x[2], 1.0));
g.load("press", Load::Pressure { set: "top".into(), magnitude: 1.0 });
solve_static(&g.model, None, &Limits::default()).unwrap_err().to_string()
}
#[test]
fn hybrid_refusals() {
for (kind, needle) in [(ElementKind::Tet4, "hybrid tet4"), (ElementKind::Wedge6, "hybrid wedge6")] {
let e = refusal(kind, iso(10.0, NU));
assert!(e.contains(needle), "{e}");
}
let ortho = Elastic::EngineeringConstants { young: [10.0, 20.0, 30.0], poisson: [0.3, 0.2, 0.1], shear: [5.0, 6.0, 7.0] };
let e = refusal(ElementKind::Hex8, ortho);
assert!(e.contains("orthotropic material `Mat`"), "{e}");
}
#[test]
fn heated_confined_block_carries_the_bulk_thermal_stress() {
// Held normal to all six faces and heated by ΔT: σ = −3 K α ΔT in every direction, the
// pressure the hybrid elements carry as their own unknown.
let (e, alpha, dt) = (10.0, 1e-4, 30.0);
let bulk = e / (3.0 * (1.0 - 2.0 * NU));
for (kind, name) in hybrids() {
let mut g = grid([2, 2, 2], kind, |x| x);
g.define(iso(e, NU), None, hybrid(), Procedure::Static {});
let thermal = Thermal { expansion: Some(Directional::Isotropic(alpha)), ..Thermal::default() };
g.model.replace_material("Mat", Material { elastic: Some(iso(e, NU)), thermal: Some(thermal), ..Material::default() }).unwrap();
for (set, plane, k, v) in [
("x0", Symmetry::X, 0, 0.0),
("x1", Symmetry::X, 0, 1.0),
("y0", Symmetry::Y, 1, 0.0),
("y1", Symmetry::Y, 1, 1.0),
("z0", Symmetry::Z, 2, 0.0),
("z1", Symmetry::Z, 2, 1.0),
] {
g.symmetry(set, plane, |x| near(x[k], v));
}
g.node_set("everything", |_| true);
g.load("warm", Load::Temperature { target: Target::Set("everything".into()), value: dt });
let r = solve_static(&g.model, None, &Limits::default()).unwrap();
let want = -3.0 * bulk * alpha * dt;
for s in &r.stress {
for k in 0..3 {
assert!((s[k] - want).abs() < 1e-6 * want.abs(), "{name}: {s:?} vs {want}");
}
}
}
}