#![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 = 10;
const EDGES: [(usize, usize); 6] = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
fn half() -> Fix128 {
Fix128::from_raw(0, 1 << 63)
}
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]
}
fn quadrature_points() -> [[Fix128; 4]; 4] {
let five = Fix128::from_int(5);
let root5 = five.sqrt();
let twenty = Fix128::from_int(20);
let a = (five - root5) / twenty;
let b = (five + Fix128::from_int(3) * root5) / twenty;
let mut points = [[a; 4]; 4];
for (i, point) in points.iter_mut().enumerate() {
point[i] = b;
}
points
}
fn quadrature_weight() -> Fix128 {
half() * half()
}
#[derive(Clone, Copy, Debug)]
struct QuadraticElement {
nodes: [usize; NODES_PER_ELEMENT],
grad_lambda: [[Fix128; 3]; 4],
volume: Fix128,
}
#[derive(Clone, Debug)]
pub struct QuadraticMesh {
corner_count: usize,
positions: Vec<[Fix128; 3]>,
elements: Vec<QuadraticElement>,
edge_nodes: BTreeMap<(u32, u32), u32>,
}
impl QuadraticMesh {
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 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> = 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 (slot, &(i, j)) in EDGES.iter().enumerate() {
let (a, b) = (tet.vertices[i], tet.vertices[j]);
let key = if a <= b { (a, b) } else { (b, a) };
let next = u32::try_from(positions.len()).map_err(|_| FemError::EmptyMesh)?;
let node = *edge_nodes.entry(key).or_insert(next);
if node == next {
let (pa, pb) = (p[i], p[j]);
positions.push([
half() * (pa[0] + pb[0]),
half() * (pa[1] + pb[1]),
half() * (pa[2] + pb[2]),
]);
}
nodes[4 + slot] = node as usize;
}
elements.push(QuadraticElement {
nodes,
grad_lambda: [g0, g1, g2, g3],
volume: det.abs() / six,
});
}
Ok(Self {
corner_count,
positions,
elements,
edge_nodes,
})
}
#[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_count(&self) -> usize {
self.positions.len() - self.corner_count
}
#[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_node(&self, a: u32, b: u32) -> Option<u32> {
let key = if a <= b { (a, b) } else { (b, a) };
self.edge_nodes.get(&key).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)
}
}
fn shape_gradients(
element: &QuadraticElement,
lambda: &[Fix128; 4],
) -> [[Fix128; 3]; NODES_PER_ELEMENT] {
let four = Fix128::from_int(4);
let mut grad = [[Fix128::ZERO; 3]; NODES_PER_ELEMENT];
for (i, (out, gl)) in grad.iter_mut().zip(element.grad_lambda.iter()).enumerate() {
let s = four * lambda[i] - Fix128::ONE;
for (o, c) in out.iter_mut().zip(gl.iter()) {
*o = s * *c;
}
}
for (slot, &(i, j)) in EDGES.iter().enumerate() {
let (li, lj) = (lambda[i], lambda[j]);
let (gi, gj) = (element.grad_lambda[i], element.grad_lambda[j]);
for (o, (ci, cj)) in grad[4 + slot].iter_mut().zip(gi.iter().zip(gj.iter())) {
*o = four * (li * *cj + lj * *ci);
}
}
grad
}
fn stress_at(
element: &QuadraticElement,
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: &[QuadraticElement],
points: &[[Fix128; 4]; 4],
weight: Fix128,
u: &[Fix128],
lambda: Fix128,
mu: Fix128,
out: &mut [Fix128],
) {
out.fill(Fix128::ZERO);
for element in elements {
let scale = element.volume * weight;
for point in points {
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: &[QuadraticElement],
points: &[[Fix128; 4]; 4],
weight: Fix128,
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 {
let scale = element.volume * weight;
for point in points {
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_quadratic(
mesh: &QuadraticMesh,
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 weight = quadrature_weight();
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().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,
weight,
&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, weight, 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,
weight,
&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: &QuadraticMesh,
u: &[Fix128],
lambda: Fix128,
mu: Fix128,
iterations: u32,
relative_residual: Fix128,
effective_relative_tolerance: Fix128,
) -> FemSolution {
let quarter = half() * half();
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: &QuadraticElement,
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: &[QuadraticElement],
points: &[[Fix128; 4]; 4],
weight: Fix128,
u: &[Fix128],
law: MaterialLaw,
out: &mut [Fix128],
) -> Result<(), FemError> {
out.fill(Fix128::ZERO);
for (tet, element) in elements.iter().enumerate() {
for point in points {
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 [QuadraticElement],
points: &'a [[Fix128; 4]; 4],
weight: Fix128,
lame: (Fix128, Fix128),
rotations: Vec<Mat3Fix>,
}
impl<'a> RotatedOperator<'a> {
fn new(
elements: &'a [QuadraticElement],
points: &'a [[Fix128; 4]; 4],
weight: Fix128,
lame: (Fix128, Fix128),
) -> Self {
Self {
elements,
points,
weight,
lame,
rotations: vec![Mat3Fix::IDENTITY; elements.len()],
}
}
fn refresh(&mut self, u: &[Fix128], polar_iterations: u32) {
let centroid = [half() * half(); 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 in self.points {
let scale = element.volume * self.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 in self.points {
let scale = element.volume * self.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: &QuadraticMesh,
u: &[Fix128],
law: MaterialLaw,
cg: (u32, Fix128, Fix128),
) -> Result<FemSolution, FemError> {
let quarter = half() * half();
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_quadratic_hyperelastic(
mesh: &QuadraticMesh,
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_quadratic_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 weight = quadrature_weight();
let mut op = RotatedOperator::new(&mesh.elements, &points, weight, (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,
weight,
&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,
weight,
&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, weight, 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,
weight,
&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,
weight,
&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: &QuadraticMesh,
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 weight = quadrature_weight();
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, weight, &u, lambda, mu, &mut force),
Some(model) => {
let law = MaterialLaw {
model,
bulk_modulus: hyperelastic_volumetric_modulus(&model, lambda)?,
};
material_internal_force(&mesh.elements, &points, weight, &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 AdaptiveQuadraticSolution {
pub mesh: SdfTetMesh,
pub high_order: QuadraticMesh,
pub field: FemSolution,
pub rounds: u32,
pub total_indicator_history: Vec<Fix128>,
}
pub fn solve_adaptive_quadratic<F>(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary_for: F,
adaptive: &crate::linear_elastic_fem::AdaptiveConfig,
) -> Result<AdaptiveQuadraticSolution, FemError>
where
F: Fn(&QuadraticMesh) -> BoundaryConditions,
{
let (corner, field, rounds, total_indicator_history) =
crate::linear_elastic_fem::adaptive_refinement_loop(mesh, adaptive, |current| {
let high_order = QuadraticMesh::from_tet_mesh(current)?;
let boundary = boundary_for(&high_order);
let field = solve_quadratic(&high_order, material, &boundary, &adaptive.linear())?;
let indicators =
crate::linear_elastic_fem::corner_indicators_squared(current, material, &field)?;
Ok((field, indicators))
})?;
let high_order = QuadraticMesh::from_tet_mesh(&corner)?;
Ok(AdaptiveQuadraticSolution {
mesh: corner,
high_order,
field,
rounds,
total_indicator_history,
})
}