forked from 1b-agent/fieldcraft-upstream
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:
1 parent
4e6fd59409
commit
3a5a8213a1
28 files changed
+325
-28
No files matched your search
@@ -97,6 +97,65 @@ pub fn grid(n: [usize; 3], kind: ElementKind, map: impl Fn([f64; 3]) -> [f64; 3]
|
||||
Grid { model, param, kind }
|
||||
}
|
||||
|
||||
/// A hex–tet transition mesh on [0, 1]³ with `n` cells per direction: the cells with x below
|
||||
/// one half are hex8, every other cell is split from its centre into six pyramids over its faces,
|
||||
/// and each pyramid whose face is not shared with a hex cell into two tetrahedra (split along
|
||||
/// the diagonal through the face's lowest node label, so neighbours agree). The mesh is
|
||||
/// conforming: hexahedra meet pyramids on whole quadrilaterals, everything else on triangles.
|
||||
pub fn transition_grid(n: [usize; 3], map: impl Fn([f64; 3]) -> [f64; 3]) -> Grid {
|
||||
let size = n.map(|k| 2 * k + 1);
|
||||
let label = |i: [usize; 3]| (1 + i[0] + size[0] * (i[1] + size[1] * i[2])) as u32;
|
||||
let param_of = |i: [usize; 3]| [0, 1, 2].map(|k| i[k] as f64 / (2 * n[k]) as f64);
|
||||
let mut model = Model::default();
|
||||
let mut param = BTreeMap::new();
|
||||
let mut node = |model: &mut Model, i: [usize; 3]| -> u32 {
|
||||
let l = label(i);
|
||||
if let std::collections::btree_map::Entry::Vacant(slot) = param.entry(l) {
|
||||
let p = param_of(i);
|
||||
model.set_node(l, map(p)).unwrap();
|
||||
slot.insert(p);
|
||||
}
|
||||
l
|
||||
};
|
||||
let is_hex = |c: [isize; 3]| (0..3).all(|k| c[k] >= 0 && (c[k] as usize) < n[k]) && 2 * c[0] < n[0] as isize;
|
||||
const CORNERS: [[usize; 3]; 8] = [[0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0], [0, 0, 2], [2, 0, 2], [2, 2, 2], [0, 2, 2]];
|
||||
// Hex8's faces (outward) and the cell across each.
|
||||
let across: [[isize; 3]; 6] = [[0, 0, -1], [0, 0, 1], [0, -1, 0], [1, 0, 0], [0, 1, 0], [-1, 0, 0]];
|
||||
let mut next = 1u32;
|
||||
for cz in 0..n[2] {
|
||||
for cy in 0..n[1] {
|
||||
for cx in 0..n[0] {
|
||||
let base = [2 * cx, 2 * cy, 2 * cz];
|
||||
let at = |d: [usize; 3]| [base[0] + d[0], base[1] + d[1], base[2] + d[2]];
|
||||
let cell = [cx as isize, cy as isize, cz as isize];
|
||||
let corners: Vec<u32> = CORNERS.iter().map(|c| node(&mut model, at(*c))).collect();
|
||||
if is_hex(cell) {
|
||||
model.set_element(next, ElementKind::Hex8, corners).unwrap();
|
||||
next += 1;
|
||||
continue;
|
||||
}
|
||||
let apex = node(&mut model, at([1, 1, 1]));
|
||||
for (face, d) in ElementKind::Hex8.faces().iter().zip(across) {
|
||||
// Reversed, the outward face's normal points into the cell, at the apex.
|
||||
let q: Vec<u32> = face.iter().rev().map(|&i| corners[i]).collect();
|
||||
if is_hex([cell[0] + d[0], cell[1] + d[1], cell[2] + d[2]]) {
|
||||
model.set_element(next, ElementKind::Pyramid5, vec![q[0], q[1], q[2], q[3], apex]).unwrap();
|
||||
next += 1;
|
||||
continue;
|
||||
}
|
||||
let lowest = (0..4).min_by_key(|&i| q[i]).unwrap();
|
||||
let (a, b, c, e) = if lowest % 2 == 0 { (q[0], q[1], q[2], q[3]) } else { (q[1], q[2], q[3], q[0]) };
|
||||
for tet in [[a, b, c, apex], [a, c, e, apex]] {
|
||||
model.set_element(next, ElementKind::Tet4, tet.to_vec()).unwrap();
|
||||
next += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Grid { model, param, kind: ElementKind::Pyramid5 }
|
||||
}
|
||||
|
||||
pub fn near(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < 1e-9
|
||||
}
|
||||
|
||||
@@ -77,6 +77,56 @@ fn patch_test_every_element() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The patch test and uniaxial tension on a conforming hex8–pyramid5–tet4 transition mesh
|
||||
/// ([`transition_grid`]), distorted inside: the linear field is reproduced exactly, so the three
|
||||
/// kinds are compatible across their shared faces.
|
||||
#[test]
|
||||
fn hex_pyramid_tet_transition_passes_the_patch_test() {
|
||||
let a = [[1e-3, 2e-4, -3e-4], [5e-4, -2e-3, 1e-4], [-1e-4, 3e-4, 7e-4]];
|
||||
let field = |x: [f64; 3]| [0, 1, 2].map(|i| (0..3).map(|j| a[i][j] * x[j]).sum::<f64>());
|
||||
let map = |p: [f64; 3]| {
|
||||
let b = (PI * p[0]).sin() * (PI * p[1]).sin() * (PI * p[2]).sin();
|
||||
[p[0] + 0.08 * b, p[1] - 0.06 * b, p[2] + 0.07 * b]
|
||||
};
|
||||
let mut g = transition_grid([4, 2, 2], map);
|
||||
let kinds: std::collections::BTreeSet<&str> = g.model.elements().values().map(|e| e.kind.name()).collect();
|
||||
assert_eq!(kinds.into_iter().collect::<Vec<_>>(), ["hex8", "pyramid5", "tet4"]);
|
||||
g.define(iso(1000.0, 0.25), None, full(), Procedure::Static {});
|
||||
let on_boundary = |p: [f64; 3]| p.iter().any(|c| near(*c, 0.0) || near(*c, 1.0));
|
||||
for (i, label) in g.nodes_where(on_boundary).into_iter().enumerate() {
|
||||
let u = field(g.model.node(label).unwrap().xyz);
|
||||
let set = format!("b{i}");
|
||||
g.model.add_set(&set, fieldcraft_model::Set { nodes: vec![label], ..Default::default() }).unwrap();
|
||||
g.bc(
|
||||
&set,
|
||||
BoundaryCondition::Displacement { target: Target::Set(set.clone()), values: [Some(u[0]), Some(u[1]), Some(u[2]), None, None, None] },
|
||||
);
|
||||
}
|
||||
let r = static_solve(&g);
|
||||
let mut worst: f64 = 0.0;
|
||||
for (label, u) in r.nodes.iter().zip(&r.displacement) {
|
||||
let want = field(g.model.node(*label).unwrap().xyz);
|
||||
worst = worst.max((0..3).map(|k| (u[k] - want[k]).abs()).fold(0.0, f64::max));
|
||||
}
|
||||
eprintln!("transition patch: displacement error {worst:.1e}");
|
||||
assert!(worst < 1e-12, "{worst}");
|
||||
|
||||
// Uniaxial tension with symmetry planes: σxx = p and ux = p x / E everywhere.
|
||||
let (young, nu, p) = (2000.0, 0.3, 5.0);
|
||||
let mut g = transition_grid([4, 2, 2], map);
|
||||
g.define(iso(young, nu), None, full(), Procedure::Static {});
|
||||
g.symmetry("x0", Symmetry::X, |q| near(q[0], 0.0));
|
||||
g.symmetry("y0", Symmetry::Y, |q| near(q[1], 0.0));
|
||||
g.symmetry("z0", Symmetry::Z, |q| near(q[2], 0.0));
|
||||
g.face_set("end", |q| near(q[0], 1.0));
|
||||
g.load("pull", Load::Pressure { set: "end".into(), magnitude: -p });
|
||||
let r = static_solve(&g);
|
||||
let worst = r.nodes.iter().zip(&r.displacement).map(|(l, u)| (u[0] - p * g.model.node(*l).unwrap().xyz[0] / young).abs()).fold(0.0, f64::max);
|
||||
let s_err = r.stress.iter().map(|s| (s[0] - p).abs()).fold(0.0, f64::max);
|
||||
eprintln!("transition tension: displacement error {worst:.1e}, stress error {s_err:.1e}");
|
||||
assert!(worst < 1e-11 && s_err < 1e-9, "{worst} {s_err}");
|
||||
}
|
||||
|
||||
/// Uniaxial tension by a consistent surface load on a distorted block with symmetry planes:
|
||||
/// σxx = p exactly, ux = p L / E, the lateral contraction −ν p y / E, and the reactions balance
|
||||
/// the load (the force form of the patch test, and a check of pressure loads).
|
||||
|
||||
Reference in new issue
Block a user