forked from 1b-agent/fieldcraft-upstream
FEM foundation A: DofKind/DofLayout, shape-function library with Hessians, solvers on DofLayout (#35)
* fem: DofKind/DofLayout and the shape-function library with second derivatives DofKind (Ux..Rz, T, P), DofSet, ElementDofs (per-node kinds and condensed internal unknowns) and DofLayout: mesh nodes by label, kinds in order, then reference points. basis::Basis covers line2/3, tri3/6, quad4/8/9, tet4/10, wedge6/15, pyramid5, hex8/20/27 with values, first and second natural derivatives (second-order forward AD, fem::jet) and the physical gradients, Hessians and Laplacians with the map's second-derivative term. Tested for Kronecker delta, partition of unity, polynomial completeness, finite differences, affine and curved maps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver, explicit: number degrees of freedom with DofLayout The implicit mesh numbers its unknowns with a DofLayout (mesh nodes by label with the kinds their elements declare, then six per coupled reference point); assembly adds element blocks by (node, DofKind), so a node may carry any mix of kinds; couplings, boundary conditions, loads and results address (node, kind). The explicit system keeps node-major translations, now checked against its layout (NODE_DOFS). Solid formulations declare their ElementDofs. Every static, frequency and explicit result of the solver, explicit, engine and io test suites hashes bit-identically before and after (957 results). docs/fem.md describes the DOF model and the shape-function conventions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * fem, solver: simplify after review Block offsets resolved once per node pair; add_scalar/add_diagonal take the kinds like add_block; one checked Jacobian inverse (dense::positive_inverse) for the solid kernels and the basis library; static natural-node tables; the Hessian path inverts the Jacobian once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
16 files changed
+1805
-192
No files matched your search
@@ -0,0 +1,762 @@
|
||||
//! 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
|
||||
//! [`modelcraft_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 modelcraft_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::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);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,14 @@ pub fn inverse3(m: &[[f64; 3]; 3]) -> Option<([[f64; 3]; 3], f64)> {
|
||||
Some((inv, det))
|
||||
}
|
||||
|
||||
/// The inverse and determinant of a Jacobian that maps a reference element onto a valid one:
|
||||
/// `None` unless det J is positive beyond round-off relative to the product of its row norms.
|
||||
pub fn positive_inverse(j: &[[f64; 3]; 3]) -> Option<([[f64; 3]; 3], f64)> {
|
||||
let (inv, det) = inverse3(j)?;
|
||||
let norms: f64 = j.iter().map(|r| (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt()).product();
|
||||
(det > 1e-12 * norms).then_some((inv, det))
|
||||
}
|
||||
|
||||
pub fn det3(m: &[[f64; 3]; 3]) -> f64 {
|
||||
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
//! Degrees of freedom: what a node carries and how the global unknowns are numbered.
|
||||
//!
|
||||
//! Every unknown of a discrete system is a ([`DofKind`], node) pair. An element declares the
|
||||
//! kinds each of its nodes uses ([`ElementDofs`]): three translations for solids, six
|
||||
//! (translations and rotations) for shells and beams, two for plane elements, a temperature for
|
||||
//! heat transfer, a pressure for mixed u–p elements, and the internal unknowns it condenses out
|
||||
//! itself (incompatible modes, a discontinuous pressure), which never reach the global system.
|
||||
//! A node carries the union of what its elements, constraints and boundary conditions need.
|
||||
//!
|
||||
//! [`DofLayout`] numbers them: mesh nodes in label order, each node's kinds in [`DofKind`] order,
|
||||
//! then extra points (reference points) in the order they are added, so the same model always
|
||||
//! gives the same numbering (the convention of T. J. R. Hughes, *The Finite Element Method*,
|
||||
//! Dover 2000, §2.8–2.10, with the ID array replaced by per-node offsets). Assembly reads a
|
||||
//! node's unknowns as one contiguous block, so a node pair is a dense block of the global matrix.
|
||||
//!
|
||||
//! Boundary conditions and loads address a node and a [`DofKind`]; the model's 1-based degree
|
||||
//! of freedom numbers (1–3 translations, 4–6 rotations, 11 temperature, 8 pressure, the common
|
||||
//! convention of the deck formats) convert with [`DofKind::from_number`].
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Range;
|
||||
|
||||
/// One kind of nodal unknown. The order is the numbering order within a node.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DofKind {
|
||||
/// Translation along X.
|
||||
Ux,
|
||||
/// Translation along Y.
|
||||
Uy,
|
||||
/// Translation along Z.
|
||||
Uz,
|
||||
/// Rotation about X.
|
||||
Rx,
|
||||
/// Rotation about Y.
|
||||
Ry,
|
||||
/// Rotation about Z.
|
||||
Rz,
|
||||
/// Temperature.
|
||||
T,
|
||||
/// Pressure (hydrostatic, of a mixed u–p element).
|
||||
P,
|
||||
}
|
||||
|
||||
impl DofKind {
|
||||
pub const ALL: [DofKind; 8] = [DofKind::Ux, DofKind::Uy, DofKind::Uz, DofKind::Rx, DofKind::Ry, DofKind::Rz, DofKind::T, DofKind::P];
|
||||
|
||||
/// Position in [`DofKind::ALL`].
|
||||
pub const fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
|
||||
/// The translation along axis `axis` (0–2).
|
||||
pub fn translation(axis: usize) -> Option<DofKind> {
|
||||
[DofKind::Ux, DofKind::Uy, DofKind::Uz].get(axis).copied()
|
||||
}
|
||||
|
||||
/// The rotation about axis `axis` (0–2).
|
||||
pub fn rotation(axis: usize) -> Option<DofKind> {
|
||||
[DofKind::Rx, DofKind::Ry, DofKind::Rz].get(axis).copied()
|
||||
}
|
||||
|
||||
/// The structural direction 0–5 (translations, then rotations), `None` for T and P.
|
||||
pub fn direction(self) -> Option<usize> {
|
||||
(self.index() < 6).then_some(self.index())
|
||||
}
|
||||
|
||||
/// The kind of a structural direction 0–5.
|
||||
pub fn from_direction(direction: usize) -> Option<DofKind> {
|
||||
DofKind::ALL.get(direction).copied().filter(|k| k.index() < 6)
|
||||
}
|
||||
|
||||
/// The model's and decks' 1-based number: 1–3 translations, 4–6 rotations, 8 pressure,
|
||||
/// 11 temperature.
|
||||
pub fn number(self) -> u8 {
|
||||
match self {
|
||||
DofKind::T => 11,
|
||||
DofKind::P => 8,
|
||||
k => k.index() as u8 + 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of a model degree of freedom number ([`DofKind::number`]).
|
||||
pub fn from_number(number: u8) -> Option<DofKind> {
|
||||
DofKind::ALL.into_iter().find(|k| k.number() == number)
|
||||
}
|
||||
|
||||
/// What it is, in the user's terms.
|
||||
pub fn describe(self) -> &'static str {
|
||||
match self {
|
||||
DofKind::Ux => "X",
|
||||
DofKind::Uy => "Y",
|
||||
DofKind::Uz => "Z",
|
||||
DofKind::Rx => "rotation about X",
|
||||
DofKind::Ry => "rotation about Y",
|
||||
DofKind::Rz => "rotation about Z",
|
||||
DofKind::T => "temperature",
|
||||
DofKind::P => "pressure",
|
||||
}
|
||||
}
|
||||
|
||||
const fn bit(self) -> u8 {
|
||||
1 << self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of [`DofKind`]s, iterated in numbering order.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||
pub struct DofSet(u8);
|
||||
|
||||
impl DofSet {
|
||||
pub const EMPTY: DofSet = DofSet(0);
|
||||
/// Ux, Uy, Uz: solids, trusses, membranes in 3D.
|
||||
pub const TRANSLATIONS: DofSet = DofSet(0b0000_0111);
|
||||
/// Ux, Uy: plane elements.
|
||||
pub const PLANE: DofSet = DofSet(0b0000_0011);
|
||||
/// Rx, Ry, Rz.
|
||||
pub const ROTATIONS: DofSet = DofSet(0b0011_1000);
|
||||
/// Translations and rotations: shells, beams, reference points.
|
||||
pub const STRUCTURAL: DofSet = DofSet(0b0011_1111);
|
||||
pub const TEMPERATURE: DofSet = DofSet(DofKind::T.bit());
|
||||
pub const PRESSURE: DofSet = DofSet(DofKind::P.bit());
|
||||
|
||||
pub const fn of(kinds: &[DofKind]) -> DofSet {
|
||||
let mut bits = 0;
|
||||
let mut i = 0;
|
||||
while i < kinds.len() {
|
||||
bits |= kinds[i].bit();
|
||||
i += 1;
|
||||
}
|
||||
DofSet(bits)
|
||||
}
|
||||
|
||||
pub const fn contains(self, kind: DofKind) -> bool {
|
||||
self.0 & kind.bit() != 0
|
||||
}
|
||||
|
||||
/// Whether every kind of `other` is in this set.
|
||||
pub const fn includes(self, other: DofSet) -> bool {
|
||||
self.0 & other.0 == other.0
|
||||
}
|
||||
|
||||
pub const fn union(self, other: DofSet) -> DofSet {
|
||||
DofSet(self.0 | other.0)
|
||||
}
|
||||
|
||||
pub const fn len(self) -> usize {
|
||||
self.0.count_ones() as usize
|
||||
}
|
||||
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
/// The position of `kind` among this set's kinds (its offset in a node's block).
|
||||
pub const fn position(self, kind: DofKind) -> Option<usize> {
|
||||
if self.contains(kind) { Some((self.0 & (kind.bit() - 1)).count_ones() as usize) } else { None }
|
||||
}
|
||||
|
||||
/// The kinds, in numbering order.
|
||||
pub fn iter(self) -> impl Iterator<Item = DofKind> {
|
||||
DofKind::ALL.into_iter().filter(move |k| self.contains(*k))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<DofKind> for DofSet {
|
||||
fn from_iter<I: IntoIterator<Item = DofKind>>(iter: I) -> DofSet {
|
||||
DofSet(iter.into_iter().fold(0, |m, k| m | k.bit()))
|
||||
}
|
||||
}
|
||||
|
||||
/// What an element type contributes to the unknowns: the kinds every one of its nodes carries
|
||||
/// (its element matrices are ordered node by node, these kinds in order within a node) and the
|
||||
/// internal unknowns it condenses out before assembly.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ElementDofs {
|
||||
pub node: DofSet,
|
||||
pub internal: Internal,
|
||||
}
|
||||
|
||||
/// Element-internal unknowns, condensed at the element level (static condensation; Hughes
|
||||
/// 2000, §4.7 for incompatible modes, §4.4 for discontinuous pressure).
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct Internal {
|
||||
/// Incompatible (bubble) displacement modes.
|
||||
pub incompatible_modes: usize,
|
||||
/// Discontinuous pressure unknowns of a mixed element.
|
||||
pub pressure: usize,
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
pub const NONE: Internal = Internal { incompatible_modes: 0, pressure: 0 };
|
||||
|
||||
pub const fn count(self) -> usize {
|
||||
self.incompatible_modes + self.pressure
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementDofs {
|
||||
/// Three translations per node, nothing internal.
|
||||
pub const SOLID: ElementDofs = ElementDofs { node: DofSet::TRANSLATIONS, internal: Internal::NONE };
|
||||
|
||||
pub const fn new(node: DofSet) -> ElementDofs {
|
||||
ElementDofs { node, internal: Internal::NONE }
|
||||
}
|
||||
|
||||
/// Unknowns of an element with `nodes` nodes, before condensation.
|
||||
pub const fn total(self, nodes: usize) -> usize {
|
||||
nodes * self.node.len() + self.internal.count()
|
||||
}
|
||||
}
|
||||
|
||||
/// One numbered node: its label, its kinds and its first global unknown.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct Slot {
|
||||
label: u32,
|
||||
kinds: DofSet,
|
||||
first: usize,
|
||||
}
|
||||
|
||||
/// The global numbering of the unknowns.
|
||||
///
|
||||
/// Slots (positions) `0..nodes()` are the mesh nodes in label order; `nodes()..slots()` are the
|
||||
/// extra points in the order they were added. A slot's unknowns are contiguous, in
|
||||
/// [`DofKind`] order.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct DofLayout {
|
||||
slots: Vec<Slot>,
|
||||
nodes: usize,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
/// Collects what every node needs before numbering: the union of the kinds asked for.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LayoutBuilder {
|
||||
nodes: BTreeMap<u32, DofSet>,
|
||||
points: Vec<(u32, DofSet)>,
|
||||
}
|
||||
|
||||
impl LayoutBuilder {
|
||||
/// Node `label` needs `kinds` (added to what it needs already).
|
||||
pub fn node(&mut self, label: u32, kinds: DofSet) -> &mut Self {
|
||||
let entry = self.nodes.entry(label).or_default();
|
||||
*entry = entry.union(kinds);
|
||||
self
|
||||
}
|
||||
|
||||
/// Every node of an element of this kind needs the element's kinds.
|
||||
pub fn element(&mut self, labels: &[u32], dofs: ElementDofs) -> &mut Self {
|
||||
for &l in labels {
|
||||
self.node(l, dofs.node);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// An extra point (a reference point) numbered after every mesh node, in the order added;
|
||||
/// each call adds one.
|
||||
pub fn point(&mut self, label: u32, kinds: DofSet) -> &mut Self {
|
||||
self.points.push((label, kinds));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(&self) -> DofLayout {
|
||||
let mut slots = Vec::with_capacity(self.nodes.len() + self.points.len());
|
||||
let mut total = 0;
|
||||
for (&label, &kinds) in self.nodes.iter().chain(self.points.iter().map(|p| (&p.0, &p.1))) {
|
||||
slots.push(Slot { label, kinds, first: total });
|
||||
total += kinds.len();
|
||||
}
|
||||
DofLayout { slots, nodes: self.nodes.len(), total }
|
||||
}
|
||||
}
|
||||
|
||||
impl DofLayout {
|
||||
pub fn builder() -> LayoutBuilder {
|
||||
LayoutBuilder::default()
|
||||
}
|
||||
|
||||
/// Every label in `labels` (sorted or not) with the same kinds.
|
||||
pub fn uniform(labels: impl IntoIterator<Item = u32>, kinds: DofSet) -> DofLayout {
|
||||
let mut b = LayoutBuilder::default();
|
||||
for l in labels {
|
||||
b.node(l, kinds);
|
||||
}
|
||||
b.build()
|
||||
}
|
||||
|
||||
/// Total unknowns.
|
||||
pub fn dofs(&self) -> usize {
|
||||
self.total
|
||||
}
|
||||
|
||||
/// Mesh nodes (slots `0..nodes()`).
|
||||
pub fn nodes(&self) -> usize {
|
||||
self.nodes
|
||||
}
|
||||
|
||||
/// Mesh nodes and extra points.
|
||||
pub fn slots(&self) -> usize {
|
||||
self.slots.len()
|
||||
}
|
||||
|
||||
/// Unknowns of the mesh nodes; the extra points' follow them.
|
||||
pub fn node_dofs(&self) -> usize {
|
||||
self.slots.get(self.nodes).map_or(self.total, |s| s.first)
|
||||
}
|
||||
|
||||
/// The slot of extra point `k`.
|
||||
pub fn point_slot(&self, k: usize) -> usize {
|
||||
self.nodes + k
|
||||
}
|
||||
|
||||
/// The slot of a mesh node label.
|
||||
pub fn node(&self, label: u32) -> Option<usize> {
|
||||
self.slots.get(..self.nodes)?.binary_search_by_key(&label, |s| s.label).ok()
|
||||
}
|
||||
|
||||
/// The slot of a label: a mesh node, else an extra point.
|
||||
pub fn slot(&self, label: u32) -> Option<usize> {
|
||||
self.node(label).or_else(|| self.slots.get(self.nodes..)?.iter().position(|s| s.label == label).map(|k| self.nodes + k))
|
||||
}
|
||||
|
||||
pub fn label(&self, slot: usize) -> Option<u32> {
|
||||
self.slots.get(slot).map(|s| s.label)
|
||||
}
|
||||
|
||||
/// The kinds a slot carries.
|
||||
pub fn kinds(&self, slot: usize) -> DofSet {
|
||||
self.slots.get(slot).map_or(DofSet::EMPTY, |s| s.kinds)
|
||||
}
|
||||
|
||||
/// A slot's unknowns (empty for a slot that does not exist).
|
||||
pub fn range(&self, slot: usize) -> Range<usize> {
|
||||
self.slots.get(slot).map_or(0..0, |s| s.first..s.first + s.kinds.len())
|
||||
}
|
||||
|
||||
/// The global unknown of `kind` at `slot`, if the slot carries it.
|
||||
pub fn dof(&self, slot: usize, kind: DofKind) -> Option<usize> {
|
||||
let s = self.slots.get(slot)?;
|
||||
s.kinds.position(kind).map(|p| s.first + p)
|
||||
}
|
||||
|
||||
/// The slot and kind of a global unknown.
|
||||
pub fn locate(&self, dof: usize) -> Option<(usize, DofKind)> {
|
||||
if dof >= self.total {
|
||||
return None;
|
||||
}
|
||||
let slot = self.slots.partition_point(|s| s.first <= dof).checked_sub(1)?;
|
||||
let s = self.slots.get(slot)?;
|
||||
let kind = s.kinds.iter().nth(dof - s.first)?;
|
||||
Some((slot, kind))
|
||||
}
|
||||
|
||||
/// The global unknowns of an element: its slots' `kinds`, node by node, `None` when a slot
|
||||
/// lacks one of them.
|
||||
pub fn element_dofs(&self, slots: &[usize], kinds: DofSet) -> Option<Vec<usize>> {
|
||||
let mut out = Vec::with_capacity(slots.len() * kinds.len());
|
||||
for &slot in slots {
|
||||
for k in kinds.iter() {
|
||||
out.push(self.dof(slot, k)?);
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `Some(s)` when every slot carries the same `s` kinds, so unknown `s · slot + offset` is
|
||||
/// the `offset`-th kind of `slot` (node-major vectors of `s`-vectors).
|
||||
pub fn stride(&self) -> Option<usize> {
|
||||
let first = self.slots.first()?.kinds;
|
||||
self.slots.iter().all(|s| s.kinds == first).then_some(first.len())
|
||||
}
|
||||
|
||||
/// What a global unknown is, in the user's terms.
|
||||
pub fn describe(&self, dof: usize) -> String {
|
||||
match self.locate(dof) {
|
||||
Some((slot, kind)) => {
|
||||
let what = if slot < self.nodes { "node" } else { "point" };
|
||||
format!("{what} {} in {}", self.label(slot).unwrap_or(0), kind.describe())
|
||||
}
|
||||
None => format!("unknown degree of freedom {dof}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kinds_numbers_and_sets() {
|
||||
for k in DofKind::ALL {
|
||||
assert_eq!(DofKind::from_number(k.number()), Some(k));
|
||||
}
|
||||
assert_eq!(DofKind::from_number(1), Some(DofKind::Ux));
|
||||
assert_eq!(DofKind::from_number(6), Some(DofKind::Rz));
|
||||
assert_eq!(DofKind::from_number(11), Some(DofKind::T));
|
||||
assert_eq!(DofKind::from_number(0), None);
|
||||
assert_eq!(DofKind::from_number(7), None);
|
||||
assert_eq!(DofKind::from_direction(4), Some(DofKind::Ry));
|
||||
assert_eq!(DofKind::from_direction(6), None);
|
||||
let s = DofSet::of(&[DofKind::T, DofKind::Ux, DofKind::Uy, DofKind::Uz]);
|
||||
assert_eq!(s.len(), 4);
|
||||
assert_eq!(s.iter().collect::<Vec<_>>(), vec![DofKind::Ux, DofKind::Uy, DofKind::Uz, DofKind::T]);
|
||||
assert_eq!(s.position(DofKind::T), Some(3));
|
||||
assert_eq!(s.position(DofKind::Rx), None);
|
||||
assert!(DofSet::STRUCTURAL.includes(DofSet::TRANSLATIONS));
|
||||
assert!(!DofSet::TRANSLATIONS.includes(DofSet::STRUCTURAL));
|
||||
assert_eq!(DofSet::TRANSLATIONS.union(DofSet::ROTATIONS), DofSet::STRUCTURAL);
|
||||
assert_eq!(ElementDofs::SOLID.total(8), 24);
|
||||
let hybrid = ElementDofs { node: DofSet::TRANSLATIONS, internal: Internal { incompatible_modes: 0, pressure: 1 } };
|
||||
assert_eq!(hybrid.total(8), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_numbers_nodes_by_label_then_kind_then_points() {
|
||||
let mut b = DofLayout::builder();
|
||||
// A shell on 7 and 3, a solid on 3, 5 and 7, a heat element on 5; a reference point.
|
||||
b.element(&[7, 3], ElementDofs::new(DofSet::STRUCTURAL));
|
||||
b.element(&[3, 5, 7], ElementDofs::SOLID);
|
||||
b.node(5, DofSet::TEMPERATURE);
|
||||
b.point(100, DofSet::STRUCTURAL);
|
||||
let l = b.build();
|
||||
assert_eq!((l.nodes(), l.slots(), l.dofs(), l.node_dofs()), (3, 4, 22, 16));
|
||||
assert_eq!(l.node(3), Some(0));
|
||||
assert_eq!(l.node(5), Some(1));
|
||||
assert_eq!(l.node(100), None);
|
||||
assert_eq!(l.slot(100), Some(3));
|
||||
assert_eq!(l.range(1), 6..10);
|
||||
assert_eq!(l.dof(1, DofKind::T), Some(9));
|
||||
assert_eq!(l.dof(1, DofKind::Rx), None);
|
||||
assert_eq!(l.dof(2, DofKind::Rz), Some(15));
|
||||
assert_eq!(l.dof(3, DofKind::Ux), Some(16));
|
||||
assert_eq!(l.locate(9), Some((1, DofKind::T)));
|
||||
assert_eq!(l.locate(16), Some((3, DofKind::Ux)));
|
||||
assert_eq!(l.locate(22), None);
|
||||
for d in 0..l.dofs() {
|
||||
let (slot, kind) = l.locate(d).unwrap();
|
||||
assert_eq!(l.dof(slot, kind), Some(d));
|
||||
}
|
||||
assert_eq!(l.element_dofs(&[0, 1], DofSet::TRANSLATIONS), Some(vec![0, 1, 2, 6, 7, 8]));
|
||||
assert_eq!(l.element_dofs(&[0, 1], DofSet::STRUCTURAL), None);
|
||||
assert_eq!(l.describe(9), "node 5 in temperature");
|
||||
assert_eq!(l.describe(19), "point 100 in rotation about X");
|
||||
// The same needs in another order give the same layout.
|
||||
let mut c = DofLayout::builder();
|
||||
c.point(100, DofSet::STRUCTURAL).node(5, DofSet::TEMPERATURE).element(&[5, 7, 3], ElementDofs::SOLID);
|
||||
c.element(&[3, 7], ElementDofs::new(DofSet::STRUCTURAL));
|
||||
assert_eq!(c.build(), l);
|
||||
// A uniform translational layout is the classic 3 × node numbering.
|
||||
let u = DofLayout::uniform([4, 2, 9], DofSet::TRANSLATIONS);
|
||||
assert_eq!(u.dof(2, DofKind::Uy), Some(7));
|
||||
assert_eq!(u.dofs(), 9);
|
||||
assert_eq!(u.range(7), 0..0);
|
||||
assert_eq!(u.locate(usize::MAX), None);
|
||||
assert_eq!(u.stride(), Some(3));
|
||||
assert_eq!(l.stride(), None);
|
||||
assert_eq!(DofLayout::default().stride(), None);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,8 @@
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
|
||||
use crate::dense::{Cholesky, Matrix, inverse3};
|
||||
use crate::dense::{Cholesky, Matrix, inverse3, positive_inverse};
|
||||
use crate::dof::{DofSet, ElementDofs, Internal};
|
||||
use crate::material::{Elasticity, stress};
|
||||
use crate::quadrature::{Point, Rule};
|
||||
use crate::shape::{self, MAX_NODES, Shape};
|
||||
@@ -144,6 +145,13 @@ pub enum Formulation {
|
||||
}
|
||||
|
||||
impl Formulation {
|
||||
/// The unknowns of a solid element so formulated: three translations per node, and the
|
||||
/// incompatible modes it condenses out.
|
||||
pub fn dofs(self) -> ElementDofs {
|
||||
let incompatible_modes = if self == Formulation::IncompatibleModes { INCOMPATIBLE_DOFS } else { 0 };
|
||||
ElementDofs { node: DofSet::TRANSLATIONS, internal: Internal { incompatible_modes, pressure: 0 } }
|
||||
}
|
||||
|
||||
/// The formulation a model section asks for, if this element type has it.
|
||||
pub fn from_section(kind: ElementKind, f: &modelcraft_model::Formulation) -> Result<Formulation> {
|
||||
let ty = element_type(kind)?;
|
||||
@@ -220,6 +228,11 @@ impl<'a> Solid<'a> {
|
||||
Ok(Solid { ty, xyz, formulation })
|
||||
}
|
||||
|
||||
/// The element's unknowns, in the order of its matrices.
|
||||
pub fn dofs(&self) -> ElementDofs {
|
||||
self.formulation.dofs()
|
||||
}
|
||||
|
||||
/// The stiffness integration rule.
|
||||
pub fn rule(&self) -> Rule {
|
||||
match (self.formulation, self.ty.reduced) {
|
||||
@@ -249,11 +262,7 @@ impl<'a> Solid<'a> {
|
||||
fn gradients(&self, xi: [f64; 3]) -> Result<Gradients> {
|
||||
let shape = (self.ty.shape)(xi);
|
||||
let j = self.jacobian(&shape);
|
||||
let (inv, det) = inverse3(&j).ok_or_else(|| self.degenerate())?;
|
||||
let norms: f64 = j.iter().map(|r| (r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt()).product();
|
||||
if !(det > 1e-12 * norms) {
|
||||
return Err(self.degenerate());
|
||||
}
|
||||
let (inv, det) = positive_inverse(&j).ok_or_else(|| self.degenerate())?;
|
||||
let mut dx = [[0.0; 3]; MAX_NODES];
|
||||
for (g, d) in dx.iter_mut().zip(shape.derivatives()) {
|
||||
for b in 0..3 {
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Second-order forward-mode automatic differentiation in three variables.
|
||||
//!
|
||||
//! A [`Jet`] carries a value with its gradient and Hessian with respect to the natural
|
||||
//! coordinates (ξ, η, ζ). Arithmetic propagates them by the product and quotient rules, so a
|
||||
//! shape function written once as a formula yields its exact first and second derivatives (to
|
||||
//! round-off, no finite differences): the hyper-dual / truncated Taylor arithmetic of A. Griewank,
|
||||
//! A. Walther, *Evaluating Derivatives*, 2nd ed., SIAM 2008, ch. 13.
|
||||
|
||||
use std::ops::{Add, Div, Mul, Neg, Sub};
|
||||
|
||||
/// A value, its gradient and its (symmetric) Hessian.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Jet {
|
||||
pub v: f64,
|
||||
pub g: [f64; 3],
|
||||
pub h: [[f64; 3]; 3],
|
||||
}
|
||||
|
||||
impl Jet {
|
||||
pub const fn constant(v: f64) -> Jet {
|
||||
Jet { v, g: [0.0; 3], h: [[0.0; 3]; 3] }
|
||||
}
|
||||
|
||||
/// The independent variable `k` (0–2) at value `v`.
|
||||
pub fn var(v: f64, k: usize) -> Jet {
|
||||
let mut j = Jet::constant(v);
|
||||
if let Some(g) = j.g.get_mut(k) {
|
||||
*g = 1.0;
|
||||
}
|
||||
j
|
||||
}
|
||||
|
||||
/// The three natural coordinates as variables.
|
||||
pub fn point(xi: [f64; 3]) -> [Jet; 3] {
|
||||
[Jet::var(xi[0], 0), Jet::var(xi[1], 1), Jet::var(xi[2], 2)]
|
||||
}
|
||||
|
||||
/// 1 / self; the Hessian term follows from (1/b)'' = 2 b'b'ᵀ / b³ − b'' / b².
|
||||
pub fn recip(self) -> Jet {
|
||||
let r = 1.0 / self.v;
|
||||
let r2 = r * r;
|
||||
let mut out = Jet::constant(r);
|
||||
for a in 0..3 {
|
||||
out.g[a] = -self.g[a] * r2;
|
||||
for b in 0..3 {
|
||||
out.h[a][b] = 2.0 * self.g[a] * self.g[b] * r2 * r - self.h[a][b] * r2;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn map(self, other: Jet, f: impl Fn(f64, f64) -> f64) -> Jet {
|
||||
let mut out = Jet::constant(f(self.v, other.v));
|
||||
for a in 0..3 {
|
||||
out.g[a] = f(self.g[a], other.g[a]);
|
||||
for b in 0..3 {
|
||||
out.h[a][b] = f(self.h[a][b], other.h[a][b]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn scale(self, s: f64) -> Jet {
|
||||
Jet { v: self.v * s, g: self.g.map(|x| x * s), h: self.h.map(|r| r.map(|x| x * s)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Jet {
|
||||
type Output = Jet;
|
||||
fn add(self, o: Jet) -> Jet {
|
||||
self.map(o, |a, b| a + b)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Jet {
|
||||
type Output = Jet;
|
||||
fn sub(self, o: Jet) -> Jet {
|
||||
self.map(o, |a, b| a - b)
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Jet {
|
||||
type Output = Jet;
|
||||
fn neg(self) -> Jet {
|
||||
self.scale(-1.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for Jet {
|
||||
type Output = Jet;
|
||||
fn mul(self, o: Jet) -> Jet {
|
||||
let mut out = Jet::constant(self.v * o.v);
|
||||
for a in 0..3 {
|
||||
out.g[a] = self.v * o.g[a] + o.v * self.g[a];
|
||||
for b in 0..3 {
|
||||
out.h[a][b] = self.v * o.h[a][b] + o.v * self.h[a][b] + self.g[a] * o.g[b] + o.g[a] * self.g[b];
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for Jet {
|
||||
type Output = Jet;
|
||||
#[allow(clippy::suspicious_arithmetic_impl)]
|
||||
fn div(self, o: Jet) -> Jet {
|
||||
self * o.recip()
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<f64> for Jet {
|
||||
type Output = Jet;
|
||||
fn add(mut self, c: f64) -> Jet {
|
||||
self.v += c;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<f64> for Jet {
|
||||
type Output = Jet;
|
||||
fn sub(mut self, c: f64) -> Jet {
|
||||
self.v -= c;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f64> for Jet {
|
||||
type Output = Jet;
|
||||
fn mul(self, c: f64) -> Jet {
|
||||
self.scale(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Jet> for f64 {
|
||||
type Output = Jet;
|
||||
fn add(self, j: Jet) -> Jet {
|
||||
j + self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Jet> for f64 {
|
||||
type Output = Jet;
|
||||
fn sub(self, j: Jet) -> Jet {
|
||||
-j + self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Jet> for f64 {
|
||||
type Output = Jet;
|
||||
fn mul(self, j: Jet) -> Jet {
|
||||
j.scale(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn products_and_quotients_differentiate_exactly() {
|
||||
let [x, y, z] = Jet::point([0.3, -0.7, 1.9]);
|
||||
// f = x² y / (1 + z) + 3 x z.
|
||||
let f = x * x * y / (1.0 + z) + 3.0 * x * z;
|
||||
let (xv, yv, zv) = (0.3, -0.7, 1.9);
|
||||
let d = 1.0 + zv;
|
||||
assert!((f.v - (xv * xv * yv / d + 3.0 * xv * zv)).abs() < 1e-15);
|
||||
let g = [2.0 * xv * yv / d + 3.0 * zv, xv * xv / d, -xv * xv * yv / (d * d) + 3.0 * xv];
|
||||
let h = [
|
||||
[2.0 * yv / d, 2.0 * xv / d, -2.0 * xv * yv / (d * d) + 3.0],
|
||||
[2.0 * xv / d, 0.0, -xv * xv / (d * d)],
|
||||
[-2.0 * xv * yv / (d * d) + 3.0, -xv * xv / (d * d), 2.0 * xv * xv * yv / (d * d * d)],
|
||||
];
|
||||
for a in 0..3 {
|
||||
assert!((f.g[a] - g[a]).abs() < 1e-14, "g{a}");
|
||||
for b in 0..3 {
|
||||
assert!((f.h[a][b] - h[a][b]).abs() < 1e-14, "h{a}{b}: {} vs {}", f.h[a][b], h[a][b]);
|
||||
}
|
||||
}
|
||||
let s = 2.0 - x * 0.5 - (y - 1.0);
|
||||
assert_eq!(s.g, [-0.5, -1.0, 0.0]);
|
||||
assert_eq!(s.v, 2.0 - 0.15 + 1.7);
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -3,8 +3,15 @@
|
||||
//! Pure functions over element geometry and material constants, with no solver loop and no
|
||||
//! global state, so the implicit solver (`modelcraft-solver`) and the explicit solver that follows
|
||||
//! it use the same element technology:
|
||||
//! - [`dof`]: degree-of-freedom kinds, what elements declare, and the global numbering
|
||||
//! ([`DofLayout`]) every solver assembles on;
|
||||
//! - [`quadrature`]: Gauss–Legendre and tetrahedron rules;
|
||||
//! - [`shape`]: shape functions of tet4, tet10, wedge6, hex8, hex20;
|
||||
//! - [`basis`]: the shape-function library of every family (lines, triangles, quadrilaterals,
|
||||
//! tetrahedra, wedges, pyramids, hexahedra) with first and second derivatives and their
|
||||
//! physical gradients, Hessians and Laplacians; [`jet`] is the automatic differentiation behind
|
||||
//! the second derivatives;
|
||||
//! - [`shape`]: the hand-written first-order functions of tet4, tet10, wedge6, hex8, hex20 that
|
||||
//! the solid kernels evaluate;
|
||||
//! - [`material`]: isotropic and orthotropic elasticity matrices, von Mises stress;
|
||||
//! - [`element`]: the element table, stiffness (full, reduced, incompatible modes), consistent
|
||||
//! and lumped mass, body forces, integration-point stresses and their extrapolation to nodes;
|
||||
@@ -13,21 +20,27 @@
|
||||
//! shape vectors and element frequency bounds for explicit dynamics;
|
||||
//! - [`dense`]: the small dense linear algebra inside an element.
|
||||
//!
|
||||
//! Conventions: three translational degrees of freedom per node, ordered node by node; stress
|
||||
//! Conventions: element matrices are ordered node by node, each node's degrees of freedom in
|
||||
//! [`DofKind`] order (three translations for solids; docs/fem.md); stress
|
||||
//! and strain 6-vectors in the order xx, yy, zz, xy, xz, yz with engineering shear strains.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
// Matrix kernels over fixed small dimensions read as the textbook formulas with index loops.
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
pub mod basis;
|
||||
pub mod dense;
|
||||
pub mod dof;
|
||||
pub mod element;
|
||||
pub mod face;
|
||||
pub mod jet;
|
||||
pub mod material;
|
||||
pub mod quadrature;
|
||||
pub mod shape;
|
||||
pub mod uniform;
|
||||
|
||||
pub use basis::Basis;
|
||||
pub use dof::{DofKind, DofLayout, DofSet, ElementDofs};
|
||||
pub use element::{ELEMENT_TYPES, ElementType, Family, Formulation, PointStress, Solid, element_type};
|
||||
pub use material::{Elasticity, elasticity, principal, von_mises};
|
||||
|
||||
|
||||
+5
-26
@@ -21,8 +21,8 @@
|
||||
|
||||
use modelcraft_model::ElementKind;
|
||||
|
||||
/// Most nodes an element in this crate has (hex20).
|
||||
pub const MAX_NODES: usize = 20;
|
||||
/// 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)]
|
||||
@@ -157,33 +157,12 @@ pub fn hex20(xi: [f64; 3]) -> Shape {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn nodes_of(kind: &str) -> Vec<[f64; 3]> {
|
||||
match kind {
|
||||
"tet4" => TET_CORNERS.to_vec(),
|
||||
"tet10" => {
|
||||
let mut v = TET_CORNERS.to_vec();
|
||||
for e in ElementKind::Tet10.edges() {
|
||||
v.push([0, 1, 2].map(|k| (TET_CORNERS[e[0]][k] + TET_CORNERS[e[1]][k]) / 2.0));
|
||||
}
|
||||
v
|
||||
}
|
||||
"hex8" => HEX_CORNERS.to_vec(),
|
||||
"wedge6" => WEDGE_CORNERS.to_vec(),
|
||||
_ => {
|
||||
let mut v = HEX_CORNERS.to_vec();
|
||||
for e in ElementKind::Hex20.edges() {
|
||||
v.push([0, 1, 2].map(|k| (HEX_CORNERS[e[0]][k] + HEX_CORNERS[e[1]][k]) / 2.0));
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = nodes_of(name);
|
||||
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() {
|
||||
@@ -197,7 +176,7 @@ mod tests {
|
||||
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();
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user