Files
OneCAD-Simulation/crates/fem/src/shape.rs
T
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00

194 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Shape functions and their derivatives in natural coordinates.
//!
//! Node order is the model's (Abaqus/VTK, [`fieldcraft_model::ElementKind`]):
//! - tetrahedra on the reference element with corners (0,0,0), (1,0,0), (0,1,0), (0,0,1) and the
//! volume coordinates L₁ = 1 − ξ − η − ζ, L₂ = ξ, L₃ = η, L₄ = ζ; tet10 mid-side nodes on edges
//! 1-2, 2-3, 3-1, 1-4, 2-4, 3-4 with N = 4 Lₐ L_b, corners N = L(2L − 1);
//! - hexahedra on [−1, 1]³, corners 1–4 at ζ = −1 counter-clockwise from (−1,−1), 5–8 above
//! them; hex20 is the serendipity element (corners
//! N = ⅛(1 + ξξᵢ)(1 + ηηᵢ)(1 + ζζᵢ)(ξξᵢ + ηηᵢ + ζζᵢ − 2), a mid-side node with ξᵢ = 0
//! N = ¼(1 − ξ²)(1 + ηηᵢ)(1 + ζζᵢ), and likewise), mid-side nodes 9–12 on the bottom edges,
//! 13–16 on the top edges, 17–20 on the vertical edges (mid-side positions come from
//! [`fieldcraft_model::ElementKind::edges`], the one place the node order is written down);
//! - the six-node wedge (pentahedron) as the product of the linear triangle {ξ, η ≥ 0,
//! ξ + η ≤ 1} (area coordinates L₁ = 1 − ξ − η, L₂ = ξ, L₃ = η) and the linear interval
//! ζ ∈ [−1, 1]: nodes 1–3 at ζ = −1 counter-clockwise seen from above, nodes 4–6 above them,
//! Nᵢ = Lᵢ (1 − ζ)/2 and Nᵢ₊₃ = Lᵢ (1 + ζ)/2.
//!
//! The functions are the textbook ones (O. C. Zienkiewicz, R. L. Taylor, J. Z. Zhu, *The Finite
//! Element Method: Its Basis and Fundamentals*, 7th ed., Butterworth-Heinemann 2013, ch. 6;
//! T. J. R. Hughes, *The Finite Element Method*, Dover 2000, ch. 3).
use fieldcraft_model::ElementKind;
/// Most nodes an element family has (hex27, [`crate::basis::Basis::Hex27`]).
pub const MAX_NODES: usize = 27;
/// Shape function values and natural derivatives at one point.
#[derive(Debug, Clone, Copy)]
pub struct Shape {
pub count: usize,
pub n: [f64; MAX_NODES],
/// ∂Nᵢ/∂ξ, ∂Nᵢ/∂η, ∂Nᵢ/∂ζ.
pub dn: [[f64; 3]; MAX_NODES],
}
impl Shape {
fn new(count: usize) -> Shape {
Shape { count, n: [0.0; MAX_NODES], dn: [[0.0; 3]; MAX_NODES] }
}
pub fn values(&self) -> &[f64] {
self.n.get(..self.count).unwrap_or_default()
}
pub fn derivatives(&self) -> &[[f64; 3]] {
self.dn.get(..self.count).unwrap_or_default()
}
}
/// Natural coordinates of the hexahedron's corners.
pub const HEX_CORNERS: [[f64; 3]; 8] =
[[-1., -1., -1.], [1., -1., -1.], [1., 1., -1.], [-1., 1., -1.], [-1., -1., 1.], [1., -1., 1.], [1., 1., 1.], [-1., 1., 1.]];
/// Natural coordinates of the wedge's corners.
pub const WEDGE_CORNERS: [[f64; 3]; 6] = [[0., 0., -1.], [1., 0., -1.], [0., 1., -1.], [0., 0., 1.], [1., 0., 1.], [0., 1., 1.]];
/// Natural coordinates of the tetrahedron's corners.
pub const TET_CORNERS: [[f64; 3]; 4] = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]];
/// Four-node tetrahedron.
pub fn tet4(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(4);
s.n[0] = 1.0 - xi[0] - xi[1] - xi[2];
s.n[1] = xi[0];
s.n[2] = xi[1];
s.n[3] = xi[2];
s.dn[0] = [-1.0, -1.0, -1.0];
s.dn[1] = [1.0, 0.0, 0.0];
s.dn[2] = [0.0, 1.0, 0.0];
s.dn[3] = [0.0, 0.0, 1.0];
s
}
/// Ten-node tetrahedron.
pub fn tet10(xi: [f64; 3]) -> Shape {
let lin = tet4(xi);
let l = [lin.n[0], lin.n[1], lin.n[2], lin.n[3]];
let dl = [lin.dn[0], lin.dn[1], lin.dn[2], lin.dn[3]];
let mut s = Shape::new(10);
for i in 0..4 {
s.n[i] = l[i] * (2.0 * l[i] - 1.0);
s.dn[i] = dl[i].map(|d| (4.0 * l[i] - 1.0) * d);
}
// Mid-side nodes on the model's tet10 edges.
for edge in ElementKind::Tet10.edges() {
if let [a, b, m] = **edge {
s.n[m] = 4.0 * l[a] * l[b];
s.dn[m] = [0, 1, 2].map(|c| 4.0 * (dl[a][c] * l[b] + l[a] * dl[b][c]));
}
}
s
}
/// Eight-node hexahedron (trilinear).
pub fn hex8(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(8);
for (i, c) in HEX_CORNERS.iter().enumerate() {
let f = [0, 1, 2].map(|k| 1.0 + xi[k] * c[k]);
s.n[i] = f[0] * f[1] * f[2] / 8.0;
s.dn[i] = [c[0] * f[1] * f[2] / 8.0, c[1] * f[0] * f[2] / 8.0, c[2] * f[0] * f[1] / 8.0];
}
s
}
/// Six-node wedge: the linear triangle times the linear interval.
pub fn wedge6(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(6);
let l = [1.0 - xi[0] - xi[1], xi[0], xi[1]];
let dl = [[-1.0, -1.0], [1.0, 0.0], [0.0, 1.0]];
for (h, f, df) in [(0, (1.0 - xi[2]) / 2.0, -0.5), (3, (1.0 + xi[2]) / 2.0, 0.5)] {
for i in 0..3 {
s.n[h + i] = l[i] * f;
s.dn[h + i] = [dl[i][0] * f, dl[i][1] * f, l[i] * df];
}
}
s
}
/// Twenty-node serendipity hexahedron.
pub fn hex20(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(20);
let [x, y, z] = xi;
for (i, c) in HEX_CORNERS.iter().enumerate() {
let f = [0, 1, 2].map(|k| 1.0 + xi[k] * c[k]);
let g = x * c[0] + y * c[1] + z * c[2] - 2.0;
s.n[i] = f[0] * f[1] * f[2] * g / 8.0;
// ∂/∂ξ_k: c_k f_l f_m g / 8 + f_0 f_1 f_2 c_k / 8.
s.dn[i] = [c[0] * f[1] * f[2] * (g + f[0]) / 8.0, c[1] * f[0] * f[2] * (g + f[1]) / 8.0, c[2] * f[0] * f[1] * (g + f[2]) / 8.0];
}
// Mid-side nodes on the model's hex20 edges, at the midpoints of their corners.
for edge in ElementKind::Hex20.edges() {
let [a, b, i] = **edge else { continue };
let c = [0, 1, 2].map(|k| (HEX_CORNERS[a][k] + HEX_CORNERS[b][k]) / 2.0);
// The direction along the edge has cᵢ = 0: N = ¼(1 − t²)(1 + a aᵢ)(1 + b bᵢ).
let along = if c[0] == 0.0 {
0
} else if c[1] == 0.0 {
1
} else {
2
};
let (p, q) = ((along + 1) % 3, (along + 2) % 3);
let t = xi[along];
let fp = 1.0 + xi[p] * c[p];
let fq = 1.0 + xi[q] * c[q];
s.n[i] = (1.0 - t * t) * fp * fq / 4.0;
let mut d = [0.0; 3];
d[along] = -2.0 * t * fp * fq / 4.0;
d[p] = (1.0 - t * t) * c[p] * fq / 4.0;
d[q] = (1.0 - t * t) * fp * c[q] / 4.0;
s.dn[i] = d;
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kronecker_delta_partition_of_unity_and_derivatives() {
let kinds: [(&str, fn([f64; 3]) -> Shape); 5] = [("tet4", tet4), ("tet10", tet10), ("wedge6", wedge6), ("hex8", hex8), ("hex20", hex20)];
for (name, f) in kinds {
let nodes = crate::Basis::ALL.iter().find(|b| b.name() == name).map(|b| b.nodes()).unwrap_or_default();
assert!(!nodes.is_empty());
for (i, p) in nodes.iter().enumerate() {
let s = f(*p);
for (j, v) in s.values().iter().enumerate() {
assert!((v - if i == j { 1.0 } else { 0.0 }).abs() < 1e-14, "{name} N{j} at node {i}: {v}");
}
}
// At an arbitrary point: ΣN = 1, ΣdN = 0, Σ N x = x (linear completeness), and the
// derivatives match central differences.
let p = [0.21, 0.17, 0.33];
let s = f(p);
assert!((s.values().iter().sum::<f64>() - 1.0).abs() < 1e-14);
for k in 0..3 {
assert!(s.derivatives().iter().map(|d| d[k]).sum::<f64>().abs() < 1e-13);
let x: f64 = s.values().iter().zip(nodes).map(|(n, x)| n * x[k]).sum();
assert!((x - p[k]).abs() < 1e-14, "{name}");
let h = 1e-6;
let (mut a, mut b) = (p, p);
a[k] += h;
b[k] -= h;
let (sa, sb) = (f(a), f(b));
for j in 0..s.count {
let fd = (sa.n[j] - sb.n[j]) / (2.0 * h);
assert!((fd - s.dn[j][k]).abs() < 1e-8, "{name} dN{j}/dξ{k}: {fd} vs {}", s.dn[j][k]);
}
}
}
}
}