Files
OneCAD-Simulation/crates/fem/src/basis.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

764 lines
31 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.
//! The shape-function library: every element family's interpolation with its first and second
//! derivatives in natural coordinates, and their mapping to physical gradients, Hessians and
//! Laplacians through the isoparametric map.
//!
//! Families ([`Basis`]) and their reference elements (natural coordinates ξ, η, ζ; unused ones
//! are zero):
//! - **lines** on ξ ∈ [−1, 1]: line2 (ends at ξ = −1, 1), line3 (then the middle node at 0);
//! - **triangles** on {ξ, η ≥ 0, ξ + η ≤ 1}: tri3 at (0,0), (1,0), (0,1), area coordinates
//! L₁ = 1 − ξ − η, L₂ = ξ, L₃ = η; tri6 adds mid-side nodes on edges 1-2, 2-3, 3-1 with
//! N = 4 Lₐ L_b and corners N = L(2L − 1);
//! - **quadrilaterals** on [−1, 1]²: corners counter-clockwise from (−1, −1); quad4 bilinear,
//! quad8 serendipity (mid-side nodes on edges 1-2, 2-3, 3-4, 4-1), quad9 biquadratic Lagrange
//! (quad8 plus the centre);
//! - **tetrahedra**: tet4, tet10 (see [`crate::shape`]);
//! - **wedges**: the triangle times ζ ∈ [−1, 1]; wedge6, and wedge15 with mid-side nodes on the
//! bottom triangle's edges, the top's, then the vertical edges 1-4, 2-5, 3-6 (the order of
//! [`ElementKind::Wedge6`]'s edges): corners N = ½ L(2L − 1)(1 + ζζᵢ) − ½ L(1 − ζ²), triangle
//! mid-sides N = 2 Lₐ L_b (1 + ζζᵢ), vertical mid-sides N = L(1 − ζ²);
//! - **pyramid5**: square base [−1, 1]² at ζ = 0 (corners as quad4), apex (0, 0, 1); the rational
//! functions N = ¼[(1 − ζ) + ξξᵢ + ηηᵢ + ξᵢηᵢ ξη / (1 − ζ)] and N₅ = ζ, linearly complete and
//! conforming with neighbouring tetrahedra and hexahedra (L. Bedrosian, *Shape functions and
//! integration formulas for three-dimensional finite element analysis*, IJNME 35(1):95–108,
//! 1992). At the apex itself the rational term and its derivatives take their limit along the
//! axis, zero; quadrature points never lie there;
//! - **hexahedra** on [−1, 1]³: hex8 trilinear, hex20 serendipity (see [`crate::shape`]), hex27
//! triquadratic Lagrange (hex20, then the face centres in the order of [`ElementKind::Hex8`]'s
//! faces, then the centre).
//!
//! Node orders are corners first, then mid-side nodes in the model's edge order (the model is the
//! one place they are written down), then face and body nodes, the Abaqus/VTK convention of
//! [`fieldcraft_model::ElementKind`]. 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.,
//! 2013, ch. 6; T. J. R. Hughes, *The Finite Element Method*, Dover 2000, ch. 3).
//!
//! **Derivatives.** [`Basis::eval`] gives values and first derivatives; the families the solid
//! kernels used before the library existed (tet4, tet10, wedge6, hex8, hex20) keep their
//! hand-written functions there, so element results do not change by a bit. [`Basis::eval2`]
//! adds the Hessian ∂²N/∂ξₐ∂ξ_b, computed from the same formulas by second-order automatic
//! differentiation ([`crate::jet`]): exact to round-off for every family.
//!
//! **Physical derivatives** (Hughes 2000, §3.9 and §4.2 for the second derivatives): with
//! J_ab = ∂x_b/∂ξₐ, the gradient is ∇ₓN = J⁻¹ ∇_ξN and the Hessian
//! Hₓ = J⁻¹ (H_ξ − Σ_b (∂N/∂x_b) X_b″) J⁻ᵀ, where X_b″ = Σᵢ x_{i,b} H_ξ(Nᵢ) is the second
//! derivative of the map. On a curved (non-affine) element that last term is what makes a field
//! linear in x have zero Hessian; dropping it is a classic error in residual-based stabilization.
//! A family of dimension d maps onto the first d physical coordinates (plane elements in x–y,
//! lines along x); elements embedded in a higher dimension (shells, beams, trusses in 3D) pass
//! coordinates in their own local frame.
use std::sync::LazyLock;
use fieldcraft_model::ElementKind;
use crate::dense::positive_inverse;
use crate::jet::Jet;
use crate::shape::{self, HEX_CORNERS, MAX_NODES, Shape, TET_CORNERS, WEDGE_CORNERS};
use crate::{Error, Result};
/// An element family's interpolation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Basis {
Line2,
Line3,
Tri3,
Tri6,
Quad4,
Quad8,
Quad9,
Tet4,
Tet10,
Wedge6,
Wedge15,
Pyramid5,
Hex8,
Hex20,
Hex27,
}
/// Values, first and second natural derivatives at one point.
#[derive(Debug, Clone, Copy)]
pub struct Shape2 {
pub shape: Shape,
/// ∂²Nᵢ/∂ξₐ∂ξ_b.
pub d2n: [[[f64; 3]; 3]; MAX_NODES],
}
impl Shape2 {
pub fn hessians(&self) -> &[[[f64; 3]; 3]] {
self.d2n.get(..self.shape.count).unwrap_or_default()
}
}
/// Physical gradients at one point.
#[derive(Debug, Clone, Copy)]
pub struct Gradients {
pub count: usize,
/// ∂Nᵢ/∂x_b.
pub dx: [[f64; 3]; MAX_NODES],
/// det J (a length, area or volume ratio for dimension 1, 2, 3).
pub det: f64,
}
impl Gradients {
pub fn gradients(&self) -> &[[f64; 3]] {
self.dx.get(..self.count).unwrap_or_default()
}
}
/// Physical gradients and Hessians at one point.
#[derive(Debug, Clone, Copy)]
pub struct Hessians {
pub gradients: Gradients,
/// ∂²Nᵢ/∂x_a∂x_b.
pub d2x: [[[f64; 3]; 3]; MAX_NODES],
}
impl Hessians {
pub fn hessians(&self) -> &[[[f64; 3]; 3]] {
self.d2x.get(..self.gradients.count).unwrap_or_default()
}
/// ∇²Nᵢ, the trace of the Hessian.
pub fn laplacian(&self, i: usize) -> f64 {
self.d2x.get(i).map_or(0.0, |h| h[0][0] + h[1][1] + h[2][2])
}
}
const LINE_ENDS: [[f64; 3]; 2] = [[-1., 0., 0.], [1., 0., 0.]];
const TRI_CORNERS: [[f64; 3]; 3] = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]];
const QUAD_CORNERS: [[f64; 3]; 4] = [[-1., -1., 0.], [1., -1., 0.], [1., 1., 0.], [-1., 1., 0.]];
const PYRAMID_CORNERS: [[f64; 3]; 5] = [[-1., -1., 0.], [1., -1., 0.], [1., 1., 0.], [-1., 1., 0.], [0., 0., 1.]];
fn midpoint(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[0, 1, 2].map(|k| (a[k] + b[k]) / 2.0)
}
/// Corners, then the midpoints of `edges` (pairs of corner positions).
fn with_midsides(corners: &[[f64; 3]], edges: &[&[usize]]) -> Vec<[f64; 3]> {
let mut v = corners.to_vec();
for e in edges {
if let (Some(a), Some(b)) = (e.first().and_then(|a| corners.get(*a)), e.get(1).and_then(|b| corners.get(*b))) {
v.push(midpoint(*a, *b));
}
}
v
}
/// One-dimensional quadratic Lagrange function of the node at t = c ∈ {−1, 0, 1}.
fn lagrange2(t: Jet, c: f64) -> Jet {
if c < 0.0 {
t * (t - 1.0) * 0.5
} else if c > 0.0 {
t * (t + 1.0) * 0.5
} else {
1.0 - t * t
}
}
impl Basis {
pub const ALL: [Basis; 15] = [
Basis::Line2,
Basis::Line3,
Basis::Tri3,
Basis::Tri6,
Basis::Quad4,
Basis::Quad8,
Basis::Quad9,
Basis::Tet4,
Basis::Tet10,
Basis::Wedge6,
Basis::Wedge15,
Basis::Pyramid5,
Basis::Hex8,
Basis::Hex20,
Basis::Hex27,
];
pub fn name(self) -> &'static str {
match self {
Basis::Line2 => "line2",
Basis::Line3 => "line3",
Basis::Tri3 => "tri3",
Basis::Tri6 => "tri6",
Basis::Quad4 => "quad4",
Basis::Quad8 => "quad8",
Basis::Quad9 => "quad9",
Basis::Tet4 => "tet4",
Basis::Tet10 => "tet10",
Basis::Wedge6 => "wedge6",
Basis::Wedge15 => "wedge15",
Basis::Pyramid5 => "pyramid5",
Basis::Hex8 => "hex8",
Basis::Hex20 => "hex20",
Basis::Hex27 => "hex27",
}
}
/// The interpolation of a model element kind (line elements: along their axis).
pub fn of(kind: ElementKind) -> Basis {
match kind {
ElementKind::Rod2 | ElementKind::Beam2 => Basis::Line2,
ElementKind::Rod3 => Basis::Line3,
ElementKind::Tri3 => Basis::Tri3,
ElementKind::Tri6 => Basis::Tri6,
ElementKind::Quad4 => Basis::Quad4,
ElementKind::Quad8 => Basis::Quad8,
ElementKind::Tet4 => Basis::Tet4,
ElementKind::Tet10 => Basis::Tet10,
ElementKind::Wedge6 => Basis::Wedge6,
ElementKind::Hex8 => Basis::Hex8,
ElementKind::Hex20 => Basis::Hex20,
}
}
/// Dimension of the reference element.
pub fn dim(self) -> usize {
match self {
Basis::Line2 | Basis::Line3 => 1,
Basis::Tri3 | Basis::Tri6 | Basis::Quad4 | Basis::Quad8 | Basis::Quad9 => 2,
_ => 3,
}
}
/// Number of nodes (and functions).
pub fn count(self) -> usize {
match self {
Basis::Line2 => 2,
Basis::Line3 | Basis::Tri3 => 3,
Basis::Quad4 | Basis::Tet4 => 4,
Basis::Pyramid5 => 5,
Basis::Tri6 | Basis::Wedge6 => 6,
Basis::Quad8 | Basis::Hex8 => 8,
Basis::Quad9 => 9,
Basis::Tet10 => 10,
Basis::Wedge15 => 15,
Basis::Hex20 => 20,
Basis::Hex27 => 27,
}
}
/// The corner (vertex) nodes, first in the node list.
pub fn corners(self) -> usize {
match self {
Basis::Line2 | Basis::Line3 => 2,
Basis::Tri3 | Basis::Tri6 => 3,
Basis::Quad4 | Basis::Quad8 | Basis::Quad9 | Basis::Tet4 | Basis::Tet10 => 4,
Basis::Pyramid5 => 5,
Basis::Wedge6 | Basis::Wedge15 => 6,
Basis::Hex8 | Basis::Hex20 | Basis::Hex27 => 8,
}
}
/// The degree of the complete polynomials the family reproduces exactly (in natural
/// coordinates, and in physical ones on an affine map).
pub fn degree(self) -> usize {
match self {
Basis::Line2 | Basis::Tri3 | Basis::Quad4 | Basis::Tet4 | Basis::Wedge6 | Basis::Pyramid5 | Basis::Hex8 => 1,
_ => 2,
}
}
/// The natural coordinates of the nodes, in node order.
pub fn nodes(self) -> &'static [[f64; 3]] {
static TABLES: LazyLock<Vec<Vec<[f64; 3]>>> = LazyLock::new(|| Basis::ALL.iter().map(|b| b.build_nodes()).collect());
TABLES.get(self as usize).map(Vec::as_slice).unwrap_or_default()
}
fn build_nodes(self) -> Vec<[f64; 3]> {
match self {
Basis::Line2 => LINE_ENDS.to_vec(),
Basis::Line3 => with_midsides(&LINE_ENDS, ElementKind::Rod2.edges()),
Basis::Tri3 => TRI_CORNERS.to_vec(),
Basis::Tri6 => with_midsides(&TRI_CORNERS, ElementKind::Tri6.edges()),
Basis::Quad4 => QUAD_CORNERS.to_vec(),
Basis::Quad8 => with_midsides(&QUAD_CORNERS, ElementKind::Quad8.edges()),
Basis::Quad9 => {
let mut v = with_midsides(&QUAD_CORNERS, ElementKind::Quad8.edges());
v.push([0.0; 3]);
v
}
Basis::Tet4 => TET_CORNERS.to_vec(),
Basis::Tet10 => with_midsides(&TET_CORNERS, ElementKind::Tet10.edges()),
Basis::Wedge6 => WEDGE_CORNERS.to_vec(),
Basis::Wedge15 => with_midsides(&WEDGE_CORNERS, ElementKind::Wedge6.edges()),
Basis::Pyramid5 => PYRAMID_CORNERS.to_vec(),
Basis::Hex8 => HEX_CORNERS.to_vec(),
Basis::Hex20 => with_midsides(&HEX_CORNERS, ElementKind::Hex20.edges()),
Basis::Hex27 => {
let mut v = with_midsides(&HEX_CORNERS, ElementKind::Hex20.edges());
for face in ElementKind::Hex8.faces() {
let c = face.iter().filter_map(|i| HEX_CORNERS.get(*i)).fold([0.0; 3], |s, x| [0, 1, 2].map(|k| s[k] + x[k] / 4.0));
v.push(c);
}
v.push([0.0; 3]);
v
}
}
}
/// Values and first natural derivatives at `xi`.
pub fn eval(self, xi: [f64; 3]) -> Shape {
match self {
Basis::Tet4 => shape::tet4(xi),
Basis::Tet10 => shape::tet10(xi),
Basis::Wedge6 => shape::wedge6(xi),
Basis::Hex8 => shape::hex8(xi),
Basis::Hex20 => shape::hex20(xi),
_ => self.eval2(xi).shape,
}
}
/// Values, first and second natural derivatives at `xi`.
pub fn eval2(self, xi: [f64; 3]) -> Shape2 {
let jets = self.jets(xi);
let count = self.count();
let mut out = Shape2 { shape: Shape { count, n: [0.0; MAX_NODES], dn: [[0.0; 3]; MAX_NODES] }, d2n: [[[0.0; 3]; 3]; MAX_NODES] };
for (i, j) in jets.iter().enumerate().take(count) {
out.shape.n[i] = j.v;
out.shape.dn[i] = j.g;
out.d2n[i] = j.h;
}
out
}
/// The functions as jets in the natural coordinates (only the first `count` are set).
fn jets(self, xi: [f64; 3]) -> [Jet; MAX_NODES] {
let x = Jet::point(xi);
let dim = self.dim();
let mut n = [Jet::constant(0.0); MAX_NODES];
let nodes = self.nodes();
// Area or volume coordinates of the simplex families.
let tri = [1.0 - x[0] - x[1], x[0], x[1]];
let tet = [1.0 - x[0] - x[1] - x[2], x[0], x[1], x[2]];
let set = |n: &mut [Jet; MAX_NODES], i: usize, v: Jet| {
if let Some(slot) = n.get_mut(i) {
*slot = v;
}
};
match self {
Basis::Line2 | Basis::Quad4 | Basis::Hex8 => {
for (i, c) in nodes.iter().enumerate() {
let v = (0..dim).fold(Jet::constant(1.0), |p, k| p * (1.0 + x[k] * c[k]) * 0.5);
set(&mut n, i, v);
}
}
Basis::Line3 | Basis::Quad9 | Basis::Hex27 => {
for (i, c) in nodes.iter().enumerate() {
let v = (0..dim).fold(Jet::constant(1.0), |p, k| p * lagrange2(x[k], c[k]));
set(&mut n, i, v);
}
}
Basis::Quad8 | Basis::Hex20 => {
// Corners ∏(1 + ξc)/2ᵈ (Σ ξc − (d − 1)); a mid-side node with c_k = 0:
// (1 − ξ_k²) ∏_{l≠k}(1 + ξ_l c_l)/2ᵈ⁻¹.
for (i, c) in nodes.iter().enumerate() {
let along = (0..dim).find(|k| c[*k] == 0.0);
let v = match along {
None => {
let p = (0..dim).fold(Jet::constant(1.0), |p, k| p * (1.0 + x[k] * c[k]) * 0.5);
let s = (0..dim).fold(Jet::constant(-(dim as f64 - 1.0)), |s, k| s + x[k] * c[k]);
p * s
}
Some(a) => (0..dim).filter(|k| *k != a).fold(1.0 - x[a] * x[a], |p, k| p * (1.0 + x[k] * c[k]) * 0.5),
};
set(&mut n, i, v);
}
}
Basis::Tri3 => {
for (i, l) in tri.iter().enumerate() {
set(&mut n, i, *l);
}
}
Basis::Tet4 => {
for (i, l) in tet.iter().enumerate() {
set(&mut n, i, *l);
}
}
Basis::Tri6 | Basis::Tet10 => {
let (l, edges): (&[Jet], _) =
if self == Basis::Tri6 { (&tri, ElementKind::Tri6.edges()) } else { (&tet, ElementKind::Tet10.edges()) };
for (i, li) in l.iter().enumerate() {
set(&mut n, i, *li * (*li * 2.0 - 1.0));
}
for e in edges {
if let [a, b, m] = **e
&& let (Some(la), Some(lb)) = (l.get(a), l.get(b))
{
set(&mut n, m, *la * *lb * 4.0);
}
}
}
Basis::Wedge6 => {
for i in 0..3 {
set(&mut n, i, tri[i] * (1.0 - x[2]) * 0.5);
set(&mut n, i + 3, tri[i] * (1.0 + x[2]) * 0.5);
}
}
Basis::Wedge15 => {
let bubble = 1.0 - x[2] * x[2];
for i in 0..6 {
let (l, z) = (tri[i % 3], if i < 3 { -1.0 } else { 1.0 });
set(&mut n, i, l * (l * 2.0 - 1.0) * (1.0 + x[2] * z) * 0.5 - l * bubble * 0.5);
}
for (e, edge) in ElementKind::Wedge6.edges().iter().enumerate() {
let [a, b] = **edge else { continue };
let v = if e < 6 {
let z = if a < 3 { -1.0 } else { 1.0 };
tri[a % 3] * tri[b % 3] * (1.0 + x[2] * z) * 2.0
} else {
tri[a % 3] * bubble
};
set(&mut n, 6 + e, v);
}
}
Basis::Pyramid5 => {
let d = 1.0 - x[2];
let r = if d.v.abs() > 0.0 { x[0] * x[1] / d } else { Jet::constant(0.0) };
for (i, c) in PYRAMID_CORNERS.iter().take(4).enumerate() {
set(&mut n, i, (d + x[0] * c[0] + x[1] * c[1] + r * (c[0] * c[1])) * 0.25);
}
set(&mut n, 4, x[2]);
}
}
n
}
/// The Jacobian J_ab = ∂x_b/∂ξₐ over the family's dimensions, the identity in the others.
fn jacobian(self, s: &Shape, xyz: &[[f64; 3]]) -> Result<[[f64; 3]; 3]> {
if xyz.len() < s.count {
return Err(Error::Invalid(format!("a {} element has {} nodes, got {} coordinates", self.name(), s.count, xyz.len())));
}
let dim = self.dim();
let mut j = [[0.0; 3]; 3];
for (d, x) in s.derivatives().iter().zip(xyz) {
for a in 0..dim {
for b in 0..dim {
j[a][b] += d[a] * x[b];
}
}
}
for a in dim..3 {
j[a][a] = 1.0;
}
Ok(j)
}
fn degenerate(self) -> Error {
Error::Degenerate(format!("a {} element is inverted or degenerate (its Jacobian is not positive)", self.name()))
}
/// Physical gradients at a point from its natural shape (`eval`) and the node coordinates.
pub fn gradients(self, s: &Shape, xyz: &[[f64; 3]]) -> Result<Gradients> {
self.mapped(s, xyz).map(|m| m.0)
}
/// The physical gradients and J⁻¹.
fn mapped(self, s: &Shape, xyz: &[[f64; 3]]) -> Result<(Gradients, [[f64; 3]; 3])> {
let (inv, det) = positive_inverse(&self.jacobian(s, xyz)?).ok_or_else(|| self.degenerate())?;
let mut dx = [[0.0; 3]; MAX_NODES];
for (g, d) in dx.iter_mut().zip(s.derivatives()) {
for b in 0..3 {
g[b] = inv[b][0] * d[0] + inv[b][1] * d[1] + inv[b][2] * d[2];
}
}
Ok((Gradients { count: s.count, dx, det }, inv))
}
/// Physical gradients and Hessians from a point's `eval2` and the node coordinates, with the
/// second-derivative term of the map.
pub fn hessians(self, s: &Shape2, xyz: &[[f64; 3]]) -> Result<Hessians> {
let (gradients, inv) = self.mapped(&s.shape, xyz)?;
let dim = self.dim();
// X_b″ = Σᵢ x_{i,b} H_ξ(Nᵢ) for the mapped coordinates b < dim.
let mut map2 = [[[0.0; 3]; 3]; 3];
for (h, x) in s.hessians().iter().zip(xyz) {
for b in 0..dim {
for p in 0..3 {
for q in 0..3 {
map2[b][p][q] += x[b] * h[p][q];
}
}
}
}
let mut d2x = [[[0.0; 3]; 3]; MAX_NODES];
for ((out, h), g) in d2x.iter_mut().zip(s.hessians()).zip(gradients.gradients()) {
// A = H_ξ − Σ_b g_b X_b″, then Hₓ = J⁻¹ A J⁻ᵀ.
let mut a = *h;
for b in 0..dim {
for p in 0..3 {
for q in 0..3 {
a[p][q] -= g[b] * map2[b][p][q];
}
}
}
for r in 0..3 {
for c in 0..3 {
let mut v = 0.0;
for p in 0..3 {
for q in 0..3 {
v += inv[r][p] * a[p][q] * inv[c][q];
}
}
out[r][c] = v;
}
}
}
Ok(Hessians { gradients, d2x })
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A point inside every family's reference element.
const P: [f64; 3] = [0.21, 0.17, 0.33];
fn point(b: Basis) -> [f64; 3] {
match b.dim() {
1 => [P[0], 0.0, 0.0],
2 => [P[0], P[1], 0.0],
_ => P,
}
}
/// Monomials ξᵃ ηᵇ ζᶜ of total degree ≤ `degree` in the family's dimensions, with their exact
/// gradient and Hessian.
fn monomials(dim: usize, degree: usize) -> Vec<[usize; 3]> {
let mut out = Vec::new();
for a in 0..=degree {
for b in 0..=degree {
for c in 0..=degree {
let e = [a, b, c];
if a + b + c <= degree && (dim..3).all(|k| e[k] == 0) {
out.push(e);
}
}
}
}
out
}
fn monomial(e: [usize; 3], x: [f64; 3]) -> (f64, [f64; 3], [[f64; 3]; 3]) {
let p = |k: usize, d: usize| -> f64 {
// d-th derivative of x_k^{e_k}.
let n = e[k];
if d > n {
return 0.0;
}
let coef: f64 = (0..d).map(|i| (n - i) as f64).product();
coef * x[k].powi((n - d) as i32)
};
let v = p(0, 0) * p(1, 0) * p(2, 0);
let mut g = [0.0; 3];
let mut h = [[0.0; 3]; 3];
for a in 0..3 {
let mut d = [0usize; 3];
d[a] += 1;
g[a] = p(0, d[0]) * p(1, d[1]) * p(2, d[2]);
for b in 0..3 {
let mut dd = d;
dd[b] += 1;
h[a][b] = p(0, dd[0]) * p(1, dd[1]) * p(2, dd[2]);
}
}
(v, g, h)
}
#[test]
fn kronecker_delta_and_partition_of_unity() {
for (i, b) in Basis::ALL.into_iter().enumerate() {
assert_eq!(b as usize, i, "Basis::ALL is in declaration order (node tables)");
let nodes = b.nodes();
assert_eq!(nodes.len(), b.count(), "{}", b.name());
for (i, p) in nodes.iter().enumerate() {
for s in [b.eval(*p), b.eval2(*p).shape] {
for (j, v) in s.values().iter().enumerate() {
assert!((v - if i == j { 1.0 } else { 0.0 }).abs() < 1e-14, "{} N{j} at node {i}: {v}", b.name());
}
}
}
let s = b.eval2(point(b));
assert!((s.shape.values().iter().sum::<f64>() - 1.0).abs() < 1e-14, "{}", b.name());
for a in 0..3 {
assert!(s.shape.derivatives().iter().map(|d| d[a]).sum::<f64>().abs() < 1e-13, "{}", b.name());
for c in 0..3 {
assert!(s.hessians().iter().map(|h| h[a][c]).sum::<f64>().abs() < 1e-12, "{}", b.name());
}
}
}
}
#[test]
fn complete_polynomials_and_their_derivatives_are_reproduced() {
for b in Basis::ALL {
let nodes = b.nodes();
let xi = point(b);
let s = b.eval2(xi);
for e in monomials(b.dim(), b.degree()) {
let (v, g, h) = monomial(e, xi);
let at = |i: usize| monomial(e, nodes[i]).0;
let iv: f64 = (0..b.count()).map(|i| s.shape.n[i] * at(i)).sum();
assert!((iv - v).abs() < 1e-13, "{} {e:?}: {iv} vs {v}", b.name());
for a in 0..3 {
let ig: f64 = (0..b.count()).map(|i| s.shape.dn[i][a] * at(i)).sum();
assert!((ig - g[a]).abs() < 1e-12, "{} {e:?} d{a}: {ig} vs {}", b.name(), g[a]);
for c in 0..3 {
let ih: f64 = (0..b.count()).map(|i| s.d2n[i][a][c] * at(i)).sum();
assert!((ih - h[a][c]).abs() < 1e-11, "{} {e:?} d{a}{c}: {ih} vs {}", b.name(), h[a][c]);
}
}
}
}
}
#[test]
fn derivatives_match_finite_differences() {
let step = 1e-5;
for b in Basis::ALL {
let xi = point(b);
let s = b.eval2(xi);
let fast = b.eval(xi);
for i in 0..b.count() {
for a in 0..3 {
assert!((fast.dn[i][a] - s.shape.dn[i][a]).abs() < 1e-14, "{} eval vs eval2", b.name());
}
assert!((fast.n[i] - s.shape.n[i]).abs() < 1e-15);
}
for a in 0..b.dim() {
let (mut p, mut m) = (xi, xi);
p[a] += step;
m[a] -= step;
let (sp, sm) = (b.eval2(p), b.eval2(m));
for i in 0..b.count() {
let fd = (sp.shape.n[i] - sm.shape.n[i]) / (2.0 * step);
assert!((fd - s.shape.dn[i][a]).abs() < 1e-8, "{} dN{i}/dξ{a}", b.name());
for c in 0..3 {
let fd2 = (sp.shape.dn[i][c] - sm.shape.dn[i][c]) / (2.0 * step);
assert!((fd2 - s.d2n[i][a][c]).abs() < 1e-7, "{} d²N{i}/dξ{a}dξ{c}: {fd2} vs {}", b.name(), s.d2n[i][a][c]);
}
}
}
}
}
/// An affine map and a curved one (the affine map plus a smooth bend), applied to the
/// reference nodes.
fn affine(x: [f64; 3]) -> [f64; 3] {
[2.0 * x[0] + 0.3 * x[1] - 0.2 * x[2] + 1.0, 0.4 * x[0] + 1.5 * x[1] + 0.1 * x[2] - 2.0, -0.3 * x[0] + 0.2 * x[1] + 1.2 * x[2] + 0.5]
}
fn curved(x: [f64; 3]) -> [f64; 3] {
let a = affine(x);
[a[0] + 0.15 * x[1] * x[1], a[1] + 0.1 * x[0] * x[0], a[2] + 0.12 * x[0] * x[1]]
}
/// A quadratic field in physical coordinates (in the family's dimensions) with its gradient
/// and Hessian.
fn field(dim: usize, x: [f64; 3], degree: usize) -> (f64, [f64; 3], [[f64; 3]; 3]) {
let c = [0.7, -1.3, 0.4];
let mut v = 0.5;
let mut g = [0.0; 3];
let mut h = [[0.0; 3]; 3];
for k in 0..dim {
v += c[k] * x[k];
g[k] = c[k];
}
if degree >= 2 {
let q = [[1.1, 0.3, -0.4], [0.3, -0.6, 0.25], [-0.4, 0.25, 0.9]];
for a in 0..dim {
for b in 0..dim {
v += 0.5 * q[a][b] * x[a] * x[b];
g[a] += q[a][b] * x[b];
h[a][b] = q[a][b];
}
}
}
(v, g, h)
}
fn mapped(b: Basis, map: fn([f64; 3]) -> [f64; 3]) -> Vec<[f64; 3]> {
b.nodes()
.iter()
.map(|p| {
let x = map(*p);
[0, 1, 2].map(|k| if k < b.dim() { x[k] } else { 0.0 })
})
.collect()
}
#[test]
fn physical_gradients_hessians_and_laplacians() {
for b in Basis::ALL {
let xi = point(b);
let s = b.eval2(xi);
for (map, degree) in [(affine as fn([f64; 3]) -> [f64; 3], b.degree()), (curved, 1)] {
let xyz = mapped(b, map);
let h = b.hessians(&s, &xyz).unwrap();
let x: [f64; 3] = (0..b.count()).fold([0.0; 3], |acc, i| [0, 1, 2].map(|k| acc[k] + s.shape.n[i] * xyz[i][k]));
let (_, g, hh) = field(b.dim(), x, degree);
let nodal: Vec<f64> = xyz.iter().map(|p| field(b.dim(), *p, degree).0).collect();
let grad: [f64; 3] = [0, 1, 2].map(|a| (0..b.count()).map(|i| h.gradients.dx[i][a] * nodal[i]).sum());
for a in 0..3 {
assert!((grad[a] - g[a]).abs() < 1e-11, "{} grad {a}: {} vs {}", b.name(), grad[a], g[a]);
for c in 0..3 {
let v: f64 = (0..b.count()).map(|i| h.d2x[i][a][c] * nodal[i]).sum();
assert!((v - hh[a][c]).abs() < 1e-10, "{} hess {a}{c}: {v} vs {}", b.name(), hh[a][c]);
}
}
let lap: f64 = (0..b.count()).map(|i| h.laplacian(i) * nodal[i]).sum();
assert!((lap - (hh[0][0] + hh[1][1] + hh[2][2])).abs() < 1e-10, "{} laplacian", b.name());
}
}
}
#[test]
fn curved_map_hessians_match_finite_differences_in_physical_space() {
// On a curved element a quadratic field is not reproduced, but the physical Hessian of
// the interpolant must still be the derivative of its physical gradient. Differentiate
// the gradient along the natural directions and map: ∂(∇ₓu)/∂ξₐ = Σ_b J_ab Hₓ[b].
for b in Basis::ALL.into_iter().filter(|b| b.degree() == 2) {
let xyz = mapped(b, curved);
let nodal: Vec<f64> = xyz.iter().map(|p| field(b.dim(), *p, 2).0 + 0.3 * p[0] * p[0] * p[0]).collect();
let grad_at = |xi: [f64; 3]| -> [f64; 3] {
let g = b.gradients(&b.eval(xi), &xyz).unwrap();
[0, 1, 2].map(|a| (0..b.count()).map(|i| g.dx[i][a] * nodal[i]).sum())
};
let xi = point(b);
let s = b.eval2(xi);
let h = b.hessians(&s, &xyz).unwrap();
let hu: [[f64; 3]; 3] = [0, 1, 2].map(|r| [0, 1, 2].map(|c| (0..b.count()).map(|i| h.d2x[i][r][c] * nodal[i]).sum()));
let j = b.jacobian(&s.shape, &xyz).unwrap();
let step = 1e-5;
for a in 0..b.dim() {
let (mut p, mut m) = (xi, xi);
p[a] += step;
m[a] -= step;
let (gp, gm) = (grad_at(p), grad_at(m));
for c in 0..b.dim() {
let fd = (gp[c] - gm[c]) / (2.0 * step);
let exact: f64 = (0..3).map(|k| j[a][k] * hu[k][c]).sum();
assert!((fd - exact).abs() < 1e-6, "{} ∂(∂u/∂x{c})/∂ξ{a}: {fd} vs {exact}", b.name());
}
}
}
}
#[test]
fn inverted_and_short_inputs_are_errors() {
let xyz = mapped(Basis::Quad4, affine);
let s = Basis::Quad4.eval([0.0; 3]);
assert!(Basis::Quad4.gradients(&s, &xyz[..3]).is_err());
let flipped: Vec<[f64; 3]> = xyz.iter().map(|p| [p[0], -p[1], 0.0]).collect();
assert!(matches!(Basis::Quad4.gradients(&s, &flipped), Err(Error::Degenerate(_))));
let collapsed = vec![[0.0; 3]; 4];
assert!(Basis::Quad4.gradients(&s, &collapsed).is_err());
// The pyramid's apex is finite.
let apex = Basis::Pyramid5.eval2([0.0, 0.0, 1.0]);
assert!(apex.shape.values().iter().chain(apex.shape.derivatives().iter().flatten()).all(|v| v.is_finite()));
assert_eq!(Basis::of(ElementKind::Hex20), Basis::Hex20);
assert_eq!(Basis::of(ElementKind::Rod2).dim(), 1);
}
}