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 <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 22:20:21 -04:00
1 parent 0899587127
commit 5ebb9f16dc
11 files changed
+525 -32

No files matched your search

+16 -5
View File
@@ -34,7 +34,7 @@ fn distorted(kind: ElementKind, curved: bool) -> Vec<[f64; 3]> {
xyz
}
const CASES: [(ElementKind, Formulation); 7] = [
const CASES: [(ElementKind, Formulation); 10] = [
(ElementKind::Tet4, Formulation::Full),
(ElementKind::Tet10, Formulation::Full),
(ElementKind::Wedge6, Formulation::Full),
@@ -42,6 +42,9 @@ const CASES: [(ElementKind, Formulation); 7] = [
(ElementKind::Hex8, Formulation::IncompatibleModes),
(ElementKind::Hex20, Formulation::Full),
(ElementKind::Hex20, Formulation::Reduced),
(ElementKind::Tet10, Formulation::Hybrid { pressures: 1 }),
(ElementKind::Hex8, Formulation::Hybrid { pressures: 1 }),
(ElementKind::Hex20, Formulation::Hybrid { pressures: 4 }),
];
fn rigid_modes(xyz: &[[f64; 3]]) -> Vec<Vec<f64>> {
@@ -75,8 +78,11 @@ fn stiffness_is_symmetric_with_exactly_six_rigid_body_modes() {
}
// Holding six degrees of freedom (node 1 fully, node 2 in y and z, node 3 in z) leaves a
// positive definite matrix: no other zero-energy mode. Reduced hex20 has spurious modes
// on a single element, so it is skipped.
if f != Formulation::Reduced {
// on a single element, and so has hybrid tet10: its deviatoric stiffness is blind to the
// four dilatations with a linear volumetric strain, and its constant pressure controls
// one; neighbouring elements restrain the other three (P2–P0, crate::hybrid). Both are
// skipped.
if f != Formulation::Reduced && !(kind == ElementKind::Tet10 && matches!(f, Formulation::Hybrid { .. })) {
let mut held = k.clone();
for dof in [0, 1, 2, 4, 5, 8] {
held.add(dof, dof, 1e3 * scale);
@@ -95,7 +101,9 @@ fn constant_strain_is_reproduced_exactly() {
let strain = [a[0][0], a[1][1], a[2][2], a[0][1] + a[1][0], a[0][2] + a[2][0], a[1][2] + a[2][1]];
for d in [isotropic(210e3, 0.3).unwrap(), orthotropic([140e3, 10e3, 12e3], [0.3, 0.25, 0.4], [5e3, 4e3, 3.8e3]).unwrap()] {
let want = stress(&d, &strain);
for (kind, f) in CASES {
// Hybrid elements need an isotropic material.
let hybrid_ok = crate::hybrid::split(&d).is_ok();
for (kind, f) in CASES.into_iter().filter(|(_, f)| hybrid_ok || !matches!(f, Formulation::Hybrid { .. })) {
// Curved mid-sides keep the field in the isoparametric span only for straight edges.
let xyz = distorted(kind, false);
let u: Vec<[f64; 3]> = xyz.iter().map(|x| [0, 1, 2].map(|i| (0..3).map(|j| a[i][j] * x[j]).sum())).collect();
@@ -213,9 +221,12 @@ fn section_formulations() {
fn uniform_thermal_strain_is_free_expansion_and_its_load_balances_it() {
// A uniform ΔT with orthotropic α: the free expansion u = diag(α ΔT) x is stress-free, and
// the thermal load equals K u for it (the element is in equilibrium with no other load).
let d = orthotropic([1000.0, 800.0, 600.0], [0.3, 0.25, 0.2], [400.0, 300.0, 350.0]).unwrap();
let ortho = orthotropic([1000.0, 800.0, 600.0], [0.3, 0.25, 0.2], [400.0, 300.0, 350.0]).unwrap();
let iso = isotropic(1000.0, 0.4999).unwrap();
let alpha = [1.2e-5, 2.0e-5, 0.7e-5];
for (kind, f) in CASES {
// Hybrid elements take isotropic (here nearly incompressible) materials.
let d = if matches!(f, Formulation::Hybrid { .. }) { iso } else { ortho };
let xyz = distorted(kind, true);
let solid = Solid::new(kind, &xyz, f).unwrap();
let th = crate::ThermalStrain { alpha, delta: vec![80.0; xyz.len()] };