use crate::coupled_field::{CoupledField, TemperatureRise};
use crate::hyperelastic::HyperelasticModel;
use crate::math::{Fix128, Mat3Fix, PolarError, Vec3Fix};
use crate::sdf_fem_mesh::SdfTetMesh;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Axis {
X,
Y,
Z,
}
impl Axis {
pub const ALL: [Self; 3] = [Self::X, Self::Y, Self::Z];
#[must_use]
pub const fn index(self) -> usize {
match self {
Self::X => 0,
Self::Y => 1,
Self::Z => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FemError {
EmptyMesh,
InvalidMaterial(&'static str),
InvalidConfig(&'static str),
VertexOutOfRange {
vertex: u32,
vertex_count: usize,
},
DegenerateElement {
tet: usize,
},
RotationFailed {
tet: usize,
cause: PolarError,
},
UnderConstrained,
NotConverged {
iterations: u32,
relative_residual: Fix128,
},
Stagnated {
iterations: u32,
relative_residual: Fix128,
without_improvement: u32,
},
TemperatureFieldDoesNotCoverMesh {
vertex: u32,
},
SolutionDoesNotMatchMesh {
nodes: usize,
vertex_count: usize,
},
SolutionElementCountDoesNotMatchMesh {
elements: usize,
tet_count: usize,
},
DepositGridHasDegenerateAxis {
axis: Axis,
},
CoupledSubIterationFailed(crate::coupled_iteration::CoupledIterationError),
ResidualNormUnfaithful {
index: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElasticMaterial {
youngs_modulus_mpa: Fix128,
poissons_ratio: Fix128,
}
impl ElasticMaterial {
pub fn new(youngs_modulus_mpa: Fix128, poissons_ratio: Fix128) -> Result<Self, FemError> {
if youngs_modulus_mpa <= Fix128::ZERO {
return Err(FemError::InvalidMaterial(
"Young's modulus must be positive",
));
}
if poissons_ratio <= Fix128::NEG_ONE {
return Err(FemError::InvalidMaterial(
"Poisson's ratio must be greater than -1",
));
}
if poissons_ratio >= half() {
return Err(FemError::InvalidMaterial(
"Poisson's ratio must be less than 0.5 (0.5 is incompressible)",
));
}
let safe_max = Fix128::from_raw(i64::MAX, u64::MAX) / Fix128::from_int(16);
let gap_hi = half() - poissons_ratio;
let gap_lo = poissons_ratio - Fix128::NEG_ONE;
if youngs_modulus_mpa >= gap_hi * safe_max || youngs_modulus_mpa >= gap_lo * safe_max {
return Err(FemError::InvalidMaterial(
"Poisson's ratio is too close to the 0.5 / -1 singularity for this Young's modulus: lame() would overflow Fix128",
));
}
Ok(Self {
youngs_modulus_mpa,
poissons_ratio,
})
}
pub fn from_filament(
material: &crate::filament_db::MaterialProperties,
) -> Result<Self, FemError> {
let e_mpa = material.youngs_modulus_gpa * Fix128::from_int(1000);
Self::new(e_mpa, Self::default_poissons_ratio(material.category))
}
pub fn with_poisson(self, poissons_ratio: Fix128) -> Result<Self, FemError> {
Self::new(self.youngs_modulus_mpa, poissons_ratio)
}
#[must_use]
pub fn default_poissons_ratio(category: crate::filament_db::MaterialCategory) -> Fix128 {
use crate::filament_db::MaterialCategory as C;
match category {
C::Fdm | C::Sla => Fix128::from_ratio(35, 100),
C::SheetMetal => Fix128::from_ratio(30, 100),
C::Powder => Fix128::from_ratio(40, 100),
}
}
#[must_use]
pub const fn youngs_modulus_mpa(&self) -> Fix128 {
self.youngs_modulus_mpa
}
#[must_use]
pub const fn poissons_ratio(&self) -> Fix128 {
self.poissons_ratio
}
#[must_use]
pub fn lame(&self) -> (Fix128, Fix128) {
let nu = self.poissons_ratio;
let one_plus = Fix128::ONE + nu;
let one_minus_two = Fix128::ONE - (nu + nu);
let lambda = self.youngs_modulus_mpa * nu / (one_plus * one_minus_two);
let mu = self.youngs_modulus_mpa / (one_plus + one_plus);
(lambda, mu)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ThermalExpansion<'a> {
field: &'a CoupledField,
alpha_per_k: Fix128,
}
impl<'a> ThermalExpansion<'a> {
#[must_use]
#[deprecated(
since = "1.5.0",
note = "a bare `&CoupledField` cannot say whether it holds rises or absolute \
temperatures, and an absolute one loads the reference as a rise; build a \
`coupled_field::TemperatureRise` with `TemperatureRise::from_absolute` and \
pass it to `ThermalExpansion::from_rise`"
)]
pub const fn new(field: &'a CoupledField, alpha_per_k: Fix128) -> Self {
Self { field, alpha_per_k }
}
#[must_use]
pub const fn from_rise(rise: &'a TemperatureRise, alpha_per_k: Fix128) -> Self {
Self {
field: rise.field(),
alpha_per_k,
}
}
#[must_use]
pub const fn field(&self) -> &'a CoupledField {
self.field
}
#[must_use]
pub const fn alpha_per_k(&self) -> Fix128 {
self.alpha_per_k
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BoundaryConditions {
prescribed: Vec<(u32, Axis, Fix128)>,
loads: Vec<(u32, Axis, Fix128)>,
}
impl BoundaryConditions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn prescribe(&mut self, vertex: u32, axis: Axis, displacement_mm: Fix128) -> &mut Self {
if let Some(slot) = self
.prescribed
.iter_mut()
.find(|(v, a, _)| *v == vertex && *a == axis)
{
slot.2 = displacement_mm;
} else {
self.prescribed.push((vertex, axis, displacement_mm));
}
self
}
pub fn fix(&mut self, vertex: u32) -> &mut Self {
self.prescribe_all(vertex, [Fix128::ZERO; 3])
}
pub fn prescribe_all(&mut self, vertex: u32, displacement_mm: [Fix128; 3]) -> &mut Self {
for axis in Axis::ALL {
self.prescribe(vertex, axis, displacement_mm[axis.index()]);
}
self
}
pub fn add_load(&mut self, vertex: u32, axis: Axis, force_n: Fix128) -> &mut Self {
if let Some(slot) = self
.loads
.iter_mut()
.find(|(v, a, _)| *v == vertex && *a == axis)
{
slot.2 = slot.2 + force_n;
} else {
self.loads.push((vertex, axis, force_n));
}
self
}
#[must_use]
pub fn prescribed(&self) -> &[(u32, Axis, Fix128)] {
&self.prescribed
}
#[must_use]
pub fn loads(&self) -> &[(u32, Axis, Fix128)] {
&self.loads
}
#[must_use]
pub fn prescribed_count(&self) -> usize {
self.prescribed.len()
}
#[must_use]
pub fn load_count(&self) -> usize {
self.loads.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StressTensor {
pub xx: Fix128,
pub yy: Fix128,
pub zz: Fix128,
pub xy: Fix128,
pub yz: Fix128,
pub zx: Fix128,
}
impl StressTensor {
#[must_use]
pub fn complementary_energy_density(&self, material: &ElasticMaterial) -> Fix128 {
let (lambda, mu) = material.lame();
let two_mu = mu + mu;
let bulk_share = lambda / (Fix128::from_int(3) * lambda + two_mu);
let double_dot = self.xx * self.xx
+ self.yy * self.yy
+ self.zz * self.zz
+ (self.xy * self.xy + self.yz * self.yz + self.zx * self.zx) * Fix128::from_int(2);
let trace = self.xx + self.yy + self.zz;
(double_dot - bulk_share * trace * trace) / two_mu
}
#[must_use]
pub fn von_mises(&self) -> Fix128 {
let a = self.xx - self.yy;
let b = self.yy - self.zz;
let c = self.zz - self.xx;
let shear = self.xy * self.xy + self.yz * self.yz + self.zx * self.zx;
(half() * (a * a + b * b + c * c) + Fix128::from_int(3) * shear).sqrt()
}
#[must_use]
pub fn hydrostatic(&self) -> Fix128 {
(self.xx + self.yy + self.zz) / Fix128::from_int(3)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Preconditioner {
#[default]
None,
JacobiScaled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiagonalStats {
pub min: Fix128,
pub max: Fix128,
pub mean: Fix128,
pub free_dofs: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SolverConfig {
max_iterations: u32,
relative_tolerance: Fix128,
stagnation_min_window: u32,
stagnation_window_fraction: Fix128,
stagnation_min_improvement: Fix128,
preconditioner: Preconditioner,
}
impl SolverConfig {
pub fn try_new(max_iterations: u32, relative_tolerance: Fix128) -> Result<Self, FemError> {
if max_iterations == 0 {
return Err(FemError::InvalidConfig("max_iterations must be positive"));
}
if relative_tolerance <= Fix128::ZERO {
return Err(FemError::InvalidConfig(
"relative_tolerance must be positive",
));
}
if relative_tolerance >= Fix128::ONE {
return Err(FemError::InvalidConfig(
"relative_tolerance of 1 or more accepts the zero vector",
));
}
Ok(Self {
max_iterations,
relative_tolerance,
..Self::default()
})
}
pub fn with_stagnation(
self,
min_window: u32,
min_improvement: Fix128,
) -> Result<Self, FemError> {
if min_window == 0 {
return Err(FemError::InvalidConfig(
"stagnation window must be positive",
));
}
if min_improvement <= Fix128::ZERO || min_improvement >= Fix128::ONE {
return Err(FemError::InvalidConfig(
"stagnation improvement must be a fraction strictly between 0 and 1",
));
}
Ok(Self {
stagnation_min_window: min_window,
stagnation_min_improvement: min_improvement,
..self
})
}
pub fn with_stagnation_fraction(self, fraction: Fix128) -> Result<Self, FemError> {
if fraction <= Fix128::ZERO || fraction >= Fix128::ONE {
return Err(FemError::InvalidConfig(
"stagnation window fraction must be strictly between 0 and 1; at 1 the rule \
can never fire",
));
}
Ok(Self {
stagnation_window_fraction: fraction,
..self
})
}
#[must_use]
pub const fn stagnation_window_fraction(&self) -> Fix128 {
self.stagnation_window_fraction
}
#[must_use]
pub const fn with_preconditioner(self, preconditioner: Preconditioner) -> Self {
Self {
preconditioner,
..self
}
}
#[must_use]
pub const fn preconditioner(&self) -> Preconditioner {
self.preconditioner
}
#[must_use]
pub const fn stagnation_min_window(&self) -> u32 {
self.stagnation_min_window
}
#[must_use]
pub const fn stagnation_min_improvement(&self) -> Fix128 {
self.stagnation_min_improvement
}
#[must_use]
pub const fn max_iterations(&self) -> u32 {
self.max_iterations
}
#[must_use]
pub const fn relative_tolerance(&self) -> Fix128 {
self.relative_tolerance
}
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
max_iterations: 10_000,
relative_tolerance: Fix128::from_raw(0, 1 << 34),
stagnation_min_window: 500,
stagnation_window_fraction: half(),
stagnation_min_improvement: Fix128::from_raw(0, 1 << 54),
preconditioner: Preconditioner::None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FemSolution {
pub displacements: Vec<[Fix128; 3]>,
pub element_stress: Vec<StressTensor>,
pub iterations: u32,
pub relative_residual: Fix128,
pub effective_relative_tolerance: Fix128,
}
impl FemSolution {
#[must_use]
pub fn max_von_mises_mpa(&self) -> Fix128 {
self.element_stress
.iter()
.map(StressTensor::von_mises)
.max()
.unwrap_or(Fix128::ZERO)
}
}
pub const RESIDUAL_NORM_FLOOR: Fix128 = Fix128::from_raw(0, 1 << 34);
#[inline]
fn half() -> Fix128 {
Fix128::from_raw(0, 1 << 63)
}
#[inline]
fn sub3(a: [Fix128; 3], b: [Fix128; 3]) -> [Fix128; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
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],
]
}
#[inline]
fn dot3(a: [Fix128; 3], b: [Fix128; 3]) -> Fix128 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn checked_l2_norm(v: &[Fix128]) -> Result<Fix128, FemError> {
crate::coupled_iteration::residual_norm_l2_checked(v).map_err(|e| match e {
crate::coupled_iteration::CoupledIterationError::ArithmeticWrapped { sweeps } => {
FemError::ResidualNormUnfaithful { index: sweeps }
}
other => FemError::CoupledSubIterationFailed(other),
})
}
fn run_sub_iteration_fallible<F>(
config: crate::coupled_iteration::SubIterationConfig,
mut sweep: F,
) -> Result<crate::coupled_iteration::SubIterationReport, FemError>
where
F: FnMut(u32) -> Result<Fix128, FemError>,
{
let mut failure: Option<FemError> = None;
let outcome = crate::coupled_iteration::run_sub_iteration(config, |index| {
if failure.is_some() {
return Fix128::ZERO;
}
match sweep(index) {
Ok(residual) => residual,
Err(e) => {
failure = Some(e);
Fix128::ZERO
}
}
});
if let Some(e) = failure {
return Err(e);
}
outcome.map_err(FemError::CoupledSubIterationFailed)
}
#[inline]
fn div3(a: [Fix128; 3], d: Fix128) -> [Fix128; 3] {
[a[0] / d, a[1] / d, a[2] / d]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Element {
nodes: [usize; 4],
grad: [[Fix128; 3]; 4],
volume: Fix128,
}
fn check_element_scale(elements: &[Element], lambda: Fix128, mu: Fix128) -> Result<(), FemError> {
let modulus = (lambda + mu + mu).abs();
for (t, e) in elements.iter().enumerate() {
let gmax = e
.grad
.iter()
.flat_map(|g| g.iter())
.map(|c| c.abs())
.max()
.unwrap_or(Fix128::ZERO);
let fits = gmax
.checked_mul(gmax)
.and_then(|g2| g2.checked_mul(modulus))
.is_some();
if !fits {
return Err(FemError::DegenerateElement { tet: t });
}
}
Ok(())
}
fn build_elements(mesh: &SdfTetMesh) -> Result<Vec<Element>, FemError> {
let vertex_count = mesh.vertices.len();
let six = Fix128::from_int(6);
let mut elements = Vec::with_capacity(mesh.tets.len());
for (t, tet) in mesh.tets.iter().enumerate() {
let mut nodes = [0usize; 4];
let mut p = [[Fix128::ZERO; 3]; 4];
for (i, &v) in tet.vertices.iter().enumerate() {
let idx = v as usize;
if idx >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex: v,
vertex_count,
});
}
nodes[i] = idx;
let q = mesh.vertices[idx];
p[i] = [
Fix128::from_f32(q[0]),
Fix128::from_f32(q[1]),
Fix128::from_f32(q[2]),
];
}
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]),
];
elements.push(Element {
nodes,
grad: [g0, g1, g2, g3],
volume: det.abs() / six,
});
}
Ok(elements)
}
#[inline]
fn gather(element: &Element, u: &[Fix128]) -> [[Fix128; 3]; 4] {
let mut local = [[Fix128::ZERO; 3]; 4];
for (slot, &node) in local.iter_mut().zip(element.nodes.iter()) {
let base = node * 3;
*slot = [u[base], u[base + 1], u[base + 2]];
}
local
}
fn element_stress_local(
element: &Element,
u: &[[Fix128; 3]; 4],
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_u) in element.grad.iter().zip(u.iter()) {
let (ux, uy, uz) = (node_u[0], node_u[1], node_u[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 element_stress(element: &Element, u: &[Fix128], lambda: Fix128, mu: Fix128) -> StressTensor {
element_stress_local(element, &gather(element, u), lambda, mu)
}
fn element_force_local(
element: &Element,
u: &[[Fix128; 3]; 4],
lambda: Fix128,
mu: Fix128,
) -> [[Fix128; 3]; 4] {
element_force_from_stress(element, element_stress_local(element, u, lambda, mu))
}
fn apply_stiffness(
elements: &[Element],
u: &[Fix128],
lambda: Fix128,
mu: Fix128,
out: &mut [Fix128],
) {
out.fill(Fix128::ZERO);
for element in elements {
let force = element_force_local(element, &gather(element, u), lambda, mu);
for (f, &node) in force.iter().zip(element.nodes.iter()) {
let base = node * 3;
out[base] = out[base] + f[0];
out[base + 1] = out[base + 1] + f[1];
out[base + 2] = out[base + 2] + f[2];
}
}
}
fn stiffness_diagonal(
elements: &[Element],
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 (g, &node) in element.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] + element.volume * (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
}
struct CgResult {
x: Vec<Fix128>,
iterations: u32,
residual_norm: Fix128,
b_norm: Fix128,
target: Fix128,
}
fn build_preconditioner(
diag: &[Fix128],
is_free: &[bool],
config: &SolverConfig,
) -> Result<Vec<Fix128>, FemError> {
let ndof = diag.len();
let mut diag_sum = Fix128::ZERO;
let mut free_count = 0u32;
for (d, value) in diag.iter().enumerate() {
if !is_free[d] {
continue;
}
if *value <= Fix128::ZERO {
return Err(FemError::UnderConstrained);
}
diag_sum = diag_sum + *value;
free_count += 1;
}
let mean_diag = if free_count == 0 {
Fix128::ONE
} else {
diag_sum / Fix128::from_int(i64::from(free_count))
};
let mut precond = vec![Fix128::ZERO; ndof];
for d in 0..ndof {
if is_free[d] {
precond[d] = match config.preconditioner {
Preconditioner::JacobiScaled => mean_diag / diag[d],
_ => Fix128::ONE,
};
}
}
Ok(precond)
}
fn conjugate_gradient<A>(
b: &[Fix128],
is_free: &[bool],
precond: &[Fix128],
config: &SolverConfig,
mut apply: A,
) -> Result<CgResult, FemError>
where
A: FnMut(&[Fix128], &mut [Fix128]),
{
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 = checked_l2_norm(b)?;
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 = checked_l2_norm(&r)?;
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];
}
}
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 = checked_l2_norm(&r)?;
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(CgResult {
x,
iterations,
residual_norm,
b_norm,
target,
})
}
pub fn solve(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &SolverConfig,
) -> Result<FemSolution, FemError> {
solve_with_eigenstrain(mesh, material, boundary, config, None)
}
pub fn solve_with_eigenstrain(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &SolverConfig,
thermal: Option<ThermalExpansion<'_>>,
) -> Result<FemSolution, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(vertex, _, _) in boundary.prescribed.iter().chain(boundary.loads.iter()) {
if vertex as usize >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex,
vertex_count,
});
}
}
let elements = build_elements(mesh)?;
let (lambda, mu) = material.lame();
check_element_scale(&elements, lambda, mu)?;
let ndof = vertex_count * 3;
let thermal_stress = match &thermal {
None => None,
Some(t) => Some(thermal_stresses(mesh, &elements, t, lambda, mu)?),
};
let mut prescribed_value = vec![Fix128::ZERO; ndof];
let mut is_free = vec![true; ndof];
for &(vertex, axis, value) in &boundary.prescribed {
let d = vertex as usize * 3 + axis.index();
is_free[d] = false;
prescribed_value[d] = value;
}
let constrained = is_free.iter().filter(|f| !**f).count();
if constrained < 6 {
return Err(FemError::UnderConstrained);
}
let mut scratch = vec![Fix128::ZERO; ndof];
apply_stiffness(&elements, &prescribed_value, lambda, mu, &mut scratch);
let mut b = vec![Fix128::ZERO; ndof];
for &(vertex, axis, force) in &boundary.loads {
let d = vertex as usize * 3 + axis.index();
if is_free[d] {
b[d] = b[d] + force;
}
}
if let Some(stresses) = &thermal_stress {
add_eigenstrain_load(&elements, stresses, &mut b);
}
for (d, value) in b.iter_mut().enumerate() {
if is_free[d] {
*value = *value - scratch[d];
} else {
*value = Fix128::ZERO;
}
}
let diag = stiffness_diagonal(&elements, lambda, mu, ndof);
let precond = build_preconditioner(&diag, &is_free, config)?;
let cg = conjugate_gradient(&b, &is_free, &precond, config, |p, out| {
apply_stiffness(&elements, p, lambda, mu, out);
})?;
let mut x = cg.x;
for (d, value) in x.iter_mut().enumerate() {
if !is_free[d] {
*value = prescribed_value[d];
}
}
let displacements = (0..vertex_count)
.map(|v| [x[v * 3], x[v * 3 + 1], x[v * 3 + 2]])
.collect();
let element_stress = match &thermal_stress {
None => elements
.iter()
.map(|e| element_stress(e, &x, lambda, mu))
.collect(),
Some(stresses) => elements
.iter()
.zip(stresses.iter())
.map(|(e, &s)| sub_stress(element_stress(e, &x, lambda, mu), s))
.collect(),
};
Ok(FemSolution {
displacements,
element_stress,
iterations: cg.iterations,
relative_residual: relative(cg.residual_norm, cg.b_norm),
effective_relative_tolerance: relative(cg.target, cg.b_norm),
})
}
fn add_eigenstrain_load(elements: &[Element], stresses: &[StressTensor], out: &mut [Fix128]) {
for (element, &s) in elements.iter().zip(stresses.iter()) {
let force = element_force_from_stress(element, s);
for (f, &node) in force.iter().zip(element.nodes.iter()) {
let base = node * 3;
out[base] = out[base] + f[0];
out[base + 1] = out[base + 1] + f[1];
out[base + 2] = out[base + 2] + f[2];
}
}
}
fn flatten(displacements: &[[Fix128; 3]]) -> Vec<Fix128> {
let mut u = vec![Fix128::ZERO; displacements.len() * 3];
for (v, d) in displacements.iter().enumerate() {
u[v * 3] = d[0];
u[v * 3 + 1] = d[1];
u[v * 3 + 2] = d[2];
}
u
}
fn free_mask(boundary: &BoundaryConditions, ndof: usize) -> Vec<bool> {
let mut is_free = vec![true; ndof];
for &(vertex, axis, _) in &boundary.prescribed {
is_free[vertex as usize * 3 + axis.index()] = false;
}
is_free
}
fn check_reaction_inputs(
mesh: &SdfTetMesh,
boundary: &BoundaryConditions,
nodes: usize,
) -> Result<usize, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(vertex, _, _) in boundary.prescribed.iter().chain(boundary.loads.iter()) {
if vertex as usize >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex,
vertex_count,
});
}
}
if nodes != vertex_count {
return Err(FemError::SolutionDoesNotMatchMesh {
nodes,
vertex_count,
});
}
Ok(vertex_count)
}
#[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: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
thermal: Option<ThermalExpansion<'_>>,
solution: &FemSolution,
) -> Result<Vec<[Fix128; 3]>, FemError> {
let vertex_count = check_reaction_inputs(mesh, boundary, solution.displacements.len())?;
let elements = build_elements(mesh)?;
let (lambda, mu) = material.lame();
check_element_scale(&elements, lambda, mu)?;
let ndof = vertex_count * 3;
let thermal_stress = match &thermal {
None => None,
Some(t) => Some(thermal_stresses(mesh, &elements, t, lambda, mu)?),
};
let u = flatten(&solution.displacements);
let mut force = vec![Fix128::ZERO; ndof];
apply_stiffness(&elements, &u, lambda, mu, &mut force);
if let Some(stresses) = &thermal_stress {
let mut load = vec![Fix128::ZERO; ndof];
add_eigenstrain_load(&elements, stresses, &mut load);
for (value, taken) in force.iter_mut().zip(load.iter()) {
*value = *value - *taken;
}
}
for &(vertex, axis, applied) in &boundary.loads {
let d = vertex as usize * 3 + axis.index();
force[d] = force[d] - applied;
}
let is_free = free_mask(boundary, ndof);
for (d, value) in force.iter_mut().enumerate() {
if is_free[d] {
*value = Fix128::ZERO;
}
}
Ok((0..vertex_count)
.map(|v| [force[v * 3], force[v * 3 + 1], force[v * 3 + 2]])
.collect())
}
#[inline]
fn relative(residual_norm: Fix128, b_norm: Fix128) -> Fix128 {
if b_norm.is_zero() {
Fix128::ZERO
} else {
residual_norm / b_norm
}
}
pub fn stiffness_diagonal_stats(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
) -> Result<DiagonalStats, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(vertex, _, _) in boundary.prescribed.iter().chain(boundary.loads.iter()) {
if vertex as usize >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex,
vertex_count,
});
}
}
let elements = build_elements(mesh)?;
let (lambda, mu) = material.lame();
check_element_scale(&elements, lambda, mu)?;
let ndof = vertex_count * 3;
let mut is_free = vec![true; ndof];
for &(vertex, axis, _) in &boundary.prescribed {
is_free[vertex as usize * 3 + axis.index()] = false;
}
let diag = stiffness_diagonal(&elements, lambda, mu, ndof);
let mut min = Fix128::ZERO;
let mut max = Fix128::ZERO;
let mut sum = Fix128::ZERO;
let mut count = 0usize;
for (d, value) in diag.iter().enumerate() {
if !is_free[d] {
continue;
}
if count == 0 || *value < min {
min = *value;
}
if count == 0 || *value > max {
max = *value;
}
sum = sum + *value;
count += 1;
}
if count == 0 {
return Err(FemError::UnderConstrained);
}
Ok(DiagonalStats {
min,
max,
mean: sum / Fix128::from_int(i64::try_from(count).unwrap_or(i64::MAX)),
free_dofs: count,
})
}
const POLAR_DET_FLOOR: Fix128 = Fix128::from_raw(0, 1 << 44);
const FRAME_SETTLED: Fix128 = Fix128 { hi: 0, lo: 16 };
fn frames_settled(next: &[Mat3Fix], previous: &[Mat3Fix]) -> bool {
for (a, b) in next.iter().zip(previous.iter()) {
for (ca, cb) in [(a.col0, b.col0), (a.col1, b.col1), (a.col2, b.col2)] {
for (x, y) in [(ca.x, cb.x), (ca.y, cb.y), (ca.z, cb.z)] {
if (x - y).abs() >= FRAME_SETTLED {
return false;
}
}
}
}
true
}
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
}
pub(crate) mod consistent_tangent;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CorotationalConfig {
linear: SolverConfig,
newton_iterations: u32,
newton_tolerance: Fix128,
increments: u32,
polar_iterations: u32,
material: Option<HyperelasticModel>,
consistent_tangent: bool,
}
impl CorotationalConfig {
pub fn try_new(
linear: SolverConfig,
newton_iterations: u32,
newton_tolerance: Fix128,
increments: u32,
polar_iterations: u32,
) -> Result<Self, FemError> {
if newton_iterations == 0 {
return Err(FemError::InvalidConfig(
"newton_iterations must be positive",
));
}
if increments == 0 {
return Err(FemError::InvalidConfig("increments must be positive"));
}
if polar_iterations == 0 {
return Err(FemError::InvalidConfig("polar_iterations must be positive"));
}
if newton_tolerance <= Fix128::ZERO || newton_tolerance >= Fix128::ONE {
return Err(FemError::InvalidConfig(
"newton_tolerance must be a fraction strictly between 0 and 1",
));
}
Ok(Self {
linear,
newton_iterations,
newton_tolerance,
increments,
polar_iterations,
material: None,
consistent_tangent: false,
})
}
#[must_use]
pub const fn with_hyperelastic(mut self, model: HyperelasticModel) -> Self {
self.material = Some(model);
self
}
#[must_use]
pub const fn hyperelastic(&self) -> Option<HyperelasticModel> {
self.material
}
#[must_use]
pub const fn with_consistent_tangent(mut self) -> Self {
self.consistent_tangent = true;
self
}
#[must_use]
pub const fn consistent_tangent(&self) -> bool {
self.consistent_tangent
}
#[must_use]
pub const fn linear(&self) -> SolverConfig {
self.linear
}
#[must_use]
pub const fn newton_iterations(&self) -> u32 {
self.newton_iterations
}
#[must_use]
pub const fn newton_tolerance(&self) -> Fix128 {
self.newton_tolerance
}
#[must_use]
pub const fn increments(&self) -> u32 {
self.increments
}
#[must_use]
pub const fn polar_iterations(&self) -> u32 {
self.polar_iterations
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CorotationalSolution {
pub field: FemSolution,
pub newton_iterations: u32,
pub increments: u32,
}
#[inline]
fn rows_of(m: Mat3Fix) -> [[Fix128; 3]; 3] {
[
[m.col0.x, m.col1.x, m.col2.x],
[m.col0.y, m.col1.y, m.col2.y],
[m.col0.z, m.col1.z, m.col2.z],
]
}
#[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 deformation_gradient(element: &Element, u: &[[Fix128; 3]; 4]) -> Mat3Fix {
let mut cols = [[Fix128::ZERO; 3]; 3];
for (g, node_u) in element.grad.iter().zip(u.iter()) {
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 rotate_stress(r: Mat3Fix, s: StressTensor) -> StressTensor {
let m = Mat3Fix::from_cols(
Vec3Fix::new(s.xx, s.xy, s.zx),
Vec3Fix::new(s.xy, s.yy, s.yz),
Vec3Fix::new(s.zx, s.yz, s.zz),
);
let rotated = r.mul_mat(m).mul_mat(r.transpose());
StressTensor {
xx: rotated.col0.x,
yy: rotated.col1.y,
zz: rotated.col2.z,
xy: rotated.col1.x,
yz: rotated.col2.y,
zx: rotated.col2.x,
}
}
fn corotational_local_stress(
rotation_transpose: Mat3Fix,
gradient: Mat3Fix,
lambda: Fix128,
mu: Fix128,
) -> StressTensor {
let g = rows_of(rotation_transpose.mul_mat(gradient));
let exx = g[0][0] - Fix128::ONE;
let eyy = g[1][1] - Fix128::ONE;
let ezz = g[2][2] - Fix128::ONE;
let gxy = g[0][1] + g[1][0];
let gyz = g[1][2] + g[2][1];
let gzx = g[0][2] + g[2][0];
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 settle_step(
elements: &[Element],
previous: &[Fix128],
next: &mut [Fix128],
accepted: Option<Vec<Fix128>>,
) {
match accepted {
Some(moved) => next.copy_from_slice(&moved),
None => step_stays_regular(elements, previous, next),
}
}
const REGULARITY_BACKTRACKS: u32 = 16;
fn all_elements_regular(elements: &[Element], u: &[Fix128]) -> bool {
elements
.iter()
.all(|e| deformation_gradient(e, &gather(e, u)).determinant() > POLAR_DET_FLOOR)
}
fn step_stays_regular(elements: &[Element], previous: &[Fix128], next: &mut [Fix128]) {
if all_elements_regular(elements, next) {
return;
}
let full: Vec<Fix128> = next.to_vec();
for k in 1..=REGULARITY_BACKTRACKS {
let alpha = Fix128::from_raw(0, 1u64 << (64 - k));
for ((n, p), f) in next.iter_mut().zip(previous.iter()).zip(full.iter()) {
*n = *p + (*f - *p) * alpha;
}
if all_elements_regular(elements, next) {
return;
}
}
}
pub(crate) fn hyperelastic_volumetric_modulus(
model: &HyperelasticModel,
lambda: Fix128,
) -> Result<Fix128, FemError> {
crate::hyperelastic::volumetric_modulus(model, lambda).ok_or(FemError::InvalidConfig(
"hyperelastic model needs lambda >= 4*C2 (Mooney-Rivlin) or 8*C2 (Yeoh): volumetric modulus would be negative",
))
}
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))
}
fn element_force_from_piola(element: &Element, p: Mat3Fix) -> [[Fix128; 3]; 4] {
let rows = rows_of(p);
let mut force = [[Fix128::ZERO; 3]; 4];
for (slot, g) in force.iter_mut().zip(element.grad.iter()) {
for (axis, out) in slot.iter_mut().enumerate() {
let row = rows[axis];
*out = element.volume * (row[0] * g[0] + row[1] * g[1] + row[2] * g[2]);
}
}
force
}
#[inline]
fn quarter() -> Fix128 {
Fix128::from_raw(0, 1 << 62)
}
#[inline]
fn sub_stress(a: StressTensor, b: StressTensor) -> StressTensor {
StressTensor {
xx: a.xx - b.xx,
yy: a.yy - b.yy,
zz: a.zz - b.zz,
xy: a.xy - b.xy,
yz: a.yz - b.yz,
zx: a.zx - b.zx,
}
}
fn thermal_stresses(
mesh: &SdfTetMesh,
elements: &[Element],
thermal: &ThermalExpansion<'_>,
lambda: Fix128,
mu: Fix128,
) -> Result<Vec<StressTensor>, FemError> {
for element in elements {
for &node in &element.nodes {
let q = mesh.vertices[node];
let p = Vec3Fix::new(
Fix128::from_f32(q[0]),
Fix128::from_f32(q[1]),
Fix128::from_f32(q[2]),
);
if !thermal.field.contains(p) {
return Err(FemError::TemperatureFieldDoesNotCoverMesh {
vertex: u32::try_from(node).unwrap_or(u32::MAX),
});
}
}
}
let bulk_three = lambda + lambda + lambda + mu + mu;
let mut out = Vec::with_capacity(elements.len());
for element in elements {
let mut sum = [Fix128::ZERO; 3];
for &node in &element.nodes {
let q = mesh.vertices[node];
sum[0] = sum[0] + Fix128::from_f32(q[0]);
sum[1] = sum[1] + Fix128::from_f32(q[1]);
sum[2] = sum[2] + Fix128::from_f32(q[2]);
}
let centroid = Vec3Fix::new(sum[0] * quarter(), sum[1] * quarter(), sum[2] * quarter());
let delta_t = thermal.field.sample(centroid);
let normal = bulk_three * thermal.alpha_per_k * delta_t;
out.push(StressTensor {
xx: normal,
yy: normal,
zz: normal,
..StressTensor::default()
});
}
Ok(out)
}
fn element_force_from_stress(element: &Element, s: StressTensor) -> [[Fix128; 3]; 4] {
let mut force = [[Fix128::ZERO; 3]; 4];
for (slot, g) in force.iter_mut().zip(element.grad.iter()) {
*slot = [
element.volume * (g[0] * s.xx + g[1] * s.xy + g[2] * s.zx),
element.volume * (g[1] * s.yy + g[0] * s.xy + g[2] * s.yz),
element.volume * (g[2] * s.zz + g[1] * s.yz + g[0] * s.zx),
];
}
force
}
fn apply_rotated_stiffness(
elements: &[Element],
rotations: &[Mat3Fix],
v: &[Fix128],
lambda: Fix128,
mu: Fix128,
out: &mut [Fix128],
) {
out.fill(Fix128::ZERO);
for (element, rotation) in elements.iter().zip(rotations.iter()) {
let transpose = rotation.transpose();
let gathered = gather(element, v);
let mut local = [[Fix128::ZERO; 3]; 4];
for (slot, node_v) in local.iter_mut().zip(gathered.iter()) {
*slot = mul3(transpose, *node_v);
}
let force = element_force_local(element, &local, lambda, mu);
for (f, &node) in force.iter().zip(element.nodes.iter()) {
let global = mul3(*rotation, *f);
let base = node * 3;
out[base] = out[base] + global[0];
out[base + 1] = out[base + 1] + global[1];
out[base + 2] = out[base + 2] + global[2];
}
}
}
struct Assembly<'a> {
elements: &'a [Element],
rotations: &'a [Mat3Fix],
lame: (Fix128, Fix128),
is_free: &'a [bool],
}
fn corotational_residual(
assembly: &Assembly<'_>,
u: &[Fix128],
f_ext: &[Fix128],
material: Option<(HyperelasticModel, Fix128)>,
out: &mut [Fix128],
) -> Result<(), FemError> {
out.copy_from_slice(f_ext);
subtract_internal_force(assembly, u, material, out)?;
for (d, value) in out.iter_mut().enumerate() {
if !assembly.is_free[d] {
*value = Fix128::ZERO;
}
}
Ok(())
}
fn subtract_internal_force(
assembly: &Assembly<'_>,
u: &[Fix128],
material: Option<(HyperelasticModel, Fix128)>,
out: &mut [Fix128],
) -> Result<(), FemError> {
let &Assembly {
elements,
rotations,
lame: (lambda, mu),
..
} = assembly;
for (tet, (element, rotation)) in elements.iter().zip(rotations.iter()).enumerate() {
let gradient = deformation_gradient(element, &gather(element, u));
let (force, frame) = match &material {
Some((model, bulk)) => {
let (_, piola) = hyperelastic_stress(model, *bulk, gradient).ok_or(
FemError::RotationFailed {
tet,
cause: PolarError::Inverted,
},
)?;
(element_force_from_piola(element, piola), Mat3Fix::IDENTITY)
}
None => (
element_force_from_stress(
element,
corotational_local_stress(rotation.transpose(), gradient, lambda, mu),
),
*rotation,
),
};
for (f, &node) in force.iter().zip(element.nodes.iter()) {
let global = mul3(frame, *f);
let base = node * 3;
out[base] = out[base] - global[0];
out[base + 1] = out[base + 1] - global[1];
out[base + 2] = out[base + 2] - global[2];
}
}
Ok(())
}
fn material_correction(
assembly: &Assembly<'_>,
u: &[Fix128],
model: &HyperelasticModel,
bulk_modulus: Fix128,
out: &mut [Fix128],
) -> Result<(), FemError> {
let &Assembly {
elements,
rotations,
lame: (lambda, mu),
is_free,
} = assembly;
for (tet, (element, rotation)) in elements.iter().zip(rotations.iter()).enumerate() {
let gradient = deformation_gradient(element, &gather(element, u));
let (_, piola) =
hyperelastic_stress(model, bulk_modulus, gradient).ok_or(FemError::RotationFailed {
tet,
cause: PolarError::Inverted,
})?;
let linear = element_force_from_stress(
element,
corotational_local_stress(rotation.transpose(), gradient, lambda, mu),
);
let material = element_force_from_piola(element, piola);
for ((l, m), &node) in linear.iter().zip(material.iter()).zip(element.nodes.iter()) {
let rotated = mul3(*rotation, *l);
let base = node * 3;
for axis in 0..3 {
out[base + axis] = out[base + axis] + rotated[axis] - m[axis];
}
}
}
for (d, value) in out.iter_mut().enumerate() {
if !is_free[d] {
*value = Fix128::ZERO;
}
}
Ok(())
}
fn rotated_stiffness_diagonal(
elements: &[Element],
rotations: &[Mat3Fix],
lambda: Fix128,
mu: Fix128,
ndof: usize,
) -> Vec<Fix128> {
let mut diag = vec![Fix128::ZERO; ndof];
let lambda_2mu = lambda + mu + mu;
let lambda_mu = lambda + mu;
for (element, rotation) in elements.iter().zip(rotations.iter()) {
let r = rows_of(*rotation);
for (g, &node) in element.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 {
element.volume
* (lambda_2mu * sq[m] + mu * (sq[(m + 1) % 3] + sq[(m + 2) % 3]))
} else {
element.volume * (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
}
pub fn solve_corotational(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &CorotationalConfig,
) -> Result<CorotationalSolution, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(vertex, _, _) in boundary.prescribed.iter().chain(boundary.loads.iter()) {
if vertex as usize >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex,
vertex_count,
});
}
}
let elements = build_elements(mesh)?;
let (lambda, mu) = material.lame();
check_element_scale(&elements, lambda, mu)?;
let law = config
.material
.map(|model| hyperelastic_volumetric_modulus(&model, lambda).map(|k| (model, k)))
.transpose()?;
if config.consistent_tangent() && law.is_none() {
return Err(FemError::InvalidConfig(
"consistent_tangent needs a hyperelastic law (with_hyperelastic)",
));
}
let ndof = vertex_count * 3;
let mut prescribed_value = vec![Fix128::ZERO; ndof];
let mut is_free = vec![true; ndof];
for &(vertex, axis, value) in &boundary.prescribed {
let d = vertex as usize * 3 + axis.index();
is_free[d] = false;
prescribed_value[d] = value;
}
if is_free.iter().filter(|f| !**f).count() < 6 {
return Err(FemError::UnderConstrained);
}
let mut f_ext = vec![Fix128::ZERO; ndof];
for &(vertex, axis, force) in &boundary.loads {
let d = vertex as usize * 3 + axis.index();
if is_free[d] {
f_ext[d] = f_ext[d] + force;
}
}
let mut scratch = vec![Fix128::ZERO; ndof];
apply_stiffness(&elements, &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 rotations = vec![Mat3Fix::IDENTITY; elements.len()];
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;
for increment in 1..=config.increments {
let final_increment = increment == 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 {
let mut frames = Vec::with_capacity(elements.len());
let mut refused = None;
for (t, element) in elements.iter().enumerate() {
match deformation_gradient(element, &gather(element, &u))
.polar_rotation(POLAR_DET_FLOOR, config.polar_iterations)
{
Ok(r) => frames.push(r),
Err(cause) => {
refused = Some(FemError::RotationFailed { tet: t, cause });
break;
}
}
}
match refused {
None => {
if law.is_none() && step > 0 && frames_settled(&frames, &rotations) {
break;
}
rotations.copy_from_slice(&frames);
}
Some(error) if step > 0 => return Err(error),
Some(_) => {}
}
let residual_decides = !final_increment || law.is_some();
if (residual_decides && step > 0) || step >= config.newton_iterations {
corotational_residual(
&Assembly {
elements: &elements,
rotations: &rotations,
lame: (lambda, mu),
is_free: &is_free,
},
&u,
&f_ext,
law,
&mut residual,
)?;
let residual_reach = max_abs(&residual);
if residual_decides && step > 0 && residual_reach <= newton_target {
break;
}
if step >= config.newton_iterations {
return Err(FemError::NotConverged {
iterations: step,
relative_residual: relative(residual_reach, newton_target),
});
}
}
if config.consistent_tangent() && step > 0 {
if let Some(law) = law {
let assembly = Assembly {
elements: &elements,
rotations: &rotations,
lame: (lambda, mu),
is_free: &is_free,
};
let report = consistent_tangent::newton_step(
&assembly,
&mut u,
&f_ext,
law,
&config.linear,
&mut residual,
)?;
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;
}
}
let assembly = Assembly {
elements: &elements,
rotations: &rotations,
lame: (lambda, mu),
is_free: &is_free,
};
corotational_residual(&assembly, &boundary_field, &f_ext, None, &mut residual)?;
if let Some((model, bulk)) = law {
if step > 0 {
material_correction(&assembly, &u, &model, bulk, &mut residual)?;
}
}
let diag = rotated_stiffness_diagonal(&elements, &rotations, lambda, mu, ndof);
let precond = build_preconditioner(&diag, &is_free, &config.linear)?;
let cg =
conjugate_gradient(&residual, &is_free, &precond, &config.linear, |p, out| {
apply_rotated_stiffness(&elements, &rotations, p, lambda, mu, out);
})?;
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);
let mut next: Vec<Fix128> = (0..ndof)
.map(|d| {
if is_free[d] {
cg.x[d]
} else {
boundary_field[d]
}
})
.collect();
if law.is_some() && step > 0 {
let assembly = Assembly {
elements: &elements,
rotations: &rotations,
lame: (lambda, mu),
is_free: &is_free,
};
let mut probe = vec![Fix128::ZERO; ndof];
corotational_residual(&assembly, &u, &f_ext, law, &mut probe)?;
let before = max_abs(&probe);
let delta: Vec<Fix128> = next.iter().zip(u.iter()).map(|(n, p)| *n - *p).collect();
let accepted = consistent_tangent::backtrack(before, &u, &delta, &is_free, |t| {
corotational_residual(&assembly, t, &f_ext, law, &mut probe)
.ok()
.map(|()| max_abs(&probe))
});
settle_step(&elements, &u, &mut next, accepted);
}
u.copy_from_slice(&next);
step += 1;
newton_iterations = newton_iterations.saturating_add(1);
}
}
corotational_residual(
&Assembly {
elements: &elements,
rotations: &rotations,
lame: (lambda, mu),
is_free: &is_free,
},
&u,
&f_ext,
law,
&mut residual,
)?;
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 displacements = (0..vertex_count)
.map(|v| [u[v * 3], u[v * 3 + 1], u[v * 3 + 2]])
.collect();
let mut element_stress = Vec::with_capacity(elements.len());
for (tet, (element, rotation)) in elements.iter().zip(rotations.iter()).enumerate() {
let gradient = deformation_gradient(element, &gather(element, &u));
element_stress.push(match law {
Some((model, bulk)) => {
hyperelastic_stress(&model, bulk, gradient)
.ok_or(FemError::RotationFailed {
tet,
cause: PolarError::Inverted,
})?
.0
}
None => rotate_stress(
*rotation,
corotational_local_stress(rotation.transpose(), gradient, lambda, mu),
),
});
}
Ok(CorotationalSolution {
field: FemSolution {
displacements,
element_stress,
iterations: cg_iterations,
relative_residual,
effective_relative_tolerance,
},
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 corotational_reactions(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &CorotationalConfig,
solution: &CorotationalSolution,
) -> Result<Vec<[Fix128; 3]>, FemError> {
let vertex_count = check_reaction_inputs(mesh, boundary, solution.field.displacements.len())?;
let elements = build_elements(mesh)?;
let (lambda, mu) = material.lame();
check_element_scale(&elements, lambda, mu)?;
let law = config
.material
.map(|model| hyperelastic_volumetric_modulus(&model, lambda).map(|k| (model, k)))
.transpose()?;
let ndof = vertex_count * 3;
let u = flatten(&solution.field.displacements);
let is_free = free_mask(boundary, ndof);
let mut rotations = Vec::with_capacity(elements.len());
for (tet, element) in elements.iter().enumerate() {
let frame = deformation_gradient(element, &gather(element, &u))
.polar_rotation(POLAR_DET_FLOOR, config.polar_iterations)
.map_err(|cause| FemError::RotationFailed { tet, cause })?;
rotations.push(frame);
}
let mut out = vec![Fix128::ZERO; ndof];
subtract_internal_force(
&Assembly {
elements: &elements,
rotations: &rotations,
lame: (lambda, mu),
is_free: &is_free,
},
&u,
law,
&mut out,
)?;
for &(vertex, axis, applied) in &boundary.loads {
let d = vertex as usize * 3 + axis.index();
out[d] = out[d] + applied;
}
for (d, value) in out.iter_mut().enumerate() {
*value = if is_free[d] { Fix128::ZERO } else { -*value };
}
Ok((0..vertex_count)
.map(|v| [out[v * 3], out[v * 3 + 1], out[v * 3 + 2]])
.collect())
}
const PLASTIC_PARAMETER_MAX: i64 = 1 << 30;
const LOAD_FACTOR_MAX: i64 = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ElastoplasticConfig {
linear: SolverConfig,
newton_iterations: u32,
newton_tolerance: Fix128,
yield_stress_mpa: Fix128,
hardening_modulus_mpa: Fix128,
}
impl ElastoplasticConfig {
pub fn try_new(
linear: SolverConfig,
newton_iterations: u32,
newton_tolerance: Fix128,
yield_stress_mpa: Fix128,
hardening_modulus_mpa: Fix128,
) -> Result<Self, FemError> {
if newton_iterations == 0 {
return Err(FemError::InvalidConfig(
"newton_iterations must be positive",
));
}
if newton_tolerance <= Fix128::ZERO || newton_tolerance >= Fix128::ONE {
return Err(FemError::InvalidConfig(
"newton_tolerance must be a fraction strictly between 0 and 1",
));
}
let cap = Fix128::from_int(PLASTIC_PARAMETER_MAX);
if yield_stress_mpa <= Fix128::ZERO || yield_stress_mpa > cap {
return Err(FemError::InvalidConfig(
"yield_stress_mpa must be positive and at most 2^30",
));
}
if hardening_modulus_mpa.is_negative() || hardening_modulus_mpa > cap {
return Err(FemError::InvalidConfig(
"hardening_modulus_mpa must be in [0, 2^30] (softening is not supported)",
));
}
Ok(Self {
linear,
newton_iterations,
newton_tolerance,
yield_stress_mpa,
hardening_modulus_mpa,
})
}
}
impl Default for ElastoplasticConfig {
fn default() -> Self {
Self {
linear: SolverConfig::default(),
newton_iterations: 50,
newton_tolerance: Fix128::from_raw(0, 1 << 44),
yield_stress_mpa: Fix128::from_int(PLASTIC_PARAMETER_MAX),
hardening_modulus_mpa: Fix128::ZERO,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ElastoplasticSolution {
pub field: FemSolution,
pub plastic_strain: Vec<StressTensor>,
pub equivalent_plastic_strain: Vec<Fix128>,
pub dissipation: Vec<Fix128>,
pub newton_iterations: u32,
pub steps: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct PlasticHeating {
taylor_quinney: Fix128,
volumetric_heat_capacity_mpa_per_k: Fix128,
}
impl PlasticHeating {
pub fn try_new(
taylor_quinney: Fix128,
volumetric_heat_capacity_mpa_per_k: Fix128,
) -> Result<Self, FemError> {
if taylor_quinney.is_negative() || taylor_quinney > Fix128::ONE {
return Err(FemError::InvalidConfig(
"taylor_quinney must be a fraction in [0, 1]",
));
}
if volumetric_heat_capacity_mpa_per_k <= Fix128::ZERO {
return Err(FemError::InvalidConfig(
"volumetric_heat_capacity_mpa_per_k must be positive (MPa/K, not J/(kg K))",
));
}
Ok(Self {
taylor_quinney,
volumetric_heat_capacity_mpa_per_k,
})
}
#[must_use]
pub const fn taylor_quinney(&self) -> Fix128 {
self.taylor_quinney
}
#[must_use]
pub const fn volumetric_heat_capacity_mpa_per_k(&self) -> Fix128 {
self.volumetric_heat_capacity_mpa_per_k
}
#[must_use]
pub fn temperature_rise(&self, dissipation: Fix128) -> Fix128 {
self.taylor_quinney * dissipation / self.volumetric_heat_capacity_mpa_per_k
}
}
#[must_use = "the temperature rise is the whole output of the conversion; \
dropping it leaves the dissipated heat unaccounted for"]
pub fn plastic_temperature_rise(
solution: &ElastoplasticSolution,
heating: &PlasticHeating,
) -> Vec<Fix128> {
solution
.dissipation
.iter()
.map(|&w| heating.temperature_rise(w))
.collect()
}
pub fn deposit_plastic_heat(
mesh: &SdfTetMesh,
solution: &ElastoplasticSolution,
heating: &PlasticHeating,
field: &mut CoupledField,
) -> Result<(), FemError> {
deposit_increment_heat(mesh, &solution.dissipation, heating, field)
}
pub fn deposit_increment_heat(
mesh: &SdfTetMesh,
plastic_work: &[Fix128],
heating: &PlasticHeating,
field: &mut CoupledField,
) -> Result<(), FemError> {
if mesh.vertices.is_empty() || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
if plastic_work.len() != mesh.tets.len() {
return Err(FemError::SolutionElementCountDoesNotMatchMesh {
elements: plastic_work.len(),
tet_count: mesh.tets.len(),
});
}
let (nx, ny, nz) = (field.nx(), field.ny(), field.nz());
for (count, axis) in [(nx, Axis::X), (ny, Axis::Y), (nz, Axis::Z)] {
if count < 2 {
return Err(FemError::DepositGridHasDegenerateAxis { axis });
}
}
let elements = build_elements(mesh)?;
let (hx, hy, hz) = field.cell_size();
let cell_volume = hx * hy * hz;
let quarter = Fix128::from_raw(0, 1 << 62);
let mut staged = field.clone();
staged.clear();
for (element, &work) in elements.iter().zip(plastic_work.iter()) {
if work.is_zero() {
continue;
}
let mut centroid = Vec3Fix::ZERO;
for &node in &element.nodes {
let v = mesh.vertices[node];
centroid = centroid
+ Vec3Fix::new(
Fix128::from_f32(v[0]),
Fix128::from_f32(v[1]),
Fix128::from_f32(v[2]),
);
}
centroid = centroid * quarter;
let rise = heating.temperature_rise(work) * element.volume / cell_volume;
staged.splat(centroid, rise);
}
for iz in 0..nz {
for iy in 0..ny {
for ix in 0..nx {
let staged_value = staged.get(ix, iy, iz);
if staged_value.is_zero() {
continue;
}
let mut boundary_axes = 0u32;
for (i, n) in [(ix, nx), (iy, ny), (iz, nz)] {
if i == 0 || i == n - 1 {
boundary_axes += 1;
}
}
let inverse_dual_weight = Fix128::from_int(1 << boundary_axes);
let corrected = staged_value * inverse_dual_weight;
field.set(ix, iy, iz, field.get(ix, iy, iz) + corrected);
}
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PlasticState {
strain: [Fix128; 6],
equivalent: Fix128,
dissipation: Fix128,
}
impl PlasticState {
const VIRGIN: Self = Self {
strain: [Fix128::ZERO; 6],
equivalent: Fix128::ZERO,
dissipation: Fix128::ZERO,
};
}
#[derive(Clone, Copy)]
struct PlasticTangent {
shear_loss: Fix128,
theta_bar: Fix128,
direction: StressTensor,
}
#[derive(Clone, Copy)]
struct ReturnMap {
stress: StressTensor,
state: PlasticState,
tangent: Option<PlasticTangent>,
}
fn element_strain(element: &Element, u: &[[Fix128; 3]; 4]) -> [Fix128; 6] {
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_u) in element.grad.iter().zip(u.iter()) {
let (ux, uy, uz) = (node_u[0], node_u[1], node_u[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;
}
[exx, eyy, ezz, gxy, gyz, gzx]
}
fn hooke(e: [Fix128; 6], lambda: Fix128, mu: Fix128) -> StressTensor {
let trace = e[0] + e[1] + e[2];
let two_mu = mu + mu;
StressTensor {
xx: lambda * trace + two_mu * e[0],
yy: lambda * trace + two_mu * e[1],
zz: lambda * trace + two_mu * e[2],
xy: mu * e[3],
yz: mu * e[4],
zx: mu * e[5],
}
}
fn return_map(
strain: [Fix128; 6],
state: &PlasticState,
lambda: Fix128,
mu: Fix128,
yield_stress: Fix128,
hardening: Fix128,
) -> ReturnMap {
let mut e = strain;
for (value, plastic) in e.iter_mut().zip(state.strain.iter()) {
*value = *value - *plastic;
}
let trial = hooke(e, lambda, mu);
let q = trial.von_mises();
let f = q - (yield_stress + hardening * state.equivalent);
if f <= Fix128::ZERO {
return ReturnMap {
stress: trial,
state: *state,
tangent: None,
};
}
let three_mu = Fix128::from_int(3) * mu;
let d_eq = f / (three_mu + hardening);
let pressure = (trial.xx + trial.yy + trial.zz) / Fix128::from_int(3);
let s = StressTensor {
xx: trial.xx - pressure,
yy: trial.yy - pressure,
zz: trial.zz - pressure,
..trial
};
let ratio = three_mu * d_eq / q;
let stress = StressTensor {
xx: trial.xx - ratio * s.xx,
yy: trial.yy - ratio * s.yy,
zz: trial.zz - ratio * s.zz,
xy: trial.xy - ratio * s.xy,
yz: trial.yz - ratio * s.yz,
zx: trial.zx - ratio * s.zx,
};
let flow = d_eq / q;
let normal = Fix128::from_raw(1, 1 << 63) * flow; let shear = Fix128::from_int(3) * flow;
let mut next = *state;
next.strain[0] = next.strain[0] + normal * s.xx;
next.strain[1] = next.strain[1] + normal * s.yy;
next.strain[2] = next.strain[2] + normal * s.zz;
next.strain[3] = next.strain[3] + shear * s.xy;
next.strain[4] = next.strain[4] + shear * s.yz;
next.strain[5] = next.strain[5] + shear * s.zx;
next.equivalent = next.equivalent + d_eq;
next.dissipation = next.dissipation + d_eq * (yield_stress + hardening * next.equivalent);
let frobenius = (s.xx * s.xx
+ s.yy * s.yy
+ s.zz * s.zz
+ Fix128::from_int(2) * (s.xy * s.xy + s.yz * s.yz + s.zx * s.zx))
.sqrt();
let direction = StressTensor {
xx: s.xx / frobenius,
yy: s.yy / frobenius,
zz: s.zz / frobenius,
xy: s.xy / frobenius,
yz: s.yz / frobenius,
zx: s.zx / frobenius,
};
ReturnMap {
stress,
state: next,
tangent: Some(PlasticTangent {
shear_loss: ratio,
theta_bar: three_mu / (three_mu + hardening) - ratio,
direction,
}),
}
}
fn element_tangent_force(
element: &Element,
p: &[[Fix128; 3]; 4],
tangent: Option<&PlasticTangent>,
lambda: Fix128,
mu: Fix128,
) -> [[Fix128; 3]; 4] {
let Some(t) = tangent else {
return element_force_local(element, p, lambda, mu);
};
let e = element_strain(element, p);
let elastic = hooke(e, lambda, mu);
let two_mu = mu + mu;
let mean = (e[0] + e[1] + e[2]) / Fix128::from_int(3);
let n = t.direction;
let projection =
n.xx * e[0] + n.yy * e[1] + n.zz * e[2] + n.xy * e[3] + n.yz * e[4] + n.zx * e[5];
let along = two_mu * t.theta_bar * projection;
let loss = two_mu * t.shear_loss;
let stress = StressTensor {
xx: elastic.xx - loss * (e[0] - mean) - along * n.xx,
yy: elastic.yy - loss * (e[1] - mean) - along * n.yy,
zz: elastic.zz - loss * (e[2] - mean) - along * n.zz,
xy: elastic.xy - mu * t.shear_loss * e[3] - along * n.xy,
yz: elastic.yz - mu * t.shear_loss * e[4] - along * n.yz,
zx: elastic.zx - mu * t.shear_loss * e[5] - along * n.zx,
};
element_force_from_stress(element, stress)
}
pub fn solve_elastoplastic(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &ElastoplasticConfig,
load_path: &[Fix128],
) -> Result<ElastoplasticSolution, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(vertex, _, _) in boundary.prescribed.iter().chain(boundary.loads.iter()) {
if vertex as usize >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex,
vertex_count,
});
}
}
if load_path.is_empty() {
return Err(FemError::InvalidConfig("load_path must not be empty"));
}
let factor_cap = Fix128::from_int(LOAD_FACTOR_MAX);
if load_path.iter().any(|t| t.abs() > factor_cap) {
return Err(FemError::InvalidConfig(
"load factors must have magnitude at most 2^20",
));
}
let problem = ElastoplasticProblem::try_new(mesh, material, boundary, config)?;
let mut state = problem.virgin_state();
for &factor in load_path {
let increment = problem.step(&state, &ElastoplasticIncrementRequest::new(factor))?;
increment.commit(&mut state);
}
Ok(problem.finish(&state, load_path.len()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ThermalSoftening {
yield_per_k: Fix128,
hardening_per_k: Fix128,
}
impl ThermalSoftening {
pub fn try_new(yield_per_k: Fix128, hardening_per_k: Fix128) -> Result<Self, FemError> {
let cap = Fix128::from_int(PLASTIC_PARAMETER_MAX);
if yield_per_k.is_negative() || hardening_per_k.is_negative() {
return Err(FemError::InvalidConfig(
"thermal softening fractions must not be negative",
));
}
if yield_per_k > cap || hardening_per_k > cap {
return Err(FemError::InvalidConfig(
"thermal softening fractions must be at most 2^30 per kelvin",
));
}
Ok(Self {
yield_per_k,
hardening_per_k,
})
}
#[must_use]
pub const fn none() -> Self {
Self {
yield_per_k: Fix128::ZERO,
hardening_per_k: Fix128::ZERO,
}
}
#[must_use]
pub const fn yield_per_k(&self) -> Fix128 {
self.yield_per_k
}
#[must_use]
pub const fn hardening_per_k(&self) -> Fix128 {
self.hardening_per_k
}
fn at(&self, delta_t: Fix128, yield_stress: Fix128, hardening: Fix128) -> (Fix128, Fix128) {
let shrink = |fraction: Fix128, value: Fix128| -> Fix128 {
let scale = Fix128::ONE - fraction * delta_t;
if scale.is_negative() {
Fix128::ZERO
} else {
value * scale
}
};
(
shrink(self.yield_per_k, yield_stress),
shrink(self.hardening_per_k, hardening),
)
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ElastoplasticIncrementRequest<'a> {
factor: Fix128,
thermal: Option<ThermalExpansion<'a>>,
softening: Option<ThermalSoftening>,
}
impl<'a> ElastoplasticIncrementRequest<'a> {
#[must_use]
pub const fn new(factor: Fix128) -> Self {
Self {
factor,
thermal: None,
softening: None,
}
}
#[must_use]
pub const fn with_thermal(
mut self,
thermal: ThermalExpansion<'a>,
softening: Option<ThermalSoftening>,
) -> Self {
self.thermal = Some(thermal);
self.softening = softening;
self
}
#[must_use]
pub const fn factor(&self) -> Fix128 {
self.factor
}
#[must_use]
pub const fn thermal(&self) -> Option<ThermalExpansion<'a>> {
self.thermal
}
#[must_use]
pub const fn softening(&self) -> Option<ThermalSoftening> {
self.softening
}
}
#[derive(Debug, Clone)]
pub struct ElastoplasticProblem {
elements: Vec<Element>,
lambda: Fix128,
mu: Fix128,
ndof: usize,
vertex_count: usize,
prescribed_value: Vec<Fix128>,
is_free: Vec<bool>,
f_ext: Vec<Fix128>,
precond: Vec<Fix128>,
config: ElastoplasticConfig,
node_positions: Vec<Vec3Fix>,
centroids: Vec<Vec3Fix>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ElastoplasticState {
committed: Vec<PlasticState>,
u: Vec<Fix128>,
field: FemSolution,
newton_total: u32,
}
impl ElastoplasticState {
#[must_use]
pub fn equivalent_plastic_strain(&self) -> Vec<Fix128> {
self.committed.iter().map(|s| s.equivalent).collect()
}
#[must_use]
pub fn dissipation(&self) -> Vec<Fix128> {
self.committed.iter().map(|s| s.dissipation).collect()
}
#[must_use]
pub fn displacements(&self) -> Vec<[Fix128; 3]> {
self.field.displacements.clone()
}
#[must_use]
pub const fn newton_iterations(&self) -> u32 {
self.newton_total
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ElastoplasticIncrement {
pub field: FemSolution,
pub plastic_work_increment: Vec<Fix128>,
trial: Vec<PlasticState>,
u: Vec<Fix128>,
newton_iterations: u32,
}
impl ElastoplasticIncrement {
pub fn commit(self, state: &mut ElastoplasticState) {
state.committed = self.trial;
state.u = self.u;
state.field = self.field;
state.newton_total = state.newton_total.saturating_add(self.newton_iterations);
}
#[must_use]
pub const fn newton_iterations(&self) -> u32 {
self.newton_iterations
}
}
impl ElastoplasticProblem {
pub fn try_new(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary: &BoundaryConditions,
config: &ElastoplasticConfig,
) -> Result<Self, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
for &(vertex, _, _) in boundary.prescribed.iter().chain(boundary.loads.iter()) {
if vertex as usize >= vertex_count {
return Err(FemError::VertexOutOfRange {
vertex,
vertex_count,
});
}
}
let elements = build_elements(mesh)?;
let (lambda, mu) = material.lame();
check_element_scale(&elements, lambda, mu)?;
let ndof = vertex_count * 3;
let mut prescribed_value = vec![Fix128::ZERO; ndof];
let mut is_free = vec![true; ndof];
for &(vertex, axis, value) in &boundary.prescribed {
let d = vertex as usize * 3 + axis.index();
is_free[d] = false;
prescribed_value[d] = value;
}
if is_free.iter().filter(|f| !**f).count() < 6 {
return Err(FemError::UnderConstrained);
}
let mut f_ext = vec![Fix128::ZERO; ndof];
for &(vertex, axis, force) in &boundary.loads {
let d = vertex as usize * 3 + axis.index();
if is_free[d] {
f_ext[d] = f_ext[d] + force;
}
}
let diag = stiffness_diagonal(&elements, lambda, mu, ndof);
let precond = build_preconditioner(&diag, &is_free, &config.linear)?;
let node_positions: Vec<Vec3Fix> = mesh
.vertices
.iter()
.map(|q| {
Vec3Fix::new(
Fix128::from_f32(q[0]),
Fix128::from_f32(q[1]),
Fix128::from_f32(q[2]),
)
})
.collect();
let centroids: Vec<Vec3Fix> = elements
.iter()
.map(|element| {
let mut sum = Vec3Fix::ZERO;
for &node in &element.nodes {
sum = sum + node_positions[node];
}
sum * quarter()
})
.collect();
Ok(Self {
elements,
lambda,
mu,
ndof,
vertex_count,
prescribed_value,
is_free,
f_ext,
precond,
config: *config,
node_positions,
centroids,
})
}
#[must_use]
pub fn virgin_state(&self) -> ElastoplasticState {
ElastoplasticState {
committed: vec![PlasticState::VIRGIN; self.elements.len()],
u: vec![Fix128::ZERO; self.ndof],
field: FemSolution {
displacements: vec![[Fix128::ZERO; 3]; self.vertex_count],
element_stress: Vec::new(),
iterations: 0,
relative_residual: Fix128::ZERO,
effective_relative_tolerance: Fix128::ZERO,
},
newton_total: 0,
}
}
pub fn step(
&self,
state: &ElastoplasticState,
request: &ElastoplasticIncrementRequest,
) -> Result<ElastoplasticIncrement, FemError> {
let config = &self.config;
let (lambda, mu) = (self.lambda, self.mu);
let sigma_y = config.yield_stress_mpa;
let hardening = config.hardening_modulus_mpa;
let ndof = self.ndof;
let thermal: Option<Vec<(Fix128, Fix128, Fix128)>> = match request.thermal {
None => None,
Some(field) => {
for element in &self.elements {
for &node in &element.nodes {
if !field.field.contains(self.node_positions[node]) {
return Err(FemError::TemperatureFieldDoesNotCoverMesh {
vertex: u32::try_from(node).unwrap_or(u32::MAX),
});
}
}
}
let law = request.softening.unwrap_or_else(ThermalSoftening::none);
Some(
self.centroids
.iter()
.map(|¢roid| {
let delta_t = field.field.sample(centroid);
let (y, h) = law.at(delta_t, sigma_y, hardening);
(field.alpha_per_k * delta_t, y, h)
})
.collect(),
)
}
};
let mut u = state.u.clone();
let mut load = vec![Fix128::ZERO; ndof];
let mut residual = vec![Fix128::ZERO; ndof];
let mut maps: Vec<ReturnMap> = Vec::new();
let mut cg_iterations = state.field.iterations;
let mut relative_residual = state.field.relative_residual;
let mut effective_relative_tolerance = state.field.effective_relative_tolerance;
let mut newton_iterations = 0u32;
for d in 0..ndof {
if self.is_free[d] {
load[d] = request.factor() * self.f_ext[d];
} else {
u[d] = request.factor() * self.prescribed_value[d];
}
}
let load_norm = max_abs(&load);
let mut reference_norm = Fix128::ZERO;
let mut iteration = 0u32;
loop {
maps.clear();
residual.copy_from_slice(&load);
for (e, (element, committed)) in
self.elements.iter().zip(state.committed.iter()).enumerate()
{
let mut strain = element_strain(element, &gather(element, &u));
let (yield_here, hardening_here) = match &thermal {
None => (sigma_y, hardening),
Some(per_element) => {
let (eigen, y, h) = per_element[e];
for normal in strain.iter_mut().take(3) {
*normal = *normal - eigen;
}
(y, h)
}
};
let map = return_map(strain, committed, lambda, mu, yield_here, hardening_here);
let force = element_force_from_stress(element, map.stress);
for (f, &node) in force.iter().zip(element.nodes.iter()) {
let base = node * 3;
for axis in 0..3 {
if self.is_free[base + axis] {
residual[base + axis] = residual[base + axis] - f[axis];
}
}
}
maps.push(map);
}
for (d, value) in residual.iter_mut().enumerate() {
if !self.is_free[d] {
*value = Fix128::ZERO;
}
}
let residual_max = max_abs(&residual);
if iteration == 0 {
reference_norm = if load_norm > residual_max {
load_norm
} else {
residual_max
};
}
let requested = reference_norm * config.newton_tolerance;
let target = if requested > RESIDUAL_NORM_FLOOR {
requested
} else {
RESIDUAL_NORM_FLOOR
};
if residual_max <= target {
break;
}
if iteration >= config.newton_iterations {
return Err(FemError::NotConverged {
iterations: iteration,
relative_residual: relative(residual_max, reference_norm),
});
}
let cg = conjugate_gradient(
&residual,
&self.is_free,
&self.precond,
&config.linear,
|p, out| {
out.fill(Fix128::ZERO);
for (element, map) in self.elements.iter().zip(maps.iter()) {
let force = element_tangent_force(
element,
&gather(element, p),
map.tangent.as_ref(),
lambda,
mu,
);
for (f, &node) in force.iter().zip(element.nodes.iter()) {
let base = node * 3;
out[base] = out[base] + f[0];
out[base + 1] = out[base + 1] + f[1];
out[base + 2] = out[base + 2] + f[2];
}
}
},
)?;
for ((value, free), delta) in u.iter_mut().zip(self.is_free.iter()).zip(cg.x.iter()) {
if *free {
*value = *value + *delta;
}
}
cg_iterations = cg.iterations;
relative_residual = relative(cg.residual_norm, cg.b_norm);
effective_relative_tolerance = relative(cg.target, cg.b_norm);
iteration += 1;
newton_iterations += 1;
}
let plastic_work_increment = maps
.iter()
.zip(state.committed.iter())
.map(|(map, committed)| map.state.dissipation - committed.dissipation)
.collect();
let displacements = (0..self.vertex_count)
.map(|v| [u[v * 3], u[v * 3 + 1], u[v * 3 + 2]])
.collect();
let element_stress = maps.iter().map(|m| m.stress).collect();
Ok(ElastoplasticIncrement {
field: FemSolution {
displacements,
element_stress,
iterations: cg_iterations,
relative_residual,
effective_relative_tolerance,
},
plastic_work_increment,
trial: maps.iter().map(|m| m.state).collect(),
u,
newton_iterations,
})
}
fn finish(&self, state: &ElastoplasticState, steps: usize) -> ElastoplasticSolution {
let plastic_strain = state
.committed
.iter()
.map(|s| StressTensor {
xx: s.strain[0],
yy: s.strain[1],
zz: s.strain[2],
xy: half() * s.strain[3],
yz: half() * s.strain[4],
zx: half() * s.strain[5],
})
.collect();
ElastoplasticSolution {
field: state.field.clone(),
plastic_strain,
equivalent_plastic_strain: state.equivalent_plastic_strain(),
dissipation: state.dissipation(),
newton_iterations: state.newton_total,
steps: u32::try_from(steps).unwrap_or(u32::MAX),
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct ThermoplasticCoupling {
heating: PlasticHeating,
expansion_per_k: Fix128,
material_reference: Fix128,
softening: Option<ThermalSoftening>,
relaxation: Fix128,
residual_floor_fraction: Fix128,
sub_iteration: crate::coupled_iteration::SubIterationConfig,
}
impl ThermoplasticCoupling {
pub fn try_new(
heating: PlasticHeating,
expansion_per_k: Fix128,
material_reference: Fix128,
softening: Option<ThermalSoftening>,
relaxation: Fix128,
residual_floor_fraction: Fix128,
sub_iteration: crate::coupled_iteration::SubIterationConfig,
) -> Result<Self, FemError> {
if relaxation <= Fix128::ZERO || relaxation > Fix128::ONE {
return Err(FemError::InvalidConfig(
"relaxation must be in (0, 1]: zero never moves, and over-relaxation is \
measured to be monotonically worse on this map",
));
}
if residual_floor_fraction <= Fix128::ZERO || residual_floor_fraction >= Fix128::ONE {
return Err(FemError::InvalidConfig(
"residual_floor_fraction must be in (0, 1): zero leaves the stopping rule \
relative-only, which the arithmetic cannot satisfy",
));
}
if expansion_per_k.is_negative() {
return Err(FemError::InvalidConfig(
"expansion_per_k must not be negative",
));
}
sub_iteration.validate().map_err(|fault| {
FemError::CoupledSubIterationFailed(
crate::coupled_iteration::CoupledIterationError::InvalidConfig { fault },
)
})?;
Ok(Self {
heating,
expansion_per_k,
material_reference,
softening,
relaxation,
residual_floor_fraction,
sub_iteration,
})
}
#[must_use]
pub const fn relaxation(&self) -> Fix128 {
self.relaxation
}
#[must_use]
pub const fn residual_floor_fraction(&self) -> Fix128 {
self.residual_floor_fraction
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ThermoplasticIncrement {
pub increment: ElastoplasticIncrement,
pub temperature_increment: CoupledField,
pub report: crate::coupled_iteration::SubIterationReport,
}
pub fn step_thermoplastic(
problem: &ElastoplasticProblem,
state: &ElastoplasticState,
mesh: &SdfTetMesh,
base: &CoupledField,
coupling: &ThermoplasticCoupling,
factor: Fix128,
) -> Result<ThermoplasticIncrement, FemError> {
use crate::coupled_iteration::residual_norm_inf;
let mut delta = base.clone();
delta.clear();
let mut target = base.clone();
let mut absolute = base.clone();
let mut difference = vec![Fix128::ZERO; base.cell_count()];
let mut floor = Fix128::ZERO;
let mut last: Option<ElastoplasticIncrement> = None;
let report = run_sub_iteration_fallible(coupling.sub_iteration, |index| {
absolute.as_mut_slice().copy_from_slice(base.as_slice());
for (value, &d) in absolute.as_mut_slice().iter_mut().zip(delta.as_slice()) {
*value = *value + d;
}
let rise = TemperatureRise::from_absolute(&absolute, coupling.material_reference);
let request = ElastoplasticIncrementRequest::new(factor).with_thermal(
ThermalExpansion::from_rise(&rise, coupling.expansion_per_k),
coupling.softening,
);
let increment = problem.step(state, &request)?;
target.clear();
deposit_increment_heat(
mesh,
&increment.plastic_work_increment,
&coupling.heating,
&mut target,
)?;
for ((slot, &t), &d) in difference
.iter_mut()
.zip(target.as_slice())
.zip(delta.as_slice())
{
*slot = t - d;
}
let raw_residual = residual_norm_inf(&difference);
if index == 0 {
floor = coupling.residual_floor_fraction * residual_norm_inf(target.as_slice());
}
let residual = if raw_residual <= floor {
Fix128::ZERO
} else {
raw_residual
};
last = Some(increment);
delta
.blend_from(&target, coupling.relaxation)
.expect("delta and target are clones of base, so the grids agree");
Ok(residual)
})?;
let increment = last.expect("a report means at least one sweep ran");
Ok(ThermoplasticIncrement {
increment,
temperature_increment: target,
report,
})
}
#[must_use = "the indicators are the whole point of estimating; dropping them \
leaves the refinement with nothing to go on"]
pub fn error_indicators_squared(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
solution: &FemSolution,
) -> Result<Vec<Fix128>, FemError> {
let vertex_count = mesh.vertices.len();
if vertex_count == 0 || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
if solution.displacements.len() != vertex_count {
return Err(FemError::SolutionDoesNotMatchMesh {
nodes: solution.displacements.len(),
vertex_count,
});
}
if solution.element_stress.len() != mesh.tets.len() {
return Err(FemError::SolutionElementCountDoesNotMatchMesh {
elements: solution.element_stress.len(),
tet_count: mesh.tets.len(),
});
}
let elements = build_elements(mesh)?;
let mut weight = vec![Fix128::ZERO; vertex_count];
let mut recovered = vec![StressTensor::default(); vertex_count];
for (element, stress) in elements.iter().zip(solution.element_stress.iter()) {
for &node in &element.nodes {
weight[node] = weight[node] + element.volume;
let acc = &mut recovered[node];
acc.xx = acc.xx + stress.xx * element.volume;
acc.yy = acc.yy + stress.yy * element.volume;
acc.zz = acc.zz + stress.zz * element.volume;
acc.xy = acc.xy + stress.xy * element.volume;
acc.yz = acc.yz + stress.yz * element.volume;
acc.zx = acc.zx + stress.zx * element.volume;
}
}
for (acc, &w) in recovered.iter_mut().zip(weight.iter()) {
if w.is_zero() {
continue;
}
acc.xx = acc.xx / w;
acc.yy = acc.yy / w;
acc.zz = acc.zz / w;
acc.xy = acc.xy / w;
acc.yz = acc.yz / w;
acc.zx = acc.zx / w;
}
let quarter = Fix128::from_raw(0, 1 << 62);
let mut indicators = Vec::with_capacity(elements.len());
for (element, stress) in elements.iter().zip(solution.element_stress.iter()) {
let mut star = StressTensor::default();
for &node in &element.nodes {
let r = &recovered[node];
star.xx = star.xx + r.xx;
star.yy = star.yy + r.yy;
star.zz = star.zz + r.zz;
star.xy = star.xy + r.xy;
star.yz = star.yz + r.yz;
star.zx = star.zx + r.zx;
}
let d = StressTensor {
xx: star.xx * quarter - stress.xx,
yy: star.yy * quarter - stress.yy,
zz: star.zz * quarter - stress.zz,
xy: star.xy * quarter - stress.xy,
yz: star.yz * quarter - stress.yz,
zx: star.zx * quarter - stress.zx,
};
let density = d.complementary_energy_density(material);
let clamped = if density.is_negative() {
Fix128::ZERO
} else {
density
};
indicators.push(element.volume * clamped);
}
Ok(indicators)
}
pub fn mark_bulk(
indicators_squared: &[Fix128],
bulk_fraction: Fix128,
) -> Result<Vec<bool>, FemError> {
if indicators_squared.is_empty() {
return Err(FemError::EmptyMesh);
}
if bulk_fraction <= Fix128::ZERO || bulk_fraction > Fix128::ONE {
return Err(FemError::InvalidConfig("bulk_fraction must be in (0, 1]"));
}
if indicators_squared.iter().any(|v| v.is_negative()) {
return Err(FemError::InvalidConfig(
"a squared error indicator cannot be negative",
));
}
let total = indicators_squared
.iter()
.fold(Fix128::ZERO, |acc, &v| acc + v);
let mut marked = vec![false; indicators_squared.len()];
if total.is_zero() {
return Ok(marked);
}
let target = mul_ceil_fraction(total, bulk_fraction);
let mut order: Vec<usize> = (0..indicators_squared.len()).collect();
order.sort_by(|&a, &b| {
indicators_squared[b]
.cmp(&indicators_squared[a])
.then(a.cmp(&b))
});
let mut running = Fix128::ZERO;
for index in order {
if running >= target {
break;
}
marked[index] = true;
running = running + indicators_squared[index];
}
Ok(marked)
}
fn mul_ceil_fraction(x: Fix128, f: Fix128) -> Fix128 {
let f_raw = (u128::from(f.hi as u64) << 64) | u128::from(f.lo);
let hi = u128::from(x.hi as u64);
let lo = u128::from(x.lo);
let low = lo * f_raw;
let carry = (low >> 64) + u128::from(low & u128::from(u64::MAX) != 0);
let raw = hi * f_raw + carry;
Fix128::from_raw((raw >> 64) as i64, raw as u64)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AdaptiveConfig {
linear: SolverConfig,
bulk_fraction: Fix128,
max_rounds: u32,
max_refine_passes: u32,
}
impl AdaptiveConfig {
pub fn try_new(
linear: SolverConfig,
bulk_fraction: Fix128,
max_rounds: u32,
max_refine_passes: u32,
) -> Result<Self, FemError> {
if max_rounds == 0 {
return Err(FemError::InvalidConfig("max_rounds must be positive"));
}
if max_refine_passes == 0 {
return Err(FemError::InvalidConfig(
"max_refine_passes must be positive",
));
}
if bulk_fraction <= Fix128::ZERO || bulk_fraction > Fix128::ONE {
return Err(FemError::InvalidConfig("bulk_fraction must be in (0, 1]"));
}
Ok(Self {
linear,
bulk_fraction,
max_rounds,
max_refine_passes,
})
}
#[must_use]
pub const fn linear(&self) -> SolverConfig {
self.linear
}
#[must_use]
pub const fn bulk_fraction(&self) -> Fix128 {
self.bulk_fraction
}
#[must_use]
pub const fn max_rounds(&self) -> u32 {
self.max_rounds
}
#[must_use]
pub const fn max_refine_passes(&self) -> u32 {
self.max_refine_passes
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct AdaptiveSolution {
pub mesh: SdfTetMesh,
pub field: FemSolution,
pub rounds: u32,
pub total_indicator_history: Vec<Fix128>,
}
pub fn solve_adaptive<F>(
mesh: &SdfTetMesh,
material: &ElasticMaterial,
boundary_for: F,
adaptive: &AdaptiveConfig,
) -> Result<AdaptiveSolution, FemError>
where
F: Fn(&SdfTetMesh) -> BoundaryConditions,
{
if mesh.vertices.is_empty() || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
let mut current = mesh.clone();
let mut history = Vec::with_capacity(adaptive.max_rounds as usize);
let mut rounds = 0_u32;
loop {
let boundary = boundary_for(¤t);
let field = solve(¤t, material, &boundary, &adaptive.linear)?;
rounds += 1;
let indicators = error_indicators_squared(¤t, material, &field)?;
let total = indicators.iter().fold(Fix128::ZERO, |acc, &v| acc + v);
history.push(total);
if total.is_zero() || rounds >= adaptive.max_rounds {
return Ok(AdaptiveSolution {
mesh: current,
field,
rounds,
total_indicator_history: history,
});
}
let marked = mark_bulk(&indicators, adaptive.bulk_fraction)?;
if !marked.iter().any(|&m| m) {
return Ok(AdaptiveSolution {
mesh: current,
field,
rounds,
total_indicator_history: history,
});
}
let mut next = current.clone();
next.try_refine_marked(&marked, adaptive.max_refine_passes)
.map_err(|_| {
FemError::InvalidConfig(
"max_refine_passes ran out while conformity still owed splits",
)
})?;
current = next;
}
}
pub(crate) fn adaptive_refinement_loop<S>(
mesh: &SdfTetMesh,
adaptive: &AdaptiveConfig,
mut step: S,
) -> Result<(SdfTetMesh, FemSolution, u32, Vec<Fix128>), FemError>
where
S: FnMut(&SdfTetMesh) -> Result<(FemSolution, Vec<Fix128>), FemError>,
{
if mesh.vertices.is_empty() || mesh.tets.is_empty() {
return Err(FemError::EmptyMesh);
}
let mut current = mesh.clone();
let mut history = Vec::with_capacity(adaptive.max_rounds as usize);
let mut rounds = 0_u32;
loop {
let (field, indicators) = step(¤t)?;
rounds += 1;
let total = indicators.iter().fold(Fix128::ZERO, |acc, &v| acc + v);
history.push(total);
if total.is_zero() || rounds >= adaptive.max_rounds {
return Ok((current, field, rounds, history));
}
let marked = mark_bulk(&indicators, adaptive.bulk_fraction)?;
if !marked.iter().any(|&m| m) {
return Ok((current, field, rounds, history));
}
let mut next = current.clone();
next.try_refine_marked(&marked, adaptive.max_refine_passes)
.map_err(|_| {
FemError::InvalidConfig(
"max_refine_passes ran out while conformity still owed splits",
)
})?;
current = next;
}
}
pub(crate) fn corner_indicators_squared(
corner_mesh: &SdfTetMesh,
material: &ElasticMaterial,
field: &FemSolution,
) -> Result<Vec<Fix128>, FemError> {
let mut view = field.clone();
view.displacements.truncate(corner_mesh.vertices.len());
error_indicators_squared(corner_mesh, material, &view)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hyperelastic::cauchy_stress;
fn single_tet_elements() -> Vec<Element> {
let mesh = SdfTetMesh {
vertices: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
],
tets: vec![crate::sdf_fem_mesh::Tetrahedron {
vertices: [0, 1, 2, 3],
}],
};
build_elements(&mesh).expect("a regular tetrahedron")
}
const UZ3: usize = 11;
#[test]
fn a_step_that_inverts_an_element_is_halved_until_it_is_regular() {
let elements = single_tet_elements();
let previous = vec![Fix128::ZERO; 12];
let mut next = previous.clone();
next[UZ3] = Fix128::from_int(-2) + Fix128::from_raw(0, 1 << 35);
step_stays_regular(&elements, &previous, &mut next);
assert_eq!(
next[UZ3],
Fix128::from_ratio(-1, 2) + Fix128::from_raw(0, 1 << 33)
);
assert!(all_elements_regular(&elements, &next));
}
#[test]
fn settle_step_takes_the_accepted_point_or_falls_back_to_regularity() {
let elements = single_tet_elements();
let previous = vec![Fix128::ZERO; 12];
let mut full = previous.clone();
full[UZ3] = Fix128::from_int(-2);
let mut accepted_point = previous.clone();
accepted_point[UZ3] = Fix128::from_ratio(1, 8);
let mut next = full.clone();
settle_step(&elements, &previous, &mut next, Some(accepted_point));
assert_eq!(next[UZ3], Fix128::from_ratio(1, 8));
let mut next = full;
settle_step(&elements, &previous, &mut next, None);
assert_eq!(next[UZ3], Fix128::from_ratio(-1, 2));
}
#[test]
fn a_regular_step_is_left_alone() {
let elements = single_tet_elements();
let previous = vec![Fix128::ZERO; 12];
let mut next = previous.clone();
next[UZ3] = Fix128::from_ratio(1, 4);
step_stays_regular(&elements, &previous, &mut next);
assert_eq!(next[UZ3], Fix128::from_ratio(1, 4));
}
#[test]
fn a_step_that_never_becomes_regular_ends_at_the_shortest_trial() {
let elements = single_tet_elements();
let previous = vec![Fix128::ZERO; 12];
let mut next = previous.clone();
next[UZ3] = Fix128::from_int(-(1 << 20));
step_stays_regular(&elements, &previous, &mut next);
assert_eq!(next[UZ3], Fix128::from_int(-16));
assert!(!all_elements_regular(&elements, &next));
}
fn from_rows(r: [[(i64, i64); 3]; 3]) -> Mat3Fix {
let e = |i: usize, j: usize| {
let (n, d) = r[i][j];
Fix128::from_ratio(n, d)
};
Mat3Fix::from_cols(
Vec3Fix::new(e(0, 0), e(1, 0), e(2, 0)),
Vec3Fix::new(e(0, 1), e(1, 1), e(2, 1)),
Vec3Fix::new(e(0, 2), e(1, 2), e(2, 2)),
)
}
fn quarter_turn_z() -> Mat3Fix {
from_rows([
[(0, 1), (-1, 1), (0, 1)],
[(1, 1), (0, 1), (0, 1)],
[(0, 1), (0, 1), (1, 1)],
])
}
fn gradients() -> [(&'static str, Mat3Fix); 5] {
[
(
"diag(2, 1/2, 1) with xy shear, det 1",
from_rows([
[(2, 1), (1, 2), (0, 1)],
[(0, 1), (1, 2), (0, 1)],
[(0, 1), (0, 1), (1, 1)],
]),
),
(
"the same with z doubled, det 2",
from_rows([
[(2, 1), (1, 2), (0, 1)],
[(0, 1), (1, 2), (0, 1)],
[(0, 1), (0, 1), (2, 1)],
]),
),
(
"diag(1, 1/2, 1) with xy shear, det 1/2",
from_rows([
[(1, 1), (1, 2), (0, 1)],
[(0, 1), (1, 2), (0, 1)],
[(0, 1), (0, 1), (1, 1)],
]),
),
(
"upper shear after stretch, det 1",
from_rows([
[(2, 1), (1, 1), (1, 2)],
[(0, 1), (1, 2), (1, 16)],
[(0, 1), (0, 1), (1, 1)],
]),
),
(
"upper shear after stretch times lower shear after stretch, det 1",
from_rows([
[(37, 8), (5, 8), (1, 2)],
[(17, 64), (17, 64), (1, 16)],
[(1, 4), (1, 4), (1, 1)],
]),
),
]
}
fn models() -> [(&'static str, HyperelasticModel); 4] {
[
(
"Neo-Hookean μ = 1000",
HyperelasticModel::NeoHookean {
mu_mpa: Fix128::from_int(1000),
},
),
(
"tpu_soft (Neo-Hookean μ = 3)",
HyperelasticModel::tpu_soft(),
),
(
"Mooney-Rivlin C₁ = 1/2, C₂ = 1/4 (the W₂ ≠ 0 branch)",
HyperelasticModel::MooneyRivlin {
c1_mpa: Fix128::from_ratio(1, 2),
c2_mpa: Fix128::from_ratio(1, 4),
},
),
(
"Yeoh C₁ = 1/2, C₂ = −1/16, C₃ = 1/64 (W₁ depends on I₁)",
HyperelasticModel::Yeoh {
c1_mpa: Fix128::from_ratio(1, 2),
c2_mpa: Fix128::from_ratio(-1, 16),
c3_mpa: Fix128::from_ratio(1, 64),
},
),
]
}
#[test]
fn the_first_piola_kirchhoff_stress_is_objective_under_a_superposed_rotation() {
let q = quarter_turn_z();
let mut separations = 0_u32;
for (model_name, model) in models() {
for bulk in [Fix128::ZERO, Fix128::from_int(1), Fix128::from_int(2048)] {
for (gradient_name, f) in gradients() {
let what =
|| format!("{model_name}, K = {}, F = {gradient_name}", bulk.to_f64());
let rotated = q.mul_mat(f);
let det = f.determinant();
assert_eq!(
det,
rotated.determinant(),
"det Q = 1, so det QF = det F: {}",
what()
);
let sigma = cauchy_stress(&model, bulk, f).expect("det F > 0");
let sigma_rotated = cauchy_stress(&model, bulk, rotated).expect("det QF > 0");
assert_eq!(
sigma_rotated,
q.mul_mat(sigma).mul_mat(q.transpose()),
"the Cauchy stress must be objective: {}",
what()
);
let (_, piola) = hyperelastic_stress(&model, bulk, f).expect("det F > 0");
let (_, piola_rotated) =
hyperelastic_stress(&model, bulk, rotated).expect("det QF > 0");
assert_eq!(
piola_rotated,
q.mul_mat(piola),
"P must rotate with one Q, which is what the F⁻ᵀ carries: {}",
what()
);
if sigma_rotated.scale(det) != q.mul_mat(sigma.scale(det)) {
separations += 1;
}
}
}
}
assert_eq!(
separations, 60,
"every case in the sweep must separate P = J σ F⁻ᵀ from P = J σ; \
a case that does not is one where this oracle says nothing"
);
}
#[test]
fn the_first_piola_kirchhoff_stress_is_what_the_closed_form_says() {
let f = from_rows([
[(2, 1), (1, 2), (0, 1)],
[(0, 1), (1, 2), (0, 1)],
[(0, 1), (0, 1), (1, 1)],
]);
let want_sigma = from_rows([
[(3250, 1), (250, 1), (0, 1)],
[(250, 1), (-750, 1), (0, 1)],
[(0, 1), (0, 1), (0, 1)],
]);
let want_piola = from_rows([
[(1500, 1), (500, 1), (0, 1)],
[(500, 1), (-1500, 1), (0, 1)],
[(0, 1), (0, 1), (0, 1)],
]);
let model = HyperelasticModel::NeoHookean {
mu_mpa: Fix128::from_int(1000),
};
assert_eq!(f.determinant(), Fix128::ONE, "the derivation assumes J = 1");
for bulk in [Fix128::ZERO, Fix128::from_int(1), Fix128::from_int(2048)] {
let (cauchy, piola) = hyperelastic_stress(&model, bulk, f).expect("det F = 1 > 0");
assert_eq!(
cauchy_stress(&model, bulk, f).expect("det F = 1 > 0"),
want_sigma,
"σ = μ(B − I) at J = 1, K = {}",
bulk.to_f64()
);
assert_eq!(cauchy.xx, Fix128::from_int(3250));
assert_eq!(cauchy.yy, Fix128::from_int(-750));
assert_eq!(cauchy.zz, Fix128::ZERO);
assert_eq!(cauchy.xy, Fix128::from_int(250));
assert_eq!(cauchy.yz, Fix128::ZERO);
assert_eq!(cauchy.zx, Fix128::ZERO);
assert_eq!(piola, want_piola, "P = J σ F⁻ᵀ at K = {}", bulk.to_f64());
assert_eq!(
piola.mul_mat(f.transpose()),
want_sigma,
"P Fᵀ = J σ, symmetric, at K = {}",
bulk.to_f64()
);
assert_ne!(
piola, want_sigma,
"P must differ from J σ on this F, or the F⁻ᵀ is unobservable here"
);
}
let (full_name, full) = gradients()[4];
let (_, full_piola) = hyperelastic_stress(&model, Fix128::ZERO, full).expect("det F > 0");
assert_ne!(
full_piola,
full_piola.transpose(),
"P must not be symmetric on {full_name}, or nothing in this crate needs \
a force integral for a non-symmetric stress"
);
}
}
#[cfg(test)]
mod fallible_driver_tests {
use super::*;
use crate::coupled_iteration::{run_sub_iteration, SubIterationConfig};
fn geometric(index: u32) -> Fix128 {
Fix128::from_raw(0, 1u64 << (63 - index.min(60)))
}
#[test]
fn a_failing_sweep_is_the_error_not_a_converged_report() {
for fail_at in [0u32, 1, 2, 3] {
let outcome = run_sub_iteration_fallible(SubIterationConfig::default(), |index| {
if index == fail_at {
Err(FemError::EmptyMesh)
} else {
Ok(geometric(index))
}
});
assert!(
matches!(outcome, Err(FemError::EmptyMesh)),
"failure at sweep {fail_at} must surface, got {outcome:?}"
);
}
}
#[test]
fn a_succeeding_sweep_reports_exactly_what_run_sub_iteration_reports() {
let config = SubIterationConfig::default();
let direct = run_sub_iteration(config, geometric).expect("converges");
let wrapped =
run_sub_iteration_fallible(config, |index| Ok(geometric(index))).expect("converges");
assert_eq!(wrapped, direct);
assert!(wrapped.sweeps > 1, "not vacuous: more than one sweep ran");
}
#[test]
fn a_monitor_refusal_is_mapped_to_the_coupled_variant() {
let outcome =
run_sub_iteration_fallible(SubIterationConfig::default(), |_| Ok(Fix128::ONE));
assert!(
matches!(outcome, Err(FemError::CoupledSubIterationFailed(_))),
"{outcome:?}"
);
}
}