forked from 1b-agent/fieldcraft-upstream
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
258 lines
12 KiB
Rust
258 lines
12 KiB
Rust
//! Structured meshes generated in code for the analytic verification suite: a block of
|
||
//! hexahedra (hex8, hex20), of wedges (two per cell) or of Kuhn-split tetrahedra (tet4, tet10) on [0, 1]³, mapped to the part
|
||
//! by a function, and helpers to define the model on it.
|
||
#![allow(dead_code)]
|
||
|
||
use std::collections::BTreeMap;
|
||
|
||
use fieldcraft_model::{
|
||
BoundaryCondition, Coupling, CouplingKind, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load, Material, Model, Procedure,
|
||
ReferencePoint, Section, SectionKind, Set, Step, Symmetry, Target,
|
||
};
|
||
|
||
pub struct Grid {
|
||
pub model: Model,
|
||
/// Parametric coordinates of every node.
|
||
pub param: BTreeMap<u32, [f64; 3]>,
|
||
pub kind: ElementKind,
|
||
}
|
||
|
||
/// A structured mesh with `n` cells per direction. Nodes live on the doubled lattice
|
||
/// (2n + 1 points per direction) so quadratic elements get their mid-side nodes from the map
|
||
/// too (curved edges follow it).
|
||
pub fn grid(n: [usize; 3], kind: ElementKind, 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 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]];
|
||
match kind {
|
||
ElementKind::Hex8 | ElementKind::Hex20 => {
|
||
let corners = [[0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0], [0, 0, 2], [2, 0, 2], [2, 2, 2], [0, 2, 2]];
|
||
let mut nodes: Vec<u32> = corners.iter().map(|c| node(&mut model, at(*c))).collect();
|
||
if kind == ElementKind::Hex20 {
|
||
for e in kind.edges() {
|
||
let (a, b) = (corners[e[0]], corners[e[1]]);
|
||
nodes.push(node(&mut model, at([0, 1, 2].map(|k| (a[k] + b[k]) / 2))));
|
||
}
|
||
}
|
||
model.set_element(next, kind, nodes).unwrap();
|
||
next += 1;
|
||
}
|
||
ElementKind::Wedge6 => {
|
||
// Two prisms along z, split by the cell's (x, y) diagonal, both counter-clockwise from above.
|
||
for tri in [[[0, 0], [2, 0], [2, 2]], [[0, 0], [2, 2], [0, 2]]] {
|
||
let nodes: Vec<u32> =
|
||
[0, 2].iter().flat_map(|&z| tri.map(|t| [t[0], t[1], z])).map(|c| node(&mut model, at(c))).collect();
|
||
model.set_element(next, kind, nodes).unwrap();
|
||
next += 1;
|
||
}
|
||
}
|
||
_ => {
|
||
// Six tetrahedra around the cell's main diagonal (Kuhn), conforming across cells.
|
||
for perm in [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] {
|
||
let mut v = [[0usize; 3]; 4];
|
||
for s in 0..3 {
|
||
v[s + 1] = v[s];
|
||
v[s + 1][perm[s]] = 2;
|
||
}
|
||
// Orientation from the parametric volume.
|
||
let p = v.map(|c| c.map(|x| x as f64));
|
||
let e = |i: usize| [0, 1, 2].map(|k| p[i][k] - p[0][k]);
|
||
let (a, b, c) = (e(1), e(2), e(3));
|
||
let vol = a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0]);
|
||
if vol < 0.0 {
|
||
v.swap(1, 2);
|
||
}
|
||
let mut nodes: Vec<u32> = v.iter().map(|c| node(&mut model, at(*c))).collect();
|
||
if kind == ElementKind::Tet10 {
|
||
for e in kind.edges() {
|
||
let (a, b) = (v[e[0]], v[e[1]]);
|
||
nodes.push(node(&mut model, at([0, 1, 2].map(|k| (a[k] + b[k]) / 2))));
|
||
}
|
||
}
|
||
model.set_element(next, kind, nodes).unwrap();
|
||
next += 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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
|
||
}
|
||
|
||
impl Grid {
|
||
pub fn nodes_where(&self, f: impl Fn([f64; 3]) -> bool) -> Vec<u32> {
|
||
self.param.iter().filter(|(_, p)| f(**p)).map(|(l, _)| *l).collect()
|
||
}
|
||
|
||
/// Element faces whose nodes all satisfy `f` (on parametric coordinates).
|
||
pub fn faces_where(&self, f: impl Fn([f64; 3]) -> bool) -> Vec<ElementFace> {
|
||
let mut out = Vec::new();
|
||
for (label, e) in self.model.elements() {
|
||
for (i, face) in e.kind.faces().iter().enumerate() {
|
||
if face.iter().all(|k| f(self.param[&e.nodes[*k]])) {
|
||
out.push(ElementFace { element: *label, face: i as u8 + 1 });
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Material, section over every element and an empty step of the given procedure.
|
||
pub fn define(&mut self, elastic: Elastic, density: Option<f64>, formulation: Formulation, procedure: Procedure) {
|
||
let all: Vec<u32> = self.model.elements().keys().copied().collect();
|
||
self.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||
self.model.add_material("Mat", Material { elastic: Some(elastic), density, ..Material::default() }).unwrap();
|
||
self.model.add_section("solid", Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::Solid {}, formulation }).unwrap();
|
||
self.model.add_step(Step::new("Step", procedure), None).unwrap();
|
||
}
|
||
|
||
pub fn node_set(&mut self, name: &str, f: impl Fn([f64; 3]) -> bool) {
|
||
let nodes = self.nodes_where(f);
|
||
self.model.add_set(name, Set { nodes, ..Set::default() }).unwrap();
|
||
}
|
||
|
||
pub fn face_set(&mut self, name: &str, f: impl Fn([f64; 3]) -> bool) {
|
||
let faces = self.faces_where(f);
|
||
self.model.add_set(name, Set { faces, ..Set::default() }).unwrap();
|
||
}
|
||
|
||
pub fn bc(&mut self, name: &str, bc: BoundaryCondition) {
|
||
self.model.add_boundary_condition(name, bc).unwrap();
|
||
self.model.set_boundary_condition_active("Step", name, true).unwrap();
|
||
}
|
||
|
||
pub fn load(&mut self, name: &str, load: Load) {
|
||
self.model.add_load(name, load).unwrap();
|
||
self.model.set_load_active("Step", name, true).unwrap();
|
||
}
|
||
|
||
pub fn symmetry(&mut self, name: &str, plane: Symmetry, f: impl Fn([f64; 3]) -> bool) {
|
||
self.node_set(name, f);
|
||
self.bc(name, BoundaryCondition::Symmetry { target: Target::Set(name.into()), plane });
|
||
}
|
||
|
||
/// A reference point `name` at `xyz`, coupled to the nodes where `f` holds.
|
||
pub fn couple(&mut self, name: &str, xyz: [f64; 3], kind: CouplingKind, dofs: &[u8], f: impl Fn([f64; 3]) -> bool) {
|
||
let set = format!("{name}_nodes");
|
||
self.node_set(&set, f);
|
||
self.model.add_reference_point(name, ReferencePoint { xyz, label: 0 }).unwrap();
|
||
let dofs = Dofs::try_from(dofs.to_vec()).unwrap();
|
||
self.model.add_coupling(&format!("{name}_coupling"), Coupling { reference_point: name.into(), set, kind, dofs }).unwrap();
|
||
}
|
||
|
||
pub fn fixed(&mut self, name: &str, f: impl Fn([f64; 3]) -> bool) {
|
||
self.node_set(name, f);
|
||
self.bc(name, BoundaryCondition::Fixed { target: Target::Set(name.into()) });
|
||
}
|
||
}
|
||
|
||
pub fn iso(young: f64, poisson: f64) -> Elastic {
|
||
Elastic::Isotropic { young, poisson }
|
||
}
|
||
|
||
pub fn full() -> Formulation {
|
||
Formulation::default()
|
||
}
|
||
|
||
pub fn reduced() -> Formulation {
|
||
Formulation { integration: Integration::Reduced, ..Formulation::default() }
|
||
}
|
||
|
||
pub fn incompatible() -> Formulation {
|
||
Formulation { incompatible_modes: true, ..Formulation::default() }
|
||
}
|
||
|
||
/// The element kinds and formulations under test.
|
||
pub fn variants() -> Vec<(ElementKind, Formulation, &'static str)> {
|
||
vec![
|
||
(ElementKind::Tet4, full(), "tet4"),
|
||
(ElementKind::Tet10, full(), "tet10"),
|
||
(ElementKind::Wedge6, full(), "wedge6"),
|
||
(ElementKind::Hex8, full(), "hex8"),
|
||
(ElementKind::Hex8, incompatible(), "hex8 incompatible"),
|
||
(ElementKind::Hex20, full(), "hex20"),
|
||
(ElementKind::Hex20, reduced(), "hex20 reduced"),
|
||
]
|
||
}
|