#![cfg(feature = "std")]
use std::collections::BTreeMap;
use crate::hyperelastic::HyperelasticModel;
use crate::linear_elastic_fem::{
hyperelastic_volumetric_modulus, BoundaryConditions, CorotationalConfig, CorotationalSolution,
ElasticMaterial, FemError, FemSolution, Preconditioner, SolverConfig, StressTensor,
RESIDUAL_NORM_FLOOR,
};
use crate::math::{Fix128, Mat3Fix, PolarError, Vec3Fix};
use crate::sdf_fem_mesh::SdfTetMesh;
const NODES_PER_ELEMENT: usize = 20;
const QUADRATURE_POINTS: usize = 24;
const EDGES: [(usize, usize); 6] = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
const FACES: [(usize, usize, usize); 4] = [(1, 2, 3), (0, 2, 3), (0, 1, 3), (0, 1, 2)];
fn dyadic(numer: i64, shift: u32) -> Fix128 {
let r = (numer as i128) << (64 - shift);
Fix128::from_raw((r >> 64) as i64, r as u64)
}
fn sub3(a: [Fix128; 3], b: [Fix128; 3]) -> [Fix128; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross3(a: [Fix128; 3], b: [Fix128; 3]) -> [Fix128; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot3(a: [Fix128; 3], b: [Fix128; 3]) -> Fix128 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn div3(a: [Fix128; 3], d: Fix128) -> [Fix128; 3] {
[a[0] / d, a[1] / d, a[2] / d]
}
struct Orbit {
s31: bool,
a: (i64, u32),
b: (i64, u32),
weight: (i64, i64),
}
const ORBITS: [Orbit; 5] = [
Orbit {
s31: true,
a: (1, 0),
b: (0, 0),
weight: (1, 300),
},
Orbit {
s31: true,
a: (5, 3),
b: (1, 3),
weight: (8, 63),
},
Orbit {
s31: true,
a: (1, 4),
b: (5, 4),
weight: (256, 1575),
},
Orbit {
s31: false,
a: (1, 1),
b: (0, 0),
weight: (1, 45),
},
Orbit {
s31: false,
a: (3, 3),
b: (1, 3),
weight: (-16, 315),
},
];
const S22_PATTERN: [[u8; 4]; 6] = [
[0, 0, 1, 1],
[0, 1, 0, 1],
[0, 1, 1, 0],
[1, 0, 0, 1],
[1, 0, 1, 0],
[1, 1, 0, 0],
];
fn quadrature_points() -> [[Fix128; 4]; QUADRATURE_POINTS] {
let mut points = [[Fix128::ZERO; 4]; QUADRATURE_POINTS];
let mut at = 0usize;
for orbit in &ORBITS {
let a = dyadic(orbit.a.0, orbit.a.1);
let b = dyadic(orbit.b.0, orbit.b.1);
if orbit.s31 {
for i in 0..4 {
let mut p = [b; 4];
p[i] = a;
points[at] = p;
at += 1;
}
} else {
for pattern in S22_PATTERN {
let mut p = [a; 4];
for (slot, &s) in p.iter_mut().zip(pattern.iter()) {
*slot = if s == 0 { a } else { b };
}
points[at] = p;
at += 1;
}
}
}
debug_assert!(at == QUADRATURE_POINTS);
points
}
fn quadrature_weights() -> [Fix128; QUADRATURE_POINTS] {
let mut weights = [Fix128::ZERO; QUADRATURE_POINTS];
let mut at = 0usize;
for orbit in &ORBITS {
let w = Fix128::from_int(orbit.weight.0) / Fix128::from_int(orbit.weight.1);
let count = if orbit.s31 { 4 } else { 6 };
for _ in 0..count {
weights[at] = w;
at += 1;
}
}
debug_assert!(at == QUADRATURE_POINTS);
weights
}
#[derive(Clone, Copy, Debug)]
struct CubicElement {
nodes: [usize; NODES_PER_ELEMENT],
grad_lambda: [[Fix128; 3]; 4],
volume: Fix128,
}
#[derive(Clone, Debug)]
pub struct CubicMesh {
corner_count: usize,
positions: Vec<[Fix128; 3]>,
elements: Vec<CubicElement>,
edge_nodes: BTreeMap<(u32, u32), (u32, u32)>,
face_nodes: BTreeMap<(u32, u32, u32), u32>,
interior_positions_exact: bool,
}
fn divide_by_three(numerator: Fix128) -> (Fix128, bool) {
let three = Fix128::from_int(3);
let q = numerator / three;
(q, q * three == numerator)
}
impl CubicMesh {
pub fn from_tet_mesh(mesh: &SdfTetMesh) -> Result<Self, FemError> {
let corner_count = mesh.vertices.len();
if corner_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
let six = Fix128::from_int(6);
let two = Fix128::from_int(2);
let mut interior_positions_exact = true;
let mut positions: Vec<[Fix128; 3]> = mesh
.vertices
.iter()
.map(|q| {
[
Fix128::from_f32(q[0]),
Fix128::from_f32(q[1]),
Fix128::from_f32(q[2]),
]
})
.collect();
let mut edge_nodes: BTreeMap<(u32, u32), (u32, u32)> = BTreeMap::new();
let mut face_nodes: BTreeMap<(u32, u32, u32), u32> = BTreeMap::new();
let mut elements = Vec::with_capacity(mesh.tets.len());
for (t, tet) in mesh.tets.iter().enumerate() {
let mut corner = [0usize; 4];
let mut p = [[Fix128::ZERO; 3]; 4];
for (i, &v) in tet.vertices.iter().enumerate() {
let idx = v as usize;
if idx >= corner_count {
return Err(FemError::VertexOutOfRange {
vertex: v,
vertex_count: corner_count,
});
}
corner[i] = idx;
p[i] = positions[idx];
}
let e1 = sub3(p[1], p[0]);
let e2 = sub3(p[2], p[0]);
let e3 = sub3(p[3], p[0]);
let det = dot3(e1, cross3(e2, e3));
if det.is_zero() {
return Err(FemError::DegenerateElement { tet: t });
}
let g1 = div3(cross3(e2, e3), det);
let g2 = div3(cross3(e3, e1), det);
let g3 = div3(cross3(e1, e2), det);
let g0 = [
-(g1[0] + g2[0] + g3[0]),
-(g1[1] + g2[1] + g3[1]),
-(g1[2] + g2[2] + g3[2]),
];
let mut nodes = [0usize; NODES_PER_ELEMENT];
nodes[..4].copy_from_slice(&corner);
for (e, &(i, j)) in EDGES.iter().enumerate() {
let (va, vb) = (tet.vertices[i], tet.vertices[j]);
let forward = va <= vb;
let key = if forward { (va, vb) } else { (vb, va) };
let next = u32::try_from(positions.len()).map_err(|_| FemError::EmptyMesh)?;
let pair = *edge_nodes.entry(key).or_insert((next, next + 1));
if pair.0 == next {
let (lo, hi) = if forward { (p[i], p[j]) } else { (p[j], p[i]) };
for (first, second) in [(lo, hi), (hi, lo)] {
let mut at = [Fix128::ZERO; 3];
for (axis, slot) in at.iter_mut().enumerate() {
let (q, exact) = divide_by_three(two * first[axis] + second[axis]);
*slot = q;
interior_positions_exact &= exact;
}
positions.push(at);
}
}
let (near_lo, near_hi) = pair;
let (near_i, near_j) = if forward {
(near_lo, near_hi)
} else {
(near_hi, near_lo)
};
nodes[4 + 2 * e] = near_i as usize;
nodes[5 + 2 * e] = near_j as usize;
}
for (f, &(i, j, k)) in FACES.iter().enumerate() {
let mut tri = [tet.vertices[i], tet.vertices[j], tet.vertices[k]];
tri.sort_unstable();
let key = (tri[0], tri[1], tri[2]);
let next = u32::try_from(positions.len()).map_err(|_| FemError::EmptyMesh)?;
let node = *face_nodes.entry(key).or_insert(next);
if node == next {
let (pa, pb, pc) = (p[i], p[j], p[k]);
let mut at = [Fix128::ZERO; 3];
for (axis, slot) in at.iter_mut().enumerate() {
let (q, exact) = divide_by_three(pa[axis] + pb[axis] + pc[axis]);
*slot = q;
interior_positions_exact &= exact;
}
positions.push(at);
}
nodes[16 + f] = node as usize;
}
elements.push(CubicElement {
nodes,
grad_lambda: [g0, g1, g2, g3],
volume: det.abs() / six,
});
}
Ok(Self {
corner_count,
positions,
elements,
edge_nodes,
face_nodes,
interior_positions_exact,
})
}
#[must_use]
pub const fn interior_node_positions_are_exact(&self) -> bool {
self.interior_positions_exact
}
#[must_use]
pub fn node_count(&self) -> usize {
self.positions.len()
}
#[must_use]
pub const fn corner_count(&self) -> usize {
self.corner_count
}
#[must_use]
pub fn edge_node_count(&self) -> usize {
self.edge_nodes.len() * 2
}
#[must_use]
pub fn face_node_count(&self) -> usize {
self.face_nodes.len()
}
#[must_use]
pub fn element_count(&self) -> usize {
self.elements.len()
}
#[must_use]
pub fn node_position(&self, node: u32) -> Option<[Fix128; 3]> {
self.positions.get(node as usize).copied()
}
#[must_use]
pub fn edge_nodes(&self, a: u32, b: u32) -> Option<(u32, u32)> {
let key = if a <= b { (a, b) } else { (b, a) };
self.edge_nodes.get(&key).copied()
}
#[must_use]
pub fn face_node(&self, a: u32, b: u32, c: u32) -> Option<u32> {
let mut tri = [a, b, c];
tri.sort_unstable();
self.face_nodes.get(&(tri[0], tri[1], tri[2])).copied()
}
#[must_use]
pub fn element_nodes(&self, element: usize) -> Option<[u32; NODES_PER_ELEMENT]> {
let e = self.elements.get(element)?;
let mut out = [0u32; NODES_PER_ELEMENT];
for (slot, &n) in e.nodes.iter().enumerate() {
out[slot] = u32::try_from(n).ok()?;
}
Some(out)
}
}
#[must_use]
pub fn shape_values(lambda: &[Fix128; 4]) -> [Fix128; NODES_PER_ELEMENT] {
let half = dyadic(1, 1);
let nine_halves = dyadic(9, 1);
let three = Fix128::from_int(3);
let two = Fix128::from_int(2);
let twenty_seven = Fix128::from_int(27);
let mut n = [Fix128::ZERO; NODES_PER_ELEMENT];
for (slot, &li) in n.iter_mut().zip(lambda.iter()) {
*slot = half * li * (three * li - Fix128::ONE) * (three * li - two);
}
for (e, &(i, j)) in EDGES.iter().enumerate() {
let (li, lj) = (lambda[i], lambda[j]);
n[4 + 2 * e] = nine_halves * li * lj * (three * li - Fix128::ONE);
n[5 + 2 * e] = nine_halves * lj * li * (three * lj - Fix128::ONE);
}
for (f, &(i, j, k)) in FACES.iter().enumerate() {
n[16 + f] = twenty_seven * lambda[i] * lambda[j] * lambda[k];
}
n
}
fn shape_gradients(
element: &CubicElement,
lambda: &[Fix128; 4],
) -> [[Fix128; 3]; NODES_PER_ELEMENT] {
let half = dyadic(1, 1);
let nine_halves = dyadic(9, 1);
let two = Fix128::from_int(2);
let three = Fix128::from_int(3);
let six = Fix128::from_int(6);
let eighteen = Fix128::from_int(18);
let twenty_seven = Fix128::from_int(27);
let mut grad = [[Fix128::ZERO; 3]; NODES_PER_ELEMENT];
for (i, (out, gl)) in grad
.iter_mut()
.take(4)
.zip(element.grad_lambda.iter())
.enumerate()
{
let li = lambda[i];
let s = half * (twenty_seven * li * li - eighteen * li + two);
for (o, c) in out.iter_mut().zip(gl.iter()) {
*o = s * *c;
}
}
for (e, &(i, j)) in EDGES.iter().enumerate() {
let (li, lj) = (lambda[i], lambda[j]);
let (gi, gj) = (element.grad_lambda[i], element.grad_lambda[j]);
let near_i = (
nine_halves * lj * (six * li - Fix128::ONE),
nine_halves * li * (three * li - Fix128::ONE),
);
let near_j = (
nine_halves * li * (six * lj - Fix128::ONE),
nine_halves * lj * (three * lj - Fix128::ONE),
);
for (axis, (ci, cj)) in gi.iter().zip(gj.iter()).enumerate() {
grad[4 + 2 * e][axis] = near_i.0 * *ci + near_i.1 * *cj;
grad[5 + 2 * e][axis] = near_j.0 * *cj + near_j.1 * *ci;
}
}
for (f, &(i, j, k)) in FACES.iter().enumerate() {
let (li, lj, lk) = (lambda[i], lambda[j], lambda[k]);
let (gi, gj, gk) = (
element.grad_lambda[i],
element.grad_lambda[j],
element.grad_lambda[k],
);
for axis in 0..3 {
grad[16 + f][axis] =
twenty_seven * (lj * lk * gi[axis] + li * lk * gj[axis] + li * lj * gk[axis]);
}
}
grad
}
fn stress_at(
element: &CubicElement,
grad: &[[Fix128; 3]; NODES_PER_ELEMENT],
u: &[Fix128],
lambda: Fix128,
mu: Fix128,
) -> StressTensor {
let mut exx = Fix128::ZERO;
let mut eyy = Fix128::ZERO;
let mut ezz = Fix128::ZERO;
let mut gxy = Fix128::ZERO;
let mut gyz = Fix128::ZERO;
let mut gzx = Fix128::ZERO;
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let base = node * 3;
let (ux, uy, uz) = (u[base], u[base + 1], u[base + 2]);
exx = exx + g[0] * ux;
eyy = eyy + g[1] * uy;
ezz = ezz + g[2] * uz;
gxy = gxy + g[1] * ux + g[0] * uy;
gyz = gyz + g[2] * uy + g[1] * uz;
gzx = gzx + g[2] * ux + g[0] * uz;
}
let trace = exx + eyy + ezz;
let two_mu = mu + mu;
StressTensor {
xx: lambda * trace + two_mu * exx,
yy: lambda * trace + two_mu * eyy,
zz: lambda * trace + two_mu * ezz,
xy: mu * gxy,
yz: mu * gyz,
zx: mu * gzx,
}
}
fn apply_stiffness(
elements: &[CubicElement],
points: &[[Fix128; 4]; QUADRATURE_POINTS],
weights: &[Fix128; QUADRATURE_POINTS],
u: &[Fix128],
lambda: Fix128,
mu: Fix128,
out: &mut [Fix128],
) {
out.fill(Fix128::ZERO);
for element in elements {
for (point, weight) in points.iter().zip(weights.iter()) {
let scale = element.volume * *weight;
let grad = shape_gradients(element, point);
let s = stress_at(element, &grad, u, lambda, mu);
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let base = node * 3;
out[base] = out[base] + scale * (g[0] * s.xx + g[1] * s.xy + g[2] * s.zx);
out[base + 1] = out[base + 1] + scale * (g[1] * s.yy + g[0] * s.xy + g[2] * s.yz);
out[base + 2] = out[base + 2] + scale * (g[2] * s.zz + g[1] * s.yz + g[0] * s.zx);
}
}
}
}
fn stiffness_diagonal(
elements: &[CubicElement],
points: &[[Fix128; 4]; QUADRATURE_POINTS],
weights: &[Fix128; QUADRATURE_POINTS],
lambda: Fix128,
mu: Fix128,
ndof: usize,
) -> Vec<Fix128> {
let mut diag = vec![Fix128::ZERO; ndof];
let lambda_2mu = lambda + mu + mu;
for element in elements {
for (point, weight) in points.iter().zip(weights.iter()) {
let scale = element.volume * *weight;
let grad = shape_gradients(element, point);
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let sq = [g[0] * g[0], g[1] * g[1], g[2] * g[2]];
let base = node * 3;
for axis in 0..3 {
let others = sq[(axis + 1) % 3] + sq[(axis + 2) % 3];
diag[base + axis] =
diag[base + axis] + scale * (lambda_2mu * sq[axis] + mu * others);
}
}
}
}
diag
}
fn dot(a: &[Fix128], b: &[Fix128]) -> Fix128 {
let mut acc = Fix128::ZERO;
for (x, y) in a.iter().zip(b.iter()) {
acc = acc + *x * *y;
}
acc
}
fn relative(residual: Fix128, b_norm: Fix128) -> Fix128 {
if b_norm.is_zero() {
Fix128::ZERO
} else {
residual / b_norm
}
}
pub fn solve_cubic(
mesh: &CubicMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &SolverConfig,
) -> Result<FemSolution, FemError> {
let node_count = mesh.node_count();
if node_count == 0 || mesh.elements.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(node, _, _) in boundary.prescribed().iter().chain(boundary.loads().iter()) {
if node as usize >= node_count {
return Err(FemError::VertexOutOfRange {
vertex: node,
vertex_count: node_count,
});
}
}
let (lambda, mu) = material.lame();
let ndof = node_count * 3;
let points = quadrature_points();
let weights = quadrature_weights();
let mut prescribed_value = vec![Fix128::ZERO; ndof];
let mut is_free = vec![true; ndof];
for &(node, axis, value) in boundary.prescribed() {
let d = node as usize * 3 + axis.index();
prescribed_value[d] = value;
is_free[d] = false;
}
if is_free.iter().filter(|f| !**f).count() < 6 {
return Err(FemError::UnderConstrained);
}
if is_free.iter().all(|f| !*f) {
return Ok(finish(
mesh,
&prescribed_value,
lambda,
mu,
0,
Fix128::ZERO,
config.relative_tolerance(),
));
}
let mut scratch = vec![Fix128::ZERO; ndof];
apply_stiffness(
&mesh.elements,
&points,
&weights,
&prescribed_value,
lambda,
mu,
&mut scratch,
);
let mut b = vec![Fix128::ZERO; ndof];
for d in 0..ndof {
if is_free[d] {
b[d] = -scratch[d];
}
}
for &(node, axis, force) in boundary.loads() {
let d = node as usize * 3 + axis.index();
if is_free[d] {
b[d] = b[d] + force;
}
}
let precond = match config.preconditioner() {
Preconditioner::None => vec![Fix128::ONE; ndof],
Preconditioner::JacobiScaled => {
let diag = stiffness_diagonal(&mesh.elements, &points, &weights, lambda, mu, ndof);
let mut sum = Fix128::ZERO;
let mut free = 0i64;
for (d, value) in diag.iter().enumerate() {
if is_free[d] {
sum = sum + *value;
free += 1;
}
}
if free == 0 || sum.is_zero() {
vec![Fix128::ONE; ndof]
} else {
let mean = sum / Fix128::from_int(free);
let mut m = vec![Fix128::ONE; ndof];
for (d, value) in diag.iter().enumerate() {
if is_free[d] && !value.is_zero() {
m[d] = mean / *value;
}
}
m
}
}
};
let mut x = vec![Fix128::ZERO; ndof];
let mut r = b.clone();
let mut z = vec![Fix128::ZERO; ndof];
for d in 0..ndof {
if is_free[d] {
z[d] = r[d] * precond[d];
}
}
let mut p = z.clone();
let mut rz = dot(&r, &z);
let b_norm = dot(&b, &b).sqrt();
let requested = config.relative_tolerance() * b_norm;
let target = if requested > RESIDUAL_NORM_FLOOR {
requested
} else {
RESIDUAL_NORM_FLOOR
};
let effective = relative(target, b_norm);
let mut iterations = 0u32;
let mut residual_norm = dot(&r, &r).sqrt();
let mut best_residual = residual_norm;
let mut since_improvement = 0u32;
while residual_norm > target {
if iterations >= config.max_iterations() {
return Err(FemError::NotConverged {
iterations,
relative_residual: relative(residual_norm, b_norm),
});
}
let window = {
let scaled =
config.stagnation_window_fraction() * Fix128::from_int(i64::from(iterations));
let scaled = if scaled.is_negative() {
0
} else {
u32::try_from(scaled.hi).unwrap_or(u32::MAX)
};
scaled.max(config.stagnation_min_window())
};
if since_improvement >= window {
return Err(FemError::Stagnated {
iterations,
relative_residual: relative(best_residual, b_norm),
without_improvement: since_improvement,
});
}
apply_stiffness(
&mesh.elements,
&points,
&weights,
&p,
lambda,
mu,
&mut scratch,
);
for (d, value) in scratch.iter_mut().enumerate() {
if !is_free[d] {
*value = Fix128::ZERO;
}
}
let pkp = dot(&p, &scratch);
if pkp <= Fix128::ZERO {
if iterations == 0 {
return Err(FemError::UnderConstrained);
}
return Err(FemError::Stagnated {
iterations,
relative_residual: relative(best_residual, b_norm),
without_improvement: since_improvement,
});
}
let alpha = rz / pkp;
for d in 0..ndof {
if is_free[d] {
x[d] = x[d] + alpha * p[d];
r[d] = r[d] - alpha * scratch[d];
}
}
for d in 0..ndof {
if is_free[d] {
z[d] = r[d] * precond[d];
}
}
let rz_next = dot(&r, &z);
let beta = rz_next / rz;
for d in 0..ndof {
if is_free[d] {
p[d] = z[d] + beta * p[d];
}
}
rz = rz_next;
residual_norm = dot(&r, &r).sqrt();
iterations += 1;
if residual_norm < best_residual - best_residual * config.stagnation_min_improvement() {
best_residual = residual_norm;
since_improvement = 0;
} else {
if residual_norm < best_residual {
best_residual = residual_norm;
}
since_improvement += 1;
}
}
let mut u = prescribed_value;
for d in 0..ndof {
if is_free[d] {
u[d] = x[d];
}
}
Ok(finish(
mesh,
&u,
lambda,
mu,
iterations,
relative(residual_norm, b_norm),
effective,
))
}
fn finish(
mesh: &CubicMesh,
u: &[Fix128],
lambda: Fix128,
mu: Fix128,
iterations: u32,
relative_residual: Fix128,
effective_relative_tolerance: Fix128,
) -> FemSolution {
let quarter = dyadic(1, 2);
let centroid = [quarter; 4];
let displacements = (0..mesh.node_count())
.map(|n| [u[n * 3], u[n * 3 + 1], u[n * 3 + 2]])
.collect();
let element_stress = mesh
.elements
.iter()
.map(|e| {
let grad = shape_gradients(e, ¢roid);
stress_at(e, &grad, u, lambda, mu)
})
.collect();
FemSolution {
displacements,
element_stress,
iterations,
relative_residual,
effective_relative_tolerance,
}
}
fn deformation_gradient_at(
element: &CubicElement,
grad: &[[Fix128; 3]; NODES_PER_ELEMENT],
u: &[Fix128],
) -> Mat3Fix {
let mut cols = [[Fix128::ZERO; 3]; 3];
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let base = node * 3;
let node_u = [u[base], u[base + 1], u[base + 2]];
for (b, gb) in g.iter().enumerate() {
for (a, ua) in node_u.iter().enumerate() {
cols[b][a] = cols[b][a] + *ua * *gb;
}
}
}
for (k, col) in cols.iter_mut().enumerate() {
col[k] = col[k] + Fix128::ONE;
}
Mat3Fix::from_cols(
Vec3Fix::new(cols[0][0], cols[0][1], cols[0][2]),
Vec3Fix::new(cols[1][0], cols[1][1], cols[1][2]),
Vec3Fix::new(cols[2][0], cols[2][1], cols[2][2]),
)
}
fn hyperelastic_stress(
model: &HyperelasticModel,
bulk_modulus: Fix128,
gradient: Mat3Fix,
) -> Option<(StressTensor, Mat3Fix)> {
let sigma = crate::hyperelastic::cauchy_stress(model, bulk_modulus, gradient)?;
let inverse_transpose = gradient.inverse()?.transpose();
let piola = sigma
.mul_mat(inverse_transpose)
.scale(gradient.determinant());
let cauchy = StressTensor {
xx: sigma.col0.x,
yy: sigma.col1.y,
zz: sigma.col2.z,
xy: sigma.col1.x,
yz: sigma.col2.y,
zx: sigma.col2.x,
};
Some((cauchy, piola))
}
#[derive(Clone, Copy, Debug)]
struct MaterialLaw {
model: HyperelasticModel,
bulk_modulus: Fix128,
}
fn material_internal_force(
elements: &[CubicElement],
points: &[[Fix128; 4]; QUADRATURE_POINTS],
weights: &[Fix128; QUADRATURE_POINTS],
u: &[Fix128],
law: MaterialLaw,
out: &mut [Fix128],
) -> Result<(), FemError> {
out.fill(Fix128::ZERO);
for (tet, element) in elements.iter().enumerate() {
for (point, weight) in points.iter().zip(weights.iter()) {
let scale = element.volume * *weight;
let grad = shape_gradients(element, point);
let gradient = deformation_gradient_at(element, &grad, u);
let (_, piola) = hyperelastic_stress(&law.model, law.bulk_modulus, gradient).ok_or(
FemError::RotationFailed {
tet,
cause: PolarError::Inverted,
},
)?;
let rows = [
[piola.col0.x, piola.col1.x, piola.col2.x],
[piola.col0.y, piola.col1.y, piola.col2.y],
[piola.col0.z, piola.col1.z, piola.col2.z],
];
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let base = node * 3;
for (axis, row) in rows.iter().enumerate() {
out[base + axis] =
out[base + axis] + scale * (row[0] * g[0] + row[1] * g[1] + row[2] * g[2]);
}
}
}
}
Ok(())
}
fn max_abs(v: &[Fix128]) -> Fix128 {
let mut worst = Fix128::ZERO;
for entry in v {
let a = entry.abs();
if a > worst {
worst = a;
}
}
worst
}
const POLAR_DET_FLOOR: Fix128 = Fix128::from_raw(0, 1 << 44);
struct RotatedOperator<'a> {
elements: &'a [CubicElement],
points: &'a [[Fix128; 4]; QUADRATURE_POINTS],
weights: &'a [Fix128; QUADRATURE_POINTS],
lame: (Fix128, Fix128),
rotations: Vec<Mat3Fix>,
}
impl<'a> RotatedOperator<'a> {
fn new(
elements: &'a [CubicElement],
points: &'a [[Fix128; 4]; QUADRATURE_POINTS],
weights: &'a [Fix128; QUADRATURE_POINTS],
lame: (Fix128, Fix128),
) -> Self {
Self {
elements,
points,
weights,
lame,
rotations: vec![Mat3Fix::IDENTITY; elements.len()],
}
}
fn refresh(&mut self, u: &[Fix128], polar_iterations: u32) {
let centroid = [dyadic(1, 2); 4];
for (slot, element) in self.elements.iter().enumerate() {
let grad = shape_gradients(element, ¢roid);
let gradient = deformation_gradient_at(element, &grad, u);
if let Ok(r) = gradient.polar_rotation(POLAR_DET_FLOOR, polar_iterations) {
self.rotations[slot] = r;
}
}
}
fn apply(&self, u: &[Fix128], out: &mut [Fix128]) {
let (lambda, mu) = self.lame;
out.fill(Fix128::ZERO);
for (element, rotation) in self.elements.iter().zip(self.rotations.iter()) {
let transpose = rotation.transpose();
let mut local = [[Fix128::ZERO; 3]; NODES_PER_ELEMENT];
for (slot, &node) in local.iter_mut().zip(element.nodes.iter()) {
let base = node * 3;
*slot = mul3(transpose, [u[base], u[base + 1], u[base + 2]]);
}
for (point, weight) in self.points.iter().zip(self.weights.iter()) {
let scale = element.volume * *weight;
let grad = shape_gradients(element, point);
let s = stress_at_local(&grad, &local, lambda, mu);
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let force = [
g[0] * s.xx + g[1] * s.xy + g[2] * s.zx,
g[1] * s.yy + g[0] * s.xy + g[2] * s.yz,
g[2] * s.zz + g[1] * s.yz + g[0] * s.zx,
];
let global = mul3(*rotation, force);
let base = node * 3;
for (axis, value) in global.iter().enumerate() {
out[base + axis] = out[base + axis] + scale * *value;
}
}
}
}
}
fn diagonal(&self, ndof: usize) -> Vec<Fix128> {
let (lambda, mu) = self.lame;
let lambda_2mu = lambda + mu + mu;
let lambda_mu = lambda + mu;
let mut diag = vec![Fix128::ZERO; ndof];
for (element, rotation) in self.elements.iter().zip(self.rotations.iter()) {
let r = [
[rotation.col0.x, rotation.col1.x, rotation.col2.x],
[rotation.col0.y, rotation.col1.y, rotation.col2.y],
[rotation.col0.z, rotation.col1.z, rotation.col2.z],
];
for (point, weight) in self.points.iter().zip(self.weights.iter()) {
let scale = element.volume * *weight;
let grad = shape_gradients(element, point);
for (g, &node) in grad.iter().zip(element.nodes.iter()) {
let sq = [g[0] * g[0], g[1] * g[1], g[2] * g[2]];
let mut block = [[Fix128::ZERO; 3]; 3];
for (m, row) in block.iter_mut().enumerate() {
for (n, entry) in row.iter_mut().enumerate() {
*entry = if m == n {
scale
* (lambda_2mu * sq[m]
+ mu * (sq[(m + 1) % 3] + sq[(m + 2) % 3]))
} else {
scale * (lambda_mu * g[m] * g[n])
};
}
}
let base = node * 3;
for (a, r_row) in r.iter().enumerate() {
let mut acc = Fix128::ZERO;
for (m, block_row) in block.iter().enumerate() {
for (n, entry) in block_row.iter().enumerate() {
acc = acc + r_row[m] * *entry * r_row[n];
}
}
diag[base + a] = diag[base + a] + acc;
}
}
}
}
diag
}
}
#[inline]
fn mul3(m: Mat3Fix, v: [Fix128; 3]) -> [Fix128; 3] {
let out = m.mul_vec(Vec3Fix::new(v[0], v[1], v[2]));
[out.x, out.y, out.z]
}
fn stress_at_local(
grad: &[[Fix128; 3]; NODES_PER_ELEMENT],
local: &[[Fix128; 3]; NODES_PER_ELEMENT],
lambda: Fix128,
mu: Fix128,
) -> StressTensor {
let mut exx = Fix128::ZERO;
let mut eyy = Fix128::ZERO;
let mut ezz = Fix128::ZERO;
let mut gxy = Fix128::ZERO;
let mut gyz = Fix128::ZERO;
let mut gzx = Fix128::ZERO;
for (g, d) in grad.iter().zip(local.iter()) {
let (ux, uy, uz) = (d[0], d[1], d[2]);
exx = exx + g[0] * ux;
eyy = eyy + g[1] * uy;
ezz = ezz + g[2] * uz;
gxy = gxy + g[1] * ux + g[0] * uy;
gyz = gyz + g[2] * uy + g[1] * uz;
gzx = gzx + g[2] * ux + g[0] * uz;
}
let trace = exx + eyy + ezz;
let two_mu = mu + mu;
StressTensor {
xx: lambda * trace + two_mu * exx,
yy: lambda * trace + two_mu * eyy,
zz: lambda * trace + two_mu * ezz,
xy: mu * gxy,
yz: mu * gyz,
zx: mu * gzx,
}
}
struct CgOutcome {
x: Vec<Fix128>,
iterations: u32,
residual_norm: Fix128,
b_norm: Fix128,
target: Fix128,
}
fn conjugate_gradient<A: Fn(&[Fix128], &mut [Fix128])>(
apply: A,
b: &[Fix128],
is_free: &[bool],
precond: &[Fix128],
config: &SolverConfig,
) -> Result<CgOutcome, FemError> {
let ndof = b.len();
let mut scratch = vec![Fix128::ZERO; ndof];
let mut x = vec![Fix128::ZERO; ndof];
let mut r = b.to_vec();
let mut z = vec![Fix128::ZERO; ndof];
for d in 0..ndof {
if is_free[d] {
z[d] = r[d] * precond[d];
}
}
let mut p = z.clone();
let mut rz = dot(&r, &z);
let b_norm = dot(b, b).sqrt();
let requested = config.relative_tolerance() * b_norm;
let target = if requested > RESIDUAL_NORM_FLOOR {
requested
} else {
RESIDUAL_NORM_FLOOR
};
let mut iterations = 0u32;
let mut residual_norm = dot(&r, &r).sqrt();
let mut best_residual = residual_norm;
let mut since_improvement = 0u32;
while residual_norm > target {
if iterations >= config.max_iterations() {
return Err(FemError::NotConverged {
iterations,
relative_residual: relative(residual_norm, b_norm),
});
}
let window = {
let scaled =
config.stagnation_window_fraction() * Fix128::from_int(i64::from(iterations));
let scaled = if scaled.is_negative() {
0
} else {
u32::try_from(scaled.hi).unwrap_or(u32::MAX)
};
scaled.max(config.stagnation_min_window())
};
if since_improvement >= window {
return Err(FemError::Stagnated {
iterations,
relative_residual: relative(best_residual, b_norm),
without_improvement: since_improvement,
});
}
apply(&p, &mut scratch);
for (d, value) in scratch.iter_mut().enumerate() {
if !is_free[d] {
*value = Fix128::ZERO;
}
}
let pkp = dot(&p, &scratch);
if pkp <= Fix128::ZERO {
if iterations == 0 {
return Err(FemError::UnderConstrained);
}
return Err(FemError::Stagnated {
iterations,
relative_residual: relative(best_residual, b_norm),
without_improvement: since_improvement,
});
}
let alpha = rz / pkp;
for d in 0..ndof {
if is_free[d] {
x[d] = x[d] + alpha * p[d];
r[d] = r[d] - alpha * scratch[d];
z[d] = r[d] * precond[d];
}
}
let rz_next = dot(&r, &z);
let beta = rz_next / rz;
for d in 0..ndof {
if is_free[d] {
p[d] = z[d] + beta * p[d];
}
}
rz = rz_next;
residual_norm = dot(&r, &r).sqrt();
iterations += 1;
if residual_norm < best_residual - best_residual * config.stagnation_min_improvement() {
best_residual = residual_norm;
since_improvement = 0;
} else {
if residual_norm < best_residual {
best_residual = residual_norm;
}
since_improvement += 1;
}
}
Ok(CgOutcome {
x,
iterations,
residual_norm,
b_norm,
target,
})
}
fn preconditioner(diag: &[Fix128], is_free: &[bool], mode: Preconditioner) -> Vec<Fix128> {
let ndof = diag.len();
match mode {
Preconditioner::None => vec![Fix128::ONE; ndof],
Preconditioner::JacobiScaled => {
let mut sum = Fix128::ZERO;
let mut free = 0i64;
for (d, value) in diag.iter().enumerate() {
if is_free[d] {
sum = sum + *value;
free += 1;
}
}
if free == 0 || sum.is_zero() {
return vec![Fix128::ONE; ndof];
}
let mean = sum / Fix128::from_int(free);
let mut m = vec![Fix128::ONE; ndof];
for (d, value) in diag.iter().enumerate() {
if is_free[d] && !value.is_zero() {
m[d] = mean / *value;
}
}
m
}
}
}
fn finish_hyperelastic(
mesh: &CubicMesh,
u: &[Fix128],
law: MaterialLaw,
cg: (u32, Fix128, Fix128),
) -> Result<FemSolution, FemError> {
let quarter = dyadic(1, 2);
let centroid = [quarter; 4];
let displacements = (0..mesh.node_count())
.map(|n| [u[n * 3], u[n * 3 + 1], u[n * 3 + 2]])
.collect();
let mut element_stress = Vec::with_capacity(mesh.elements.len());
for (tet, element) in mesh.elements.iter().enumerate() {
let grad = shape_gradients(element, ¢roid);
let gradient = deformation_gradient_at(element, &grad, u);
let (cauchy, _) = hyperelastic_stress(&law.model, law.bulk_modulus, gradient).ok_or(
FemError::RotationFailed {
tet,
cause: PolarError::Inverted,
},
)?;
element_stress.push(cauchy);
}
let (iterations, relative_residual, effective_relative_tolerance) = cg;
Ok(FemSolution {
displacements,
element_stress,
iterations,
relative_residual,
effective_relative_tolerance,
})
}
pub fn solve_cubic_hyperelastic(
mesh: &CubicMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &CorotationalConfig,
) -> Result<CorotationalSolution, FemError> {
let node_count = mesh.node_count();
if node_count == 0 || mesh.elements.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(node, _, _) in boundary.prescribed().iter().chain(boundary.loads().iter()) {
if node as usize >= node_count {
return Err(FemError::VertexOutOfRange {
vertex: node,
vertex_count: node_count,
});
}
}
let model = config.hyperelastic().ok_or(FemError::InvalidConfig(
"solve_cubic_hyperelastic needs CorotationalConfig::with_hyperelastic",
))?;
let (lambda, mu) = material.lame();
let law = MaterialLaw {
model,
bulk_modulus: hyperelastic_volumetric_modulus(&model, lambda)?,
};
let ndof = node_count * 3;
let points = quadrature_points();
let weights = quadrature_weights();
let mut op = RotatedOperator::new(&mesh.elements, &points, &weights, (lambda, mu));
let mut prescribed_value = vec![Fix128::ZERO; ndof];
let mut is_free = vec![true; ndof];
for &(node, axis, value) in boundary.prescribed() {
let d = node as usize * 3 + axis.index();
prescribed_value[d] = value;
is_free[d] = false;
}
if is_free.iter().filter(|f| !**f).count() < 6 {
return Err(FemError::UnderConstrained);
}
let mut f_ext = vec![Fix128::ZERO; ndof];
for &(node, axis, force) in boundary.loads() {
let d = node as usize * 3 + axis.index();
if is_free[d] {
f_ext[d] = f_ext[d] + force;
}
}
if is_free.iter().all(|f| !*f) {
let field = finish_hyperelastic(
mesh,
&prescribed_value,
law,
(0, Fix128::ZERO, config.linear().relative_tolerance()),
)?;
return Ok(CorotationalSolution {
field,
newton_iterations: 0,
increments: config.increments(),
});
}
let mut scratch = vec![Fix128::ZERO; ndof];
apply_stiffness(
&mesh.elements,
&points,
&weights,
&prescribed_value,
lambda,
mu,
&mut scratch,
);
let mut reference = f_ext.clone();
for (d, value) in reference.iter_mut().enumerate() {
if is_free[d] {
*value = *value - scratch[d];
} else {
*value = Fix128::ZERO;
}
}
let newton_target = max_abs(&reference) * config.newton_tolerance();
let mut boundary_field = vec![Fix128::ZERO; ndof];
let mut u = vec![Fix128::ZERO; ndof];
let mut material_force = vec![Fix128::ZERO; ndof];
let mut residual = vec![Fix128::ZERO; ndof];
let mut newton_iterations = 0u32;
let mut cg_iterations = 0u32;
let mut relative_residual = Fix128::ZERO;
let mut effective_relative_tolerance = Fix128::ZERO;
let krylov_precond = preconditioner(
&op.diagonal(ndof),
&is_free,
config.linear().preconditioner(),
);
for increment in 1..=config.increments() {
if increment > 1 {
let ratio =
Fix128::from_int(i64::from(increment)) / Fix128::from_int(i64::from(increment - 1));
for (d, value) in u.iter_mut().enumerate() {
if is_free[d] {
*value = *value * ratio;
}
}
}
boundary_field.fill(Fix128::ZERO);
if increment == config.increments() {
for (d, value) in prescribed_value.iter().enumerate() {
if !is_free[d] {
boundary_field[d] = *value;
}
}
} else {
let scale = Fix128::from_int(i64::from(increment))
/ Fix128::from_int(i64::from(config.increments()));
for (d, value) in prescribed_value.iter().enumerate() {
if !is_free[d] {
boundary_field[d] = *value * scale;
}
}
}
for (d, value) in boundary_field.iter().enumerate() {
if !is_free[d] {
u[d] = *value;
}
}
let mut step = 0u32;
loop {
if step > 0 {
material_internal_force(
&mesh.elements,
&points,
&weights,
&u,
law,
&mut material_force,
)?;
for (d, value) in residual.iter_mut().enumerate() {
*value = if is_free[d] {
f_ext[d] - material_force[d]
} else {
Fix128::ZERO
};
}
if max_abs(&residual) <= newton_target {
break;
}
}
if step >= config.newton_iterations() {
return Err(FemError::NotConverged {
iterations: step,
relative_residual: relative(max_abs(&residual), newton_target),
});
}
if config.consistent_tangent() && step > 0 {
if let Some(report) =
crate::linear_elastic_fem::consistent_tangent::newton_krylov_step(
&mut u,
&f_ext,
&is_free,
&krylov_precond,
&config.linear(),
|x, out| {
material_internal_force(&mesh.elements, &points, &weights, x, law, out)
},
)?
{
cg_iterations = cg_iterations.saturating_add(report.cg_iterations);
relative_residual = report.relative_residual;
effective_relative_tolerance = report.effective_relative_tolerance;
step += 1;
newton_iterations = newton_iterations.saturating_add(1);
continue;
}
}
if step > 0 {
op.refresh(&u, config.polar_iterations());
}
let precond = preconditioner(
&op.diagonal(ndof),
&is_free,
config.linear().preconditioner(),
);
op.apply(&boundary_field, &mut scratch);
for (d, value) in residual.iter_mut().enumerate() {
*value = if is_free[d] {
f_ext[d] - scratch[d]
} else {
Fix128::ZERO
};
}
if step > 0 {
op.apply(&u, &mut scratch);
material_internal_force(
&mesh.elements,
&points,
&weights,
&u,
law,
&mut material_force,
)?;
for (d, value) in residual.iter_mut().enumerate() {
if is_free[d] {
*value = *value + scratch[d] - material_force[d];
}
}
}
let cg = conjugate_gradient(
|p, out| op.apply(p, out),
&residual,
&is_free,
&precond,
&config.linear(),
)?;
cg_iterations = cg_iterations.saturating_add(cg.iterations);
relative_residual = relative(cg.residual_norm, cg.b_norm);
effective_relative_tolerance = relative(cg.target, cg.b_norm);
for (d, value) in u.iter_mut().enumerate() {
*value = if is_free[d] {
cg.x[d]
} else {
boundary_field[d]
};
}
step += 1;
newton_iterations = newton_iterations.saturating_add(1);
}
}
material_internal_force(
&mesh.elements,
&points,
&weights,
&u,
law,
&mut material_force,
)?;
for (d, value) in residual.iter_mut().enumerate() {
*value = if is_free[d] {
f_ext[d] - material_force[d]
} else {
Fix128::ZERO
};
}
let final_residual = max_abs(&residual);
if final_residual > newton_target {
return Err(FemError::NotConverged {
iterations: newton_iterations,
relative_residual: relative(final_residual, newton_target),
});
}
let field = finish_hyperelastic(
mesh,
&u,
law,
(
cg_iterations,
relative_residual,
effective_relative_tolerance,
),
)?;
Ok(CorotationalSolution {
field,
newton_iterations,
increments: config.increments(),
})
}
#[must_use = "the support forces are the whole point of calling this; a solve that \
drops them has not been checked against equilibrium at all"]
pub fn reactions(
mesh: &CubicMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
law: Option<HyperelasticModel>,
solution: &FemSolution,
) -> Result<Vec<[Fix128; 3]>, FemError> {
let node_count = mesh.node_count();
if node_count == 0 || mesh.elements.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(node, _, _) in boundary.prescribed().iter().chain(boundary.loads().iter()) {
if node as usize >= node_count {
return Err(FemError::VertexOutOfRange {
vertex: node,
vertex_count: node_count,
});
}
}
let nodes = solution.displacements.len();
if nodes != node_count {
return Err(FemError::SolutionDoesNotMatchMesh {
nodes,
vertex_count: node_count,
});
}
let (lambda, mu) = material.lame();
let ndof = node_count * 3;
let points = quadrature_points();
let weights = quadrature_weights();
let mut u = vec![Fix128::ZERO; ndof];
for (n, d) in solution.displacements.iter().enumerate() {
u[n * 3] = d[0];
u[n * 3 + 1] = d[1];
u[n * 3 + 2] = d[2];
}
let mut force = vec![Fix128::ZERO; ndof];
match law {
None => apply_stiffness(
&mesh.elements,
&points,
&weights,
&u,
lambda,
mu,
&mut force,
),
Some(model) => {
let law = MaterialLaw {
model,
bulk_modulus: hyperelastic_volumetric_modulus(&model, lambda)?,
};
material_internal_force(&mesh.elements, &points, &weights, &u, law, &mut force)?;
}
}
for &(node, axis, applied) in boundary.loads() {
let d = node as usize * 3 + axis.index();
force[d] = force[d] - applied;
}
let mut is_free = vec![true; ndof];
for &(node, axis, _) in boundary.prescribed() {
is_free[node as usize * 3 + axis.index()] = false;
}
for (d, value) in force.iter_mut().enumerate() {
if is_free[d] {
*value = Fix128::ZERO;
}
}
Ok((0..node_count)
.map(|n| [force[n * 3], force[n * 3 + 1], force[n * 3 + 2]])
.collect())
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AdaptiveCubicSolution {
pub mesh: SdfTetMesh,
pub high_order: CubicMesh,
pub field: FemSolution,
pub rounds: u32,
pub total_indicator_history: Vec<Fix128>,
}
pub fn solve_adaptive_cubic<F>(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary_for: F,
adaptive: &crate::linear_elastic_fem::AdaptiveConfig,
) -> Result<AdaptiveCubicSolution, FemError>
where
F: Fn(&CubicMesh) -> BoundaryConditions,
{
let (corner, field, rounds, total_indicator_history) =
crate::linear_elastic_fem::adaptive_refinement_loop(mesh, adaptive, |current| {
let high_order = CubicMesh::from_tet_mesh(current)?;
let boundary = boundary_for(&high_order);
let field = solve_cubic(&high_order, material, &boundary, &adaptive.linear())?;
let indicators =
crate::linear_elastic_fem::corner_indicators_squared(current, material, &field)?;
Ok((field, indicators))
})?;
let high_order = CubicMesh::from_tet_mesh(&corner)?;
Ok(AdaptiveCubicSolution {
mesh: corner,
high_order,
field,
rounds,
total_indicator_history,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf_fem_mesh::Tetrahedron;
fn raw(x: Fix128) -> i128 {
((x.hi as i128) << 64) | (x.lo as i128)
}
fn element_with_exact_gradients() -> CubicMesh {
let mut mesh = SdfTetMesh::default();
mesh.vertices.push([0.0, 0.0, 0.0]);
mesh.vertices.push([2.0, 0.0, 0.0]);
mesh.vertices.push([0.0, 4.0, 0.0]);
mesh.vertices.push([0.0, 0.0, 8.0]);
mesh.tets.push(Tetrahedron {
vertices: [0, 1, 2, 3],
});
CubicMesh::from_tet_mesh(&mesh).expect("well formed")
}
fn element_with_exact_node_positions() -> CubicMesh {
let mut mesh = SdfTetMesh::default();
mesh.vertices.push([0.0, 0.0, 0.0]);
mesh.vertices.push([6.0, 0.0, 0.0]);
mesh.vertices.push([0.0, 12.0, 0.0]);
mesh.vertices.push([0.0, 0.0, 24.0]);
mesh.tets.push(Tetrahedron {
vertices: [0, 1, 2, 3],
});
CubicMesh::from_tet_mesh(&mesh).expect("well formed")
}
fn keast_11_padded() -> (
[[Fix128; 4]; QUADRATURE_POINTS],
[Fix128; QUADRATURE_POINTS],
) {
let mut points = [[Fix128::ONE / Fix128::from_int(4); 4]; QUADRATURE_POINTS];
let mut weights = [Fix128::ZERO; QUADRATURE_POINTS];
weights[0] = Fix128::from_int(-148) / Fix128::from_int(1875);
let a31 = Fix128::from_int(11) / Fix128::from_int(14);
let b31 = Fix128::ONE / Fix128::from_int(14);
for i in 0..4 {
let mut p = [b31; 4];
p[i] = a31;
points[1 + i] = p;
weights[1 + i] = Fix128::from_int(343) / Fix128::from_int(7500);
}
let r = Fix128::from_int(5).sqrt() / Fix128::from_int(14).sqrt();
let a22 = (Fix128::ONE + r) / Fix128::from_int(4);
let b22 = (Fix128::ONE - r) / Fix128::from_int(4);
for (slot, pattern) in S22_PATTERN.iter().enumerate() {
let mut p = [a22; 4];
for (axis, &s) in p.iter_mut().zip(pattern.iter()) {
*axis = if s == 0 { a22 } else { b22 };
}
points[5 + slot] = p;
weights[5 + slot] = Fix128::from_int(56) / Fix128::from_int(375);
}
(points, weights)
}
fn pla() -> ElasticMaterial {
ElasticMaterial::new(Fix128::from_int(3500), dyadic(1, 2)).expect("valid")
}
#[test]
fn the_two_exactness_properties_cannot_both_hold() {
let grads = element_with_exact_gradients();
let nodes = element_with_exact_node_positions();
assert!(
!grads.interior_node_positions_are_exact(),
"legs that are powers of two cannot place (2a+b)/3 exactly"
);
assert!(
nodes.interior_node_positions_are_exact(),
"legs that are multiples of three must place (2a+b)/3 exactly; a false here \
means divide_by_three has stopped measuring anything"
);
let g = grads.elements[0].grad_lambda;
for (i, (shift, axis)) in [(1u32, 0usize), (2, 1), (3, 2)].into_iter().enumerate() {
assert_eq!(
raw(g[i + 1][axis]),
raw(dyadic(1, shift)),
"grad lambda {} must be exactly 1/2^{shift}",
i + 1
);
}
let h = nodes.elements[0].grad_lambda;
for (i, (den, axis)) in [(6i64, 0usize), (12, 1), (24, 2)].into_iter().enumerate() {
let value = h[i + 1][axis];
assert_ne!(
raw(value * Fix128::from_int(den)),
raw(Fix128::ONE),
"1/{den} carries a factor of 1/3 and cannot be exact; an exact value here \
would contradict the impossibility stated above"
);
}
}
#[test]
fn the_quadrature_points_satisfy_both_partition_identities_exactly() {
let mesh = element_with_exact_gradients();
let element = &mesh.elements[0];
for (i, point) in quadrature_points().iter().enumerate() {
let mut sum = Fix128::ZERO;
for v in shape_values(point) {
sum = sum + v;
}
assert_eq!(
raw(sum),
raw(Fix128::ONE),
"point {i}: the twenty cubic shape functions must sum to exactly one at a \
dyadic abscissa"
);
let grad = shape_gradients(element, point);
for axis in 0..3 {
let mut g = Fix128::ZERO;
for row in &grad {
g = g + row[axis];
}
assert_eq!(
raw(g),
0,
"point {i} axis {axis}: the shape function gradients must sum to exactly \
zero, or a constant field carries strain"
);
}
}
}
#[test]
fn the_shape_gradient_identity_degrades_with_the_element_geometry() {
let mesh = element_with_exact_node_positions();
let element = &mesh.elements[0];
let mut worst = 0i128;
for point in &quadrature_points() {
let grad = shape_gradients(element, point);
for axis in 0..3 {
let mut g = Fix128::ZERO;
for row in &grad {
g = g + row[axis];
}
if raw(g).abs() > worst {
worst = raw(g).abs();
}
}
}
std::eprintln!(
"[p3fem] sum of shape gradients on an element with inexact grad lambda: \
worst {worst} raw units (exact answer 0)"
);
assert_ne!(
worst, 0,
"if this element also satisfies the identity exactly, then the condition stated \
in the test above is wider than claimed and the module header is wrong"
);
assert!(
worst < 1 << 20,
"the residue must stay at the rounding floor of a 1/3 that cannot be \
represented; {worst} raw units is a different mechanism"
);
}
#[test]
fn dyadic_abscissae_lower_the_rounding_of_the_whole_assembly() {
let mesh = element_with_exact_gradients();
let (lambda, mu) = pla().lame();
let t = [dyadic(1, 4), -dyadic(3, 5), dyadic(7, 6)];
let mut u = vec![Fix128::ZERO; mesh.node_count() * 3];
for n in 0..mesh.node_count() {
for axis in 0..3 {
u[n * 3 + axis] = t[axis];
}
}
let mut out = vec![Fix128::ZERO; u.len()];
let (dp, dw) = (quadrature_points(), quadrature_weights());
apply_stiffness(&mesh.elements, &dp, &dw, &u, lambda, mu, &mut out);
let dyadic_worst = out.iter().map(|v| raw(*v).abs()).max().expect("non-empty");
let (kp, kw) = keast_11_padded();
apply_stiffness(&mesh.elements, &kp, &kw, &u, lambda, mu, &mut out);
let keast_worst = out.iter().map(|v| raw(*v).abs()).max().expect("non-empty");
std::eprintln!(
"[p3fem] internal force of a constant field (exact answer 0) -- \
dyadic 24pt {dyadic_worst} raw, Keast 11pt {keast_worst} raw"
);
assert_eq!(
dyadic_worst, 0,
"a constant field must produce exactly zero internal force on the dyadic rule; \
got {dyadic_worst} raw units. This is the property the rule was chosen for and \
it is not a tolerance"
);
assert!(
keast_worst > 0,
"the comparison is the point: if the textbook rule also returns exactly zero, \
then dyadic abscissae bought nothing at the end of the assembly and the choice \
of rule should be revisited"
);
}
}