fem: pyramid5 element (solver, quality, decks, VTK)

A five-node pyramid, the conforming transition between a hexahedron's face
and tetrahedra: Bedrosian's rational shape functions with a collapsed (Duffy)
Gauss rule, a certified Jacobian on the base square with exact volume, a
scaled Jacobian, C3D5 / CPYRAM / LS-DYNA degenerate solid, VTK_PYRAMID and
the binary mesh code 13. A distorted hex8-pyramid5-tet4 transition mesh
passes the patch test and uniaxial tension exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 12:48:01 -04:00
1 parent 4e6fd59409
commit 3a5a8213a1
28 files changed
+325 -28

No files matched your search

+1
View File
@@ -207,6 +207,7 @@ impl Basis {
ElementKind::Tet4 => Basis::Tet4,
ElementKind::Tet10 => Basis::Tet10,
ElementKind::Wedge6 => Basis::Wedge6,
ElementKind::Pyramid5 => Basis::Pyramid5,
ElementKind::Hex8 => Basis::Hex8,
ElementKind::Hex20 => Basis::Hex20,
}
+15 -2
View File
@@ -47,6 +47,7 @@ use crate::{Error, Result};
pub enum Family {
Tetrahedron,
Wedge,
Pyramid,
Hexahedron,
/// The 2D elements of plane and axisymmetric models ([`crate::plane`]).
Triangle,
@@ -74,7 +75,7 @@ pub struct ElementType {
}
/// The element table.
pub static ELEMENT_TYPES: [ElementType; 5] = [
pub static ELEMENT_TYPES: [ElementType; 6] = [
ElementType {
kind: ElementKind::Tet4,
family: Family::Tetrahedron,
@@ -108,6 +109,17 @@ pub static ELEMENT_TYPES: [ElementType; 5] = [
mass: Rule::Wedge,
incompatible_modes: false,
},
ElementType {
kind: ElementKind::Pyramid5,
family: Family::Pyramid,
nodes: 5,
corners: 5,
shape: shape::pyramid5,
full: Rule::Pyramid(2),
reduced: None,
mass: Rule::Pyramid(3),
incompatible_modes: false,
},
ElementType {
kind: ElementKind::Hex8,
family: Family::Hexahedron,
@@ -137,7 +149,7 @@ pub fn element_type(kind: ElementKind) -> Result<&'static ElementType> {
ELEMENT_TYPES
.iter()
.find(|t| t.kind == kind)
.ok_or_else(|| Error::NotSupported(format!("{} elements (the solver has tet4, tet10, wedge6, hex8 and hex20 so far)", kind.name())))
.ok_or_else(|| Error::NotSupported(format!("{} elements (the solver has tet4, tet10, wedge6, pyramid5, hex8 and hex20 so far)", kind.name())))
}
/// How a solid element is formulated.
@@ -730,6 +742,7 @@ pub fn extrapolation(ty: &ElementType, rule: Rule) -> Result<Extrapolation> {
let corner_basis = |xi: [f64; 3]| match ty.family {
Family::Tetrahedron => shape::tet4(xi),
Family::Wedge => shape::wedge6(xi),
Family::Pyramid => shape::pyramid5(xi),
Family::Hexahedron => shape::hex8(xi),
Family::Triangle => Basis::Tri3.eval(xi),
Family::Quadrilateral => Basis::Quad4.eval(xi),
+9 -3
View File
@@ -833,9 +833,15 @@ mod tests {
let x8: Vec<[f64; 3]> = x20[..8].to_vec();
let x6 = vec![[0., 0., 0.], [1., 0., 0.], [0., 1., 0.1], [0., 0., 1.], [1.1, 0., 1.], [0., 1., 1.2]];
let x4 = vec![x10[0], x10[1], x10[2], x10[3]];
for (kind, x) in
[(ElementKind::Tet4, &x4), (ElementKind::Tet10, &x10), (ElementKind::Wedge6, &x6), (ElementKind::Hex8, &x8), (ElementKind::Hex20, &x20)]
{
let x5 = vec![[0., 0., 0.], [1., 0., 0.1], [1.1, 1., 0.], [0., 1., 0.], [0.5, 0.4, 0.9]];
for (kind, x) in [
(ElementKind::Tet4, &x4),
(ElementKind::Tet10, &x10),
(ElementKind::Wedge6, &x6),
(ElementKind::Pyramid5, &x5),
(ElementKind::Hex8, &x8),
(ElementKind::Hex20, &x20),
] {
for m in [&svk, &neo] {
let solid = Solid::new(kind, x, Formulation::Full).unwrap();
check_tangent(&solid, m, &displacement(x.len(), 0.08));
+32
View File
@@ -66,6 +66,10 @@ pub enum Rule {
/// Three triangle points times two Gauss points in ζ on the reference wedge: point k of each
/// layer sits near corner k, the lower layer first.
Wedge,
/// The collapsed (Duffy) product on the reference pyramid, n points per direction: Gauss–Legendre
/// in u, v ∈ [−1, 1] and w ∈ [0, 1], mapped by ξ = u(1 − w), η = v(1 − w), ζ = w with the
/// weight times (1 − w)², which makes the pyramid's rational functions polynomials in (u, v, w).
Pyramid(u8),
}
impl Rule {
@@ -125,6 +129,19 @@ impl Rule {
let tri = [[b, b], [a, b], [b, a]];
gauss_legendre(2).iter().flat_map(|&(z, wz)| tri.iter().map(move |t| Point { xi: [t[0], t[1], z], weight: wz / 6.0 })).collect()
}
Rule::Pyramid(n) => {
let g = gauss_legendre(usize::from(n));
let mut out = Vec::with_capacity(g.len().pow(3));
for &(z, wz) in g {
let (w, ww) = ((z + 1.0) / 2.0, wz / 2.0);
for &(v, wv) in g {
for &(u, wu) in g {
out.push(Point { xi: [u * (1.0 - w), v * (1.0 - w), w], weight: wu * wv * ww * (1.0 - w) * (1.0 - w) });
}
}
}
out
}
}
}
}
@@ -182,6 +199,21 @@ mod tests {
}
}
}
// Pyramid: ∫ ξ^a η^b ζ^c over the pyramid of base [−1, 1]² at ζ = 0 and apex (0, 0, 1)
// = ∫_0^1 ζ^c ∫ ξ^a ∫ η^b over the square of half-width 1 − ζ: for even a, b,
// 4/((a+1)(b+1)) ∫_0^1 ζ^c (1 − ζ)^(a+b+2) = 4/((a+1)(b+1)) · B(c + 1, a + b + 3).
let beta = |p: i32, q: i32| f(p - 1) * f(q - 1) / f(p + q - 1);
// Three points per direction are exact to degree 5 in w, i.e. a + b + c ≤ 3.
for a in 0..=3 {
for b in 0..=3 - a {
for c in 0..=3 - a - b {
let exact = if a % 2 == 1 || b % 2 == 1 { 0.0 } else { 4.0 / f64::from((a + 1) * (b + 1)) * beta(c + 1, a + b + 3) };
let got = integrate(Rule::Pyramid(3), a, b, c);
assert!((got - exact).abs() < 1e-14, "pyramid ξ^{a} η^{b} ζ^{c}: {got} vs {exact}");
}
}
}
assert!((integrate(Rule::Pyramid(2), 0, 0, 0) - 4.0 / 3.0).abs() < 1e-15);
// Triangle: ∫ ξ^a η^b = a! b! / (a + b + 2)!.
for (rule, degree) in [(Rule::TriConical(3), 4), (Rule::TriConical(4), 6), (Rule::TriCentroid, 1), (Rule::Tri3, 2)] {
for a in 0..=degree {
+7
View File
@@ -116,6 +116,13 @@ pub fn wedge6(xi: [f64; 3]) -> Shape {
s
}
/// Five-node pyramid: base [−1, 1]² at ζ = 0, apex (0, 0, 1), the rational functions of
/// [`crate::basis`] (Bedrosian 1992), conforming with tetrahedra on its triangles and with
/// hexahedra on its base.
pub fn pyramid5(xi: [f64; 3]) -> Shape {
crate::basis::Basis::Pyramid5.eval(xi)
}
/// Twenty-node serendipity hexahedron.
pub fn hex20(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(20);
+4 -1
View File
@@ -13,6 +13,7 @@ fn distorted(kind: ElementKind, curved: bool) -> Vec<[f64; 3]> {
let corners: Vec<[f64; 3]> = match kind {
ElementKind::Tet4 | ElementKind::Tet10 => vec![[0.1, -0.05, 0.0], [1.2, 0.1, 0.05], [0.2, 0.9, -0.1], [0.05, 0.15, 1.1]],
ElementKind::Wedge6 => vec![[0.0, 0.0, 0.0], [2.1, 0.1, -0.1], [0.2, 1.3, 0.05], [0.1, -0.1, 0.9], [2.0, 0.05, 1.1], [0.15, 1.2, 1.0]],
ElementKind::Pyramid5 => vec![[0.0, 0.0, 0.0], [2.1, 0.1, -0.1], [2.3, 1.2, 0.05], [-0.1, 0.9, 0.1], [0.9, 0.6, 1.3]],
_ => vec![
[0.0, 0.0, 0.0],
[2.1, 0.1, -0.1],
@@ -34,10 +35,11 @@ fn distorted(kind: ElementKind, curved: bool) -> Vec<[f64; 3]> {
xyz
}
const CASES: [(ElementKind, Formulation); 10] = [
const CASES: [(ElementKind, Formulation); 11] = [
(ElementKind::Tet4, Formulation::Full),
(ElementKind::Tet10, Formulation::Full),
(ElementKind::Wedge6, Formulation::Full),
(ElementKind::Pyramid5, Formulation::Full),
(ElementKind::Hex8, Formulation::Full),
(ElementKind::Hex8, Formulation::IncompatibleModes),
(ElementKind::Hex20, Formulation::Full),
@@ -156,6 +158,7 @@ fn extrapolation_reproduces_linear_fields_on_affine_elements() {
let mut xyz: Vec<[f64; 3]> = match kind {
ElementKind::Tet10 => vec![[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
ElementKind::Wedge6 => vec![[0., 0., 0.], [2., 0., 0.], [0.5, 1., 0.], [0., 0.2, 1.], [2., 0.2, 1.], [0.5, 1.2, 1.]],
ElementKind::Pyramid5 => vec![[0., 0., 0.], [2., 0., 0.], [2.5, 1., 0.], [0.5, 1., 0.], [1.2, 0.5, 1.]],
_ => vec![[0., 0., 0.], [2., 0., 0.], [2.5, 1., 0.], [0.5, 1., 0.], [0., 0., 1.], [2., 0., 1.], [2.5, 1., 1.], [0.5, 1., 1.]],
};
let c = xyz.clone();