fem: hybrid plane strain and axisymmetric elements

One constant pressure per element (mean dilatation, Nagtegaal, Parks &
Rice 1974; the hoop strain in the divergence) for quad4, quad8 and tri6:
the linear stiffness, stresses and thermal load, and the finite- and
small-strain forms of nonlinear steps with their consistent tangent. The
linear triangle and plane stress are refused by name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 16:30:44 -04:00
1 parent 2ca5f84113
commit 6baa189231
8 files changed
+422 -23

No files matched your search

+155 -12
View File
@@ -515,6 +515,21 @@ impl Plane<'_> {
}
}
/// The kinematics of one point of a 2D element in a nonlinear step: F, det F_m, the volume share, the strain columns (current at finite strain), the small strain,
/// the temperature and the reference position.
struct PlaneKinematics {
f: Mat3,
/// det F_m (small strain: 1), and the dilatation the mixed element averages (small strain:
/// 1 + tr ε).
j: f64,
dilatation: f64,
dv: f64,
current: Vec<[f64; 3]>,
strain: [f64; 6],
heat: Heat,
x: [f64; 3],
}
/// One integration point of a 2D element in a nonlinear step: the total deformation gradient,
/// det F_m, the reference position and the response.
struct PlanePoint {
@@ -525,8 +540,14 @@ struct PlanePoint {
}
impl Plane<'_> {
fn check_nonlinear(&self, u: &[[f64; 2]], kin: Kinematics) -> Result<()> {
fn check_nonlinear(&self, m: &MaterialModel, u: &[[f64; 2]], kin: Kinematics) -> Result<()> {
check_displacements(u, self.ty.nodes)?;
if matches!(self.formulation, Formulation::Hybrid { .. }) && !m.is_decoupled() {
return Err(Error::NotSupported(format!(
"hybrid elements of a {} material in nonlinear steps (the mixed formulation needs a volumetric–isochoric split of the strain energy; give the material the neo-Hookean option)",
m.name()
)));
}
if let Theory::PlaneStress { .. } = self.theory {
let why = match kin {
Kinematics::Finite => "the thickness change needs a local iteration",
@@ -558,7 +579,7 @@ impl Plane<'_> {
updated: &mut [f64],
tangent: bool,
) -> Result<(Finite, Vec<PlanePoint>)> {
self.check_nonlinear(u, kin)?;
self.check_nonlinear(m, u, kin)?;
let n = self.ty.nodes;
if let Some(t) = thermal {
t.check(n)?;
@@ -570,8 +591,9 @@ impl Plane<'_> {
let mut k = tangent.then(|| Matrix::zeros(ndof));
let axisymmetric = self.theory == Theory::Axisymmetric;
let small = kin == Kinematics::Small;
let mut out = Vec::with_capacity(rule.len());
for (i, p) in rule.iter().enumerate() {
// The kinematics of every point first: the mixed element's pressure needs them all.
let mut kinematics = Vec::with_capacity(rule.len());
for p in &rule {
let at = self.at(p)?;
let cols = at.g.get(..n).unwrap_or_default();
let mut f = IDENTITY;
@@ -585,12 +607,11 @@ impl Plane<'_> {
f[2][2] += ua[0] * g[2];
}
}
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
let heat = Heat::at(thermal, at.shape.values())?;
let (j, dv, current, r) = if small {
let point = if small {
let strain = heat.mechanical_strain(&small_strain(&f));
let r = m.small_strain(&strain, heat.temperature, point_history(committed, len, i), updated_point, true)?;
(1.0, at.dv, cols.to_vec(), r)
let volume = 1.0 + strain[0] + strain[1] + strain[2];
PlaneKinematics { f, j: 1.0, dilatation: volume, dv: at.dv, current: cols.to_vec(), strain, heat, x: at.x }
} else {
let det2 = f[0][0] * f[1][1] - f[0][1] * f[1][0];
let j = det2 * f[2][2];
@@ -600,9 +621,29 @@ impl Plane<'_> {
let inv = [[f[1][1] / det2, -f[0][1] / det2], [-f[1][0] / det2, f[0][0] / det2]];
let current: Vec<[f64; 3]> =
cols.iter().map(|g| [g[0] * inv[0][0] + g[1] * inv[1][0], g[0] * inv[0][1] + g[1] * inv[1][1], g[2] / f[2][2]]).collect();
let r = m.response(&heat.mechanical(&f), heat.temperature, point_history(committed, len, i), updated_point)?;
(j / heat.volume(), at.dv * heat.volume(), current, r)
let j = j / heat.volume();
PlaneKinematics { f, j, dilatation: j, dv: at.dv * heat.volume(), current, strain: [0.0; 6], heat, x: at.x }
};
kinematics.push(point);
}
let responses: Vec<Response> = if let Formulation::Hybrid { .. } = self.formulation {
self.mean_dilatation(m, &kinematics, small, committed, updated, k.as_mut())?
} else {
let mut out = Vec::with_capacity(kinematics.len());
for (i, p) in kinematics.iter().enumerate() {
let updated_point = updated.get_mut(i * len..(i + 1) * len).unwrap_or_default();
let history = point_history(committed, len, i);
out.push(if small {
m.small_strain(&p.strain, p.heat.temperature, history, updated_point, true)?
} else {
m.response(&p.heat.mechanical(&p.f), p.heat.temperature, history, updated_point)?
});
}
out
};
let mut out = Vec::with_capacity(rule.len());
for (p, r) in kinematics.into_iter().zip(responses) {
let (dv, current) = (p.dv, &p.current);
let tau = [r.stress[0], r.stress[1], r.stress[2], r.stress[3]];
for (a, g) in current.iter().enumerate() {
if let Some(x) = force.get_mut(2 * a) {
@@ -614,7 +655,7 @@ impl Plane<'_> {
}
if let Some(k) = k.as_mut() {
let c4: [[f64; 4]; 4] = std::array::from_fn(|a| std::array::from_fn(|b| r.tangent[a][b]));
crate::plane::accumulate(&mut k.data, &current, &c4, dv);
crate::plane::accumulate(&mut k.data, current, &c4, dv);
if !small {
for (a, ga) in current.iter().enumerate() {
for (b, gb) in current.iter().enumerate() {
@@ -625,11 +666,56 @@ impl Plane<'_> {
}
}
}
out.push(PlanePoint { f, j, x: at.x, response: r });
out.push(PlanePoint { f: p.f, j: p.j, x: p.x, response: r });
}
Ok((Finite { force, tangent: k }, out))
}
/// The mean dilatation responses of a hybrid 2D element (one constant pressure): θ = ∫ J dV₀
/// / V₀ (hoop stretch included), p = U′(θ), τ = τ_iso + J p I (small strain: s + p I), and
/// the dilatation stiffness U″(θ) g gᵀ / V₀ added to `k`, g = ∫ J ∇ₓ·N dV₀ (small strain
/// ∫ ∇₀·N dV₀), the divergence taking the hoop column N/r too (the constant-pressure case of
/// the solids' three-field form, Simo, Taylor & Pister 1985).
fn mean_dilatation(
&self,
m: &MaterialModel,
points: &[PlaneKinematics],
small: bool,
committed: &[f64],
updated: &mut [f64],
k: Option<&mut Matrix>,
) -> Result<Vec<Response>> {
let len = m.history_len();
let volume: f64 = points.iter().map(|p| p.dv).sum();
if !(volume > 0.0 && volume.is_finite()) {
return Err(inverted(&format!("2D {}", self.ty.kind.name()), volume));
}
let theta = points.iter().map(|p| p.dv * p.dilatation).sum::<f64>() / volume;
if !(theta > 0.0 && theta.is_finite()) {
return Err(inverted(&format!("2D {}", self.ty.kind.name()), theta));
}
let (pressure, stiffness) = m.volumetric(theta, small).ok_or_else(|| Error::Invalid("volumetric response".into()))?;
let mut out = Vec::with_capacity(points.len());
for (i, p) in points.iter().enumerate() {
let (c, u) = (point_history(committed, len, i), updated.get_mut(i * len..(i + 1) * len).unwrap_or_default());
out.push(if small {
let mut r = m.small_strain(&p.strain, p.heat.temperature, c, u, false)?;
r.stress.iter_mut().take(3).for_each(|s| *s += pressure);
r
} else {
add_pressure(&m.isochoric(&p.heat.mechanical(&p.f), p.heat.temperature, c, u)?, p.j, pressure, 0.0)
});
}
if let Some(k) = k {
let mut g = vec![0.0; k.n];
for p in points {
crate::plane::divergence(&mut g, &p.current, p.dv * p.j);
}
crate::plane::add_outer(k, &g, stiffness / volume);
}
Ok(out)
}
/// Internal forces (u, v per node) and (with `tangent`) the tangent stiffness at nodal
/// displacements `u`, as for [`Solid::finite_strain`].
pub fn finite_strain(&self, m: &MaterialModel, u: &[[f64; 2]], committed: &[f64], updated: &mut [f64], tangent: bool) -> Result<Finite> {
@@ -1016,6 +1102,63 @@ mod tests {
assert!(matches!(stress.finite_strain(&svk, &[[0.0; 2]; 4], &[], &mut [], true), Err(Error::NotSupported(_))));
}
#[test]
fn hybrid_2d_tangents_are_the_derivative_of_the_forces() {
let quad8 = [
[1.0, 0.0, 0.0],
[2.0, 0.1, 0.0],
[2.2, 1.0, 0.0],
[0.9, 1.1, 0.0],
[1.5, 0.0, 0.0],
[2.1, 0.55, 0.0],
[1.5, 1.1, 0.0],
[0.95, 0.5, 0.0],
];
let (shear, bulk) = (300.0, 30000.0);
let neo = MaterialModel::NeoHookean { shear, bulk };
let mr = MaterialModel::Polynomial { name: "Mooney–Rivlin", c: [100.0, 50.0, 0.0, 0.0], bulk };
let hybrid = Formulation::Hybrid { pressures: 1 };
for theory in [Theory::PlaneStrain { thickness: 2.0 }, Theory::Axisymmetric] {
for (kind, x) in [(ElementKind::Quad8, &quad8[..]), (ElementKind::Quad4, &quad8[..4])] {
let plane = Plane::new(kind, x, hybrid, theory).unwrap();
for m in [&neo, &mr] {
let u: Vec<[f64; 2]> = displacement(x.len(), 0.06).iter().map(|v| [v[0], v[1]]).collect();
let k = plane.finite_strain(m, &u, &[], &mut [], true).unwrap().tangent.unwrap();
let scale = k.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
let h = 1e-6;
for j in 0..2 * x.len() {
let shifted = |s: f64| {
let mut v = u.clone();
v[j / 2][j % 2] += s;
plane.finite_strain(m, &v, &[], &mut [], false).unwrap().force
};
let (p, q) = (shifted(h), shifted(-h));
for i in 0..2 * x.len() {
let numeric = (p[i] - q[i]) / (2.0 * h);
assert!((numeric - k.get(i, j)).abs() < 1e-6 * scale, "{theory:?} {kind:?} {i} {j}: {numeric} vs {}", k.get(i, j));
}
}
}
// At rest the tangent is the linear hybrid stiffness of the same moduli.
let young = 9.0 * bulk * shear / (3.0 * bulk + shear);
let poisson = (3.0 * bulk - 2.0 * shear) / (2.0 * (3.0 * bulk + shear));
let d = isotropic(young, poisson).unwrap();
let k = plane.finite_strain(&neo, &vec![[0.0; 2]; x.len()], &[], &mut [], true).unwrap().tangent.unwrap();
let linear = plane.stiffness(&d).unwrap();
let scale = linear.data.iter().fold(0.0f64, |a, v| a.max(v.abs()));
for (a, b) in k.data.iter().zip(&linear.data) {
assert!((a - b).abs() < 1e-9 * scale, "{a} {b}");
}
}
}
// The linear triangle and plane stress have none; St Venant–Kirchhoff has no split.
assert!(Plane::new(ElementKind::Tri3, &quad8[..3], hybrid, Theory::PlaneStrain { thickness: 1.0 }).is_err());
assert!(Plane::new(ElementKind::Quad4, &quad8[..4], hybrid, Theory::PlaneStress { thickness: 1.0 }).is_err());
let svk = MaterialModel::StVenantKirchhoff(Box::new(isotropic(1000.0, 0.3).unwrap()));
let plane = Plane::new(ElementKind::Quad4, &quad8[..4], hybrid, Theory::Axisymmetric).unwrap();
assert!(matches!(plane.finite_strain(&svk, &[[0.0; 2]; 4], &[], &mut [], true), Err(Error::NotSupported(_))));
}
/// uᵀ K_σ u = ∫ σ_xx (∂u_y/∂x)² dV = σ_xx V for u_y = x under a uniform σ_xx, in a distorted
/// hex20 and a plane strain quad8, and K_σ annihilates rigid translations.
#[test]
+167 -7
View File
@@ -35,6 +35,15 @@
//! leaves the plane case unchanged (its mean is zero) and makes the axisymmetric element pass the
//! patch test.
//!
//! **Hybrid** (mixed displacement–pressure, plane strain and axisymmetric quad4, quad8 and tri6):
//! one constant pressure per element, the mean dilatation method (J. C. Nagtegaal, D. M. Parks,
//! J. R. Rice, *On numerically accurate finite element solutions in the fully plastic range*,
//! CMAME 4(2):153–177, 1974; T. J. R. Hughes, *The Finite Element Method*, Dover 2000, §4.5 for
//! the axisymmetric B-bar): the volumetric strain, hoop part included, is replaced by its volume
//! mean, so K = ∫ Bᵀ D_dev B dV + κ g gᵀ / V with g = ∫ Bᵀ m dV, m = (1, 1, 1, 0). The linear
//! triangle is refused (one pressure per element still locks it), plane stress too (σ_zz = 0
//! leaves no volume constraint to relax). The finite-strain form is [`crate::finite`]'s.
//!
//! On the axis (r = 0) the hoop strain u/r is evaluated only at integration points, which lie
//! inside the element (r > 0) for every element with some node off the axis; the solver holds
//! the radial displacement of nodes on the axis at zero, as symmetry requires.
@@ -165,11 +174,24 @@ pub fn plane_type(kind: ElementKind) -> Result<&'static ElementType> {
.ok_or_else(|| Error::NotSupported(format!("{} elements in a 2D section (2D sections take tri3, tri6, quad4 and quad8)", kind.name())))
}
/// The 2D element kinds with a hybrid (constant pressure) formulation.
const HYBRID_PRESSURES: [ElementKind; 3] = [ElementKind::Quad4, ElementKind::Quad8, ElementKind::Tri6];
/// The formulation a 2D section asks for, if this element type has it.
pub fn formulation(kind: ElementKind, f: &fieldcraft_model::Formulation) -> Result<Formulation> {
let ty = plane_type(kind)?;
if f.hybrid {
return Err(Error::NotSupported("hybrid (mixed displacement–pressure) 2D elements".into()));
if f.incompatible_modes || f.integration == fieldcraft_model::Integration::Reduced {
return Err(Error::NotSupported("hybrid 2D elements with incompatible modes or reduced integration".into()));
}
return if HYBRID_PRESSURES.contains(&kind) {
Ok(Formulation::Hybrid { pressures: 1 })
} else {
Err(Error::NotSupported(format!(
"hybrid 2D {} elements (one pressure per element still locks the linear triangle; use quad4, quad8 or tri6)",
kind.name()
)))
};
}
if f.incompatible_modes {
return if ty.incompatible_modes {
@@ -270,11 +292,14 @@ impl<'a> Plane<'a> {
Formulation::Full => true,
Formulation::Reduced => ty.reduced.is_some(),
Formulation::IncompatibleModes => ty.incompatible_modes,
Formulation::Hybrid { .. } => false,
Formulation::Hybrid { pressures } => pressures == 1 && HYBRID_PRESSURES.contains(&kind),
};
if !ok {
return Err(Error::NotSupported(format!("{formulation:?} formulation of 2D {} elements", kind.name())));
}
if matches!(formulation, Formulation::Hybrid { .. }) && matches!(theory, Theory::PlaneStress { .. }) {
return Err(Error::NotSupported("hybrid plane stress elements (σ_zz = 0 leaves no volume constraint; use full integration)".into()));
}
Ok(Plane { ty, xyz, formulation, theory })
}
@@ -282,7 +307,8 @@ impl<'a> Plane<'a> {
/// modes it condenses out.
pub fn dofs(&self) -> ElementDofs {
let incompatible_modes = if self.formulation == Formulation::IncompatibleModes { INCOMPATIBLE_DOFS } else { 0 };
ElementDofs { node: DofSet::PLANE, internal: Internal { incompatible_modes, pressure: 0 } }
let pressure = if let Formulation::Hybrid { pressures } = self.formulation { pressures } else { 0 };
ElementDofs { node: DofSet::PLANE, internal: Internal { incompatible_modes, pressure } }
}
/// The stiffness integration rule.
@@ -411,6 +437,13 @@ impl<'a> Plane<'a> {
/// The stiffness matrix, 2n × 2n, degrees of freedom ordered node by node (u, v).
pub fn stiffness(&self, d: &Elasticity) -> Result<Matrix> {
if let Some(dev) = self.deviatoric(d)? {
// K_dev + κ g gᵀ / V.
let (mut k, points, _) = self.full_stiffness(&dev)?;
let mixed = self.mixed(d, &points, None)?;
add_outer(&mut k, &mixed.g, mixed.bulk / mixed.volume);
return Ok(k);
}
let d2 = self.theory.elasticity(d)?;
let (full, _, modes) = self.full_stiffness(&d2)?;
if modes.is_none() {
@@ -433,6 +466,34 @@ impl<'a> Plane<'a> {
Ok(k)
}
/// A hybrid element's deviatoric elasticity in the theory's form; `None` for the other
/// formulations.
fn deviatoric(&self, d: &Elasticity) -> Result<Option<Elasticity2>> {
if !matches!(self.formulation, Formulation::Hybrid { .. }) {
return Ok(None);
}
Ok(Some(self.theory.elasticity(&crate::hybrid::split(d)?.0)?))
}
/// A hybrid element's parts over its stiffness `points`: the bulk modulus, g = ∫ Bᵀ m dV
/// (2n), the volume and, with a thermal strain, ∫ tr ε_th dV.
pub(crate) fn mixed(&self, d: &Elasticity, points: &[(Point, At)], thermal: Option<&ThermalStrain>) -> Result<Mixed2> {
let bulk = crate::hybrid::split(d)?.1;
let (mut g, mut volume, mut thermal_volume) = (vec![0.0; 2 * self.ty.nodes], 0.0, 0.0);
for (_, a) in points {
volume += a.dv;
divergence(&mut g, a.g.get(..self.ty.nodes).unwrap_or_default(), a.dv);
if let Some(t) = thermal {
let e = Self::thermal_strain(t, a.shape.values());
thermal_volume += a.dv * (e[0] + e[1] + e[2]);
}
}
if !(volume > 0.0) {
return Err(self.degenerate());
}
Ok(Mixed2 { bulk, g, volume, thermal_volume })
}
/// The consistent mass matrix per displacement component, n × n: m_ij = ∫ ρ Nᵢ Nⱼ dV.
pub fn mass(&self, density: f64) -> Result<Matrix> {
let n = self.ty.nodes;
@@ -518,8 +579,18 @@ impl<'a> Plane<'a> {
/// strains the hoop direction too); incompatible modes are condensed as in the stiffness.
pub fn thermal_load(&self, d: &Elasticity, thermal: &ThermalStrain) -> Result<Vec<[f64; 2]>> {
thermal.check(self.ty.nodes)?;
let d2 = self.theory.elasticity(d)?;
let n = 2 * self.ty.nodes;
if let Some(dev) = self.deviatoric(d)? {
// ∫ Bᵀ D_dev ε_th dV + κ g (∫ tr ε_th dV) / V.
let points = self.points()?;
let mixed = self.mixed(d, &points, Some(thermal))?;
let (mut fu, _) = Self::thermal_forces(&dev, thermal, &points, None, self.ty.nodes);
for (f, g) in fu.iter_mut().zip(&mixed.g) {
*f += mixed.bulk * g * mixed.thermal_volume / mixed.volume;
}
return Ok(fu.as_chunks::<2>().0.to_vec());
}
let d2 = self.theory.elasticity(d)?;
let mut fu = if self.formulation == Formulation::IncompatibleModes {
let (full, points, modes) = self.full_stiffness(&d2)?;
let (mut fu, mut fa) = Self::thermal_forces(&d2, thermal, &points, modes.as_ref(), self.ty.nodes);
@@ -549,9 +620,21 @@ impl<'a> Plane<'a> {
if let Some(t) = thermal {
t.check(self.ty.nodes)?;
}
let d2 = self.theory.elasticity(d)?;
let deviatoric = self.deviatoric(d)?;
let d2 = match deviatoric {
Some(dev) => dev,
None => self.theory.elasticity(d)?,
};
let (full, points, modes) =
if self.formulation == Formulation::IncompatibleModes { self.full_stiffness(&d2)? } else { (Matrix::zeros(0), self.points()?, None) };
// Hybrid: the element's pressure κ (gᵀ u − ∫ tr ε_th dV) / V.
let pressure = if deviatoric.is_some() {
let m = self.mixed(d, &points, thermal)?;
let gu: f64 = m.g.iter().zip(u.iter().flatten()).map(|(g, x)| g * x).sum();
m.bulk * (gu - m.thermal_volume) / m.volume
} else {
0.0
};
// Incompatible modes: K_aa α = f_a − K_au u (f_a the thermal load on the modes).
let alpha = if modes.is_some() {
let n = 2 * self.ty.nodes;
@@ -595,10 +678,11 @@ impl<'a> Plane<'a> {
};
}
// Plane stress: D̃ has no zz row or column, so the εzz just recovered adds nothing.
let s: [f64; 4] = match eth {
let mut s: [f64; 4] = match eth {
Some(t) => std::array::from_fn(|r| (0..4).map(|c| d2[r][c] * (e[c] - t[c])).sum()),
None => std::array::from_fn(|r| (0..4).map(|c| d2[r][c] * e[c]).sum()),
};
s.iter_mut().take(3).for_each(|v| *v += pressure);
out.push(PointStress { xyz: a.x, strain: [e[0], e[1], e[2], e[3], 0.0, 0.0], stress: [s[0], s[1], s[2], s[3], 0.0, 0.0] });
}
Ok(out)
@@ -614,6 +698,32 @@ impl<'a> Plane<'a> {
}
}
/// The parts of a hybrid 2D element ([`Plane::mixed`]).
pub(crate) struct Mixed2 {
bulk: f64,
pub(crate) g: Vec<f64>,
volume: f64,
thermal_volume: f64,
}
/// g += w Bᵀ m over strain columns `cols` (u takes ∂N/∂x and the hoop N/r, v takes ∂N/∂y): the
/// divergence of each nodal motion, which the hybrid elements' volume constraint acts on.
pub(crate) fn divergence(g: &mut [f64], cols: &[Column], w: f64) {
for (pair, col) in g.as_chunks_mut::<2>().0.iter_mut().zip(cols) {
pair[0] += w * (col[0] + col[2]);
pair[1] += w * col[1];
}
}
/// k += c g gᵀ.
pub(crate) fn add_outer(k: &mut Matrix, g: &[f64], c: f64) {
for (i, gi) in g.iter().enumerate() {
for (j, gj) in g.iter().enumerate() {
k.add(i, j, c * gi * gj);
}
}
}
/// The `size` × `size` block of `m` starting at (`row`, `col`).
fn sub_block(m: &Matrix, row: usize, col: usize, size: usize) -> Matrix {
let mut out = Matrix::zeros(size);
@@ -661,6 +771,53 @@ mod tests {
const THEORIES: [Theory; 3] = [Theory::PlaneStress { thickness: 0.7 }, Theory::PlaneStrain { thickness: 1.0 }, Theory::Axisymmetric];
#[test]
fn hybrid_elements_reproduce_a_uniform_strain_and_do_not_lock() {
// u = a x, v = b y: a uniform strain (axisymmetric: the hoop strain a too), whose
// stress the mean dilatation reproduces exactly at every point.
let d = isotropic(1000.0, 0.4999).unwrap();
let (a, b) = (0.002, -0.003);
for theory in [Theory::PlaneStrain { thickness: 1.5 }, Theory::Axisymmetric] {
for kind in [ElementKind::Quad4, ElementKind::Quad8, ElementKind::Tri6] {
let x = element(kind);
let plane = Plane::new(kind, &x, Formulation::Hybrid { pressures: 1 }, theory).unwrap();
let u: Vec<[f64; 2]> = x.iter().map(|p| [a * p[0], b * p[1]]).collect();
let hoop = if theory == Theory::Axisymmetric { a } else { 0.0 };
let e = [a, b, hoop, 0.0, 0.0, 0.0];
let exact = crate::material::stress(&d, &e);
for p in plane.stresses(&d, &u).unwrap() {
for c in 0..4 {
assert!((p.stress[c] - exact[c]).abs() < 1e-9 * exact[0].abs().max(1.0), "{theory:?} {kind:?} {c}: {:?} {exact:?}", p.stress);
}
}
// K u is the forces of that stress: the stiffness agrees with the stresses.
let k = plane.stiffness(&d).unwrap();
let flat: Vec<f64> = u.iter().flatten().copied().collect();
let full = Plane::new(kind, &x, Formulation::Full, theory).unwrap().stiffness(&d).unwrap();
let (ku, kf): (Vec<f64>, Vec<f64>) = (0..flat.len())
.map(|i| (0..flat.len()).fold((0.0, 0.0), |(s, t), j| (s + k.get(i, j) * flat[j], t + full.get(i, j) * flat[j])))
.unzip();
for (p, q) in ku.iter().zip(&kf) {
assert!((p - q).abs() < 1e-8 * kf.iter().fold(0.0f64, |m, v| m.max(v.abs())), "{p} {q}");
}
}
}
// Nearly incompressible: the hybrid element's volume is one constraint, so a motion that
// keeps the element's volume (orthogonal to g) costs only shear, where the full element's
// four point constraints lock it.
let x = element(ElementKind::Quad4);
let hybrid = Plane::new(ElementKind::Quad4, &x, Formulation::Hybrid { pressures: 1 }, Theory::PlaneStrain { thickness: 1.0 }).unwrap();
let g = hybrid.mixed(&d, &hybrid.points().unwrap(), None).unwrap().g;
let raw: Vec<f64> = (0..8).map(|i| ((i * 7 % 5) as f64 - 2.0) * 0.01).collect();
let along = raw.iter().zip(&g).map(|(a, b)| a * b).sum::<f64>() / g.iter().map(|v| v * v).sum::<f64>();
let shear: Vec<f64> = raw.iter().zip(&g).map(|(a, b)| a - along * b).collect();
let energy = |f: Formulation| {
let k = Plane::new(ElementKind::Quad4, &x, f, Theory::PlaneStrain { thickness: 1.0 }).unwrap().stiffness(&d).unwrap();
(0..8).map(|i| (0..8).map(|j| shear[i] * k.get(i, j) * shear[j]).sum::<f64>()).sum::<f64>()
};
assert!(energy(Formulation::Hybrid { pressures: 1 }) < 0.01 * energy(Formulation::Full));
}
#[test]
fn rigid_motion_strains_nothing_and_matrices_are_symmetric() {
let d = isotropic(1000.0, 0.3).unwrap();
@@ -783,6 +940,9 @@ mod tests {
let on_axis = sq.map(|p| [p[0] - 1.0, p[1], 0.0]);
let e = Plane::new(ElementKind::Quad4, &on_axis, Formulation::IncompatibleModes, Theory::Axisymmetric).unwrap();
assert!(e.stiffness(&d).is_ok());
assert!(formulation(ElementKind::Quad4, &fieldcraft_model::Formulation { hybrid: true, ..Default::default() }).is_err());
let hybrid = fieldcraft_model::Formulation { hybrid: true, ..Default::default() };
assert_eq!(formulation(ElementKind::Quad4, &hybrid).unwrap(), Formulation::Hybrid { pressures: 1 });
assert!(formulation(ElementKind::Tri3, &hybrid).is_err());
assert!(formulation(ElementKind::Quad4, &fieldcraft_model::Formulation { incompatible_modes: true, ..hybrid }).is_err());
}
}
+7 -3
View File
@@ -278,8 +278,9 @@ impl Formulation {
if !kind.is_continuum() && self.incompatible_modes {
return Err(Error::Invalid(format!("incompatible modes are for solid and 2D sections, not {}", kind.name())));
}
if kind.dimension() != 3 && self.hybrid {
return Err(Error::Invalid(format!("hybrid formulations are for solid sections, not {}", kind.name())));
let volume_constrained = kind.dimension() == 3 || matches!(kind, SectionKind::PlaneStrain { .. } | SectionKind::Axisymmetric {});
if self.hybrid && !volume_constrained {
return Err(Error::Invalid(format!("hybrid formulations are for solid, plane strain and axisymmetric sections, not {}", kind.name())));
}
Ok(())
}
@@ -391,7 +392,10 @@ mod tests {
let im = Formulation { incompatible_modes: true, ..Formulation::default() };
im.validate_for(&stress).unwrap();
im.validate_for(&axi).unwrap();
assert!(Formulation { hybrid: true, ..Formulation::default() }.validate_for(&strain).is_err());
let hybrid = Formulation { hybrid: true, ..Formulation::default() };
hybrid.validate_for(&strain).unwrap();
hybrid.validate_for(&axi).unwrap();
assert!(hybrid.validate_for(&stress).is_err());
assert!(im.validate_for(&SectionKind::Shell { thickness: 1.0, offset: 0.0, points: 5 }).is_err());
}
}