use crate::math::{Fix128, Mat3Fix, Vec3Fix};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Stretch {
pub l1: Fix128,
pub l2: Fix128,
pub l3: Fix128,
}
impl Stretch {
pub const UNITY: Self = Self {
l1: Fix128::ONE,
l2: Fix128::ONE,
l3: Fix128::ONE,
};
#[must_use]
pub fn uniaxial(lambda: Fix128) -> Self {
if lambda <= Fix128::ZERO {
return Self::UNITY;
}
let lateral = Fix128::ONE / lambda.sqrt();
Self {
l1: lambda,
l2: lateral,
l3: lateral,
}
}
#[must_use]
pub fn equibiaxial(lambda: Fix128) -> Self {
if lambda <= Fix128::ZERO {
return Self::UNITY;
}
let l2 = lambda * lambda;
if l2.is_zero() {
return Self::UNITY;
}
Self {
l1: lambda,
l2: lambda,
l3: Fix128::ONE / l2,
}
}
#[must_use]
pub fn volume_ratio(&self) -> Fix128 {
self.l1 * self.l2 * self.l3
}
#[must_use]
pub fn i1(&self) -> Fix128 {
self.l1 * self.l1 + self.l2 * self.l2 + self.l3 * self.l3
}
#[must_use]
pub fn i2(&self) -> Fix128 {
let l1sq = self.l1 * self.l1;
let l2sq = self.l2 * self.l2;
let l3sq = self.l3 * self.l3;
if l1sq.is_zero() || l2sq.is_zero() || l3sq.is_zero() {
return Fix128::ZERO;
}
Fix128::ONE / l1sq + Fix128::ONE / l2sq + Fix128::ONE / l3sq
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HyperelasticModel {
NeoHookean {
mu_mpa: Fix128,
},
MooneyRivlin {
c1_mpa: Fix128,
c2_mpa: Fix128,
},
Yeoh {
c1_mpa: Fix128,
c2_mpa: Fix128,
c3_mpa: Fix128,
},
}
impl HyperelasticModel {
#[must_use]
pub const fn tpu_soft() -> Self {
Self::NeoHookean {
mu_mpa: Fix128::from_raw(3, 0),
}
}
#[must_use]
pub const fn silicone_soft() -> Self {
Self::MooneyRivlin {
c1_mpa: Fix128 {
hi: 0,
lo: 0x1999_9999_9999_999A, },
c2_mpa: Fix128 {
hi: 0,
lo: 0x0CCC_CCCC_CCCC_CCCD, },
}
}
#[must_use]
pub const fn natural_rubber() -> Self {
Self::Yeoh {
c1_mpa: Fix128 {
hi: 0,
lo: 0x8000_0000_0000_0000,
},
c2_mpa: Fix128 {
hi: -1,
lo: 0xFBA3_D70A_3D70_A3D8, },
c3_mpa: Fix128 {
hi: 0,
lo: 0x0028_F5C2_8F5C_28F6, },
}
}
}
#[must_use]
pub fn strain_energy_density(model: &HyperelasticModel, stretch: &Stretch) -> Fix128 {
let i1 = stretch.i1();
let three = Fix128::from_int(3);
match model {
HyperelasticModel::NeoHookean { mu_mpa } => {
*mu_mpa * (i1 - three) * Fix128::from_ratio(1, 2)
}
HyperelasticModel::MooneyRivlin { c1_mpa, c2_mpa } => {
let i2 = stretch.i2();
*c1_mpa * (i1 - three) + *c2_mpa * (i2 - three)
}
HyperelasticModel::Yeoh {
c1_mpa,
c2_mpa,
c3_mpa,
} => {
let d = i1 - three;
*c1_mpa * d + *c2_mpa * d * d + *c3_mpa * d * d * d
}
}
}
#[must_use]
pub fn uniaxial_cauchy_stress(model: &HyperelasticModel, lambda: Fix128) -> Fix128 {
if lambda <= Fix128::ZERO {
return Fix128::ZERO;
}
let lam2 = lambda * lambda;
let inv_lam = Fix128::ONE / lambda;
let base = lam2 - inv_lam;
match model {
HyperelasticModel::NeoHookean { mu_mpa } => *mu_mpa * base,
HyperelasticModel::MooneyRivlin { c1_mpa, c2_mpa } => {
let mix = *c1_mpa + *c2_mpa * inv_lam;
Fix128::from_int(2) * mix * base
}
HyperelasticModel::Yeoh {
c1_mpa,
c2_mpa,
c3_mpa,
} => {
let stretch = Stretch::uniaxial(lambda);
let i1 = stretch.i1();
let d = i1 - Fix128::from_int(3);
let dw =
*c1_mpa + Fix128::from_int(2) * *c2_mpa * d + Fix128::from_int(3) * *c3_mpa * d * d;
Fix128::from_int(2) * dw * base
}
}
}
#[must_use]
pub fn small_strain_shear_modulus(model: &HyperelasticModel) -> Fix128 {
match model {
HyperelasticModel::NeoHookean { mu_mpa } => *mu_mpa,
HyperelasticModel::MooneyRivlin { c1_mpa, c2_mpa } => (*c1_mpa + *c2_mpa).double(),
HyperelasticModel::Yeoh { c1_mpa, .. } => c1_mpa.double(),
}
}
fn energy_derivatives(model: &HyperelasticModel, i1: Fix128) -> (Fix128, Fix128) {
match model {
HyperelasticModel::NeoHookean { mu_mpa } => {
(*mu_mpa * Fix128::from_ratio(1, 2), Fix128::ZERO)
}
HyperelasticModel::MooneyRivlin { c1_mpa, c2_mpa } => (*c1_mpa, *c2_mpa),
HyperelasticModel::Yeoh {
c1_mpa,
c2_mpa,
c3_mpa,
} => {
let d = i1 - Fix128::from_int(3);
(
*c1_mpa + Fix128::from_int(2) * *c2_mpa * d + Fix128::from_int(3) * *c3_mpa * d * d,
Fix128::ZERO,
)
}
}
}
pub(crate) fn tangent_constants(
model: &HyperelasticModel,
i1: Fix128,
) -> (Fix128, Fix128, Fix128, Fix128) {
let (w1, w2) = energy_derivatives(model, i1);
let (w1_ref, w2_ref) = energy_derivatives(model, Fix128::from_int(3));
let p_ref = Fix128::from_int(2) * (w1_ref + w2_ref.double());
let w11 = match model {
HyperelasticModel::Yeoh { c2_mpa, c3_mpa, .. } => {
Fix128::from_int(2) * *c2_mpa
+ Fix128::from_int(6) * *c3_mpa * (i1 - Fix128::from_int(3))
}
HyperelasticModel::NeoHookean { .. } | HyperelasticModel::MooneyRivlin { .. } => {
Fix128::ZERO
}
};
(w1, w2, w11, p_ref)
}
#[must_use]
pub fn small_strain_moduli(model: &HyperelasticModel) -> (Fix128, Fix128) {
let (w1, w2, w11, _) = tangent_constants(model, Fix128::from_int(3));
let four = Fix128::from_int(4);
(four * (w2 + w11), (w1 + w2).double())
}
#[must_use]
pub fn volumetric_modulus(model: &HyperelasticModel, lambda: Fix128) -> Option<Fix128> {
let kappa = lambda - small_strain_moduli(model).0;
(kappa >= Fix128::ZERO).then_some(kappa)
}
#[must_use]
pub fn cauchy_stress(
model: &HyperelasticModel,
bulk_modulus_mpa: Fix128,
f: Mat3Fix,
) -> Option<Mat3Fix> {
let j = f.determinant();
if j <= Fix128::ZERO {
return None;
}
let b = f.mul_mat(f.transpose());
let i1 = b.col0.x + b.col1.y + b.col2.z;
let (w1, w2) = energy_derivatives(model, i1);
let (w1_ref, w2_ref) = energy_derivatives(model, Fix128::from_int(3));
let p_ref = Fix128::from_int(2) * (w1_ref + w2_ref.double());
let two_over_j = Fix128::from_int(2) / j;
let mut s = b.scale(two_over_j * (w1 + i1 * w2));
if !w2.is_zero() {
s = mat_sub(s, b.mul_mat(b).scale(two_over_j * w2));
}
let pressure = bulk_modulus_mpa * (j - Fix128::ONE) - p_ref / j;
Some(mat_add_diagonal(s, pressure))
}
fn mat_sub(a: Mat3Fix, b: Mat3Fix) -> Mat3Fix {
Mat3Fix::from_cols(
Vec3Fix::new(
a.col0.x - b.col0.x,
a.col0.y - b.col0.y,
a.col0.z - b.col0.z,
),
Vec3Fix::new(
a.col1.x - b.col1.x,
a.col1.y - b.col1.y,
a.col1.z - b.col1.z,
),
Vec3Fix::new(
a.col2.x - b.col2.x,
a.col2.y - b.col2.y,
a.col2.z - b.col2.z,
),
)
}
fn mat_add_diagonal(m: Mat3Fix, s: Fix128) -> Mat3Fix {
Mat3Fix::from_cols(
Vec3Fix::new(m.col0.x + s, m.col0.y, m.col0.z),
Vec3Fix::new(m.col1.x, m.col1.y + s, m.col1.z),
Vec3Fix::new(m.col2.x, m.col2.y, m.col2.z + s),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
fn approx_eq(a: Fix128, b: Fix128, tol: Fix128) -> bool {
let d = if a > b { a - b } else { b - a };
d <= tol
}
#[test]
fn stretch_unity_is_undeformed() {
assert_eq!(Stretch::UNITY.l1, Fix128::ONE);
assert_eq!(Stretch::UNITY.i1(), Fix128::from_int(3));
assert_eq!(Stretch::UNITY.volume_ratio(), Fix128::ONE);
}
#[test]
fn stretch_uniaxial_preserves_volume() {
let s = Stretch::uniaxial(Fix128::from_int(2));
let j = s.volume_ratio();
assert!(approx_eq(j, Fix128::ONE, Fix128::from_ratio(1, 1000)));
}
#[test]
fn stretch_equibiaxial_preserves_volume() {
let s = Stretch::equibiaxial(Fix128::from_ratio(15, 10));
let j = s.volume_ratio();
assert!(approx_eq(j, Fix128::ONE, Fix128::from_ratio(1, 1000)));
}
#[test]
fn stretch_i1_undeformed_is_three() {
assert_eq!(Stretch::UNITY.i1(), Fix128::from_int(3));
}
#[test]
fn stretch_i1_grows_with_extension() {
let s1 = Stretch::uniaxial(Fix128::from_int(1));
let s2 = Stretch::uniaxial(Fix128::from_int(3));
assert!(s2.i1() > s1.i1());
}
#[test]
fn strain_energy_undeformed_is_zero() {
for model in [
HyperelasticModel::tpu_soft(),
HyperelasticModel::silicone_soft(),
HyperelasticModel::natural_rubber(),
] {
let w = strain_energy_density(&model, &Stretch::UNITY);
assert!(
approx_eq(w, Fix128::ZERO, Fix128::from_ratio(1, 1000)),
"model {:?}: W = {}",
model,
w.to_f32()
);
}
}
#[test]
fn strain_energy_grows_with_stretch() {
let model = HyperelasticModel::tpu_soft();
let s2 = Stretch::uniaxial(Fix128::from_int(2));
let s3 = Stretch::uniaxial(Fix128::from_int(3));
let w2 = strain_energy_density(&model, &s2);
let w3 = strain_energy_density(&model, &s3);
assert!(w3 > w2);
}
#[test]
fn uniaxial_stress_zero_at_unity() {
for model in [
HyperelasticModel::tpu_soft(),
HyperelasticModel::silicone_soft(),
HyperelasticModel::natural_rubber(),
] {
let s = uniaxial_cauchy_stress(&model, Fix128::ONE);
assert!(
approx_eq(s, Fix128::ZERO, Fix128::from_ratio(1, 100)),
"{:?}: stress at λ=1 was {}",
model,
s.to_f32()
);
}
}
#[test]
fn uniaxial_stress_neo_hookean_analytical() {
let model = HyperelasticModel::tpu_soft();
let s = uniaxial_cauchy_stress(&model, Fix128::from_int(2));
let expected = Fix128::from_ratio(105, 10);
assert!(
approx_eq(s, expected, Fix128::from_ratio(1, 10)),
"got {}, expected 10.5",
s.to_f32()
);
}
#[test]
fn tension_positive_compression_negative() {
let model = HyperelasticModel::tpu_soft();
let s_ext = uniaxial_cauchy_stress(&model, Fix128::from_int(2));
let s_com = uniaxial_cauchy_stress(&model, Fix128::from_ratio(5, 10));
assert!(s_ext > Fix128::ZERO);
assert!(s_com < Fix128::ZERO);
}
#[test]
fn yeoh_softens_then_stiffens() {
let model = HyperelasticModel::natural_rubber();
let s_small = uniaxial_cauchy_stress(&model, Fix128::from_ratio(12, 10));
let s_large = uniaxial_cauchy_stress(&model, Fix128::from_int(3));
assert!(s_small > Fix128::ZERO);
assert!(s_large > s_small);
}
#[test]
fn small_strain_shear_matches_neo_hookean_mu() {
let model = HyperelasticModel::tpu_soft();
let mu0 = small_strain_shear_modulus(&model);
assert_eq!(mu0, Fix128::from_int(3));
}
#[test]
fn small_strain_shear_mooney_rivlin_2x_c1_plus_c2() {
let m = HyperelasticModel::MooneyRivlin {
c1_mpa: Fix128::from_int(2),
c2_mpa: Fix128::from_int(1),
};
let mu0 = small_strain_shear_modulus(&m);
assert_eq!(mu0, Fix128::from_int(6));
}
#[test]
fn small_strain_shear_yeoh_2x_c1() {
let m = HyperelasticModel::Yeoh {
c1_mpa: Fix128::from_int(4),
c2_mpa: Fix128::from_int(1),
c3_mpa: Fix128::from_int(1),
};
let mu0 = small_strain_shear_modulus(&m);
assert_eq!(mu0, Fix128::from_int(8));
}
#[test]
fn uniaxial_lambda_zero_returns_zero_stress() {
let m = HyperelasticModel::tpu_soft();
assert_eq!(uniaxial_cauchy_stress(&m, Fix128::ZERO), Fix128::ZERO);
}
#[test]
fn uniaxial_lambda_negative_returns_zero_stress() {
let m = HyperelasticModel::tpu_soft();
assert_eq!(
uniaxial_cauchy_stress(&m, Fix128::from_int(-1)),
Fix128::ZERO
);
}
#[test]
fn stretch_uniaxial_lambda_zero_is_unity() {
let s = Stretch::uniaxial(Fix128::ZERO);
assert_eq!(s, Stretch::UNITY);
}
#[test]
fn cauchy_stress_vanishes_in_the_reference_state() {
for model in [
HyperelasticModel::tpu_soft(),
HyperelasticModel::silicone_soft(),
HyperelasticModel::natural_rubber(),
] {
for bulk in [Fix128::ZERO, Fix128::from_int(1), Fix128::from_int(4000)] {
let s = cauchy_stress(&model, bulk, Mat3Fix::IDENTITY).expect("det I = 1 > 0");
assert_eq!(
s,
Mat3Fix::ZERO,
"{model:?} at K = {} carries stress when undeformed",
bulk.to_f32()
);
}
}
}
#[test]
fn cauchy_stress_neo_hookean_isochoric_closed_form() {
let f = Mat3Fix::diagonal(Fix128::from_int(2), Fix128::from_ratio(1, 2), Fix128::ONE);
let s = cauchy_stress(&HyperelasticModel::tpu_soft(), Fix128::from_int(4000), f)
.expect("det F = 1");
let want = [Fix128::from_int(9), Fix128::from_ratio(-9, 4), Fix128::ZERO];
let got = [s.col0.x, s.col1.y, s.col2.z];
for (g, w) in got.iter().zip(want.iter()) {
assert!(
approx_eq(*g, *w, Fix128::from_ratio(1, 1000)),
"σ = {:?}, expected diag(9, -2.25, 0)",
got.map(Fix128::to_f32)
);
}
assert_eq!(s.col1.x, Fix128::ZERO, "a diagonal F gives a diagonal σ");
assert_eq!(s.col2.y, Fix128::ZERO, "a diagonal F gives a diagonal σ");
}
#[test]
fn cauchy_stress_refuses_a_non_positive_determinant() {
let model = HyperelasticModel::tpu_soft();
let bulk = Fix128::from_int(4000);
let flat = Mat3Fix::diagonal(Fix128::ONE, Fix128::ONE, Fix128::ZERO);
let inverted = Mat3Fix::diagonal(Fix128::from_int(-1), Fix128::ONE, Fix128::ONE);
assert!(cauchy_stress(&model, bulk, flat).is_none());
assert!(cauchy_stress(&model, bulk, inverted).is_none());
}
fn fx(v: f64) -> Fix128 {
Fix128::from_f64(v)
}
fn laws() -> [(&'static str, HyperelasticModel); 3] {
[
(
"neo-hookean",
HyperelasticModel::NeoHookean { mu_mpa: fx(2.0) },
),
(
"mooney-rivlin",
HyperelasticModel::MooneyRivlin {
c1_mpa: fx(1.5),
c2_mpa: fx(0.25),
},
),
(
"yeoh",
HyperelasticModel::Yeoh {
c1_mpa: fx(1.0),
c2_mpa: fx(0.25),
c3_mpa: fx(0.5),
},
),
]
}
fn piola(model: &HyperelasticModel, kappa: Fix128, f: Mat3Fix) -> [[f64; 3]; 3] {
let sigma = cauchy_stress(model, kappa, f).expect("det F > 0");
let p = sigma
.mul_mat(f.inverse().expect("regular").transpose())
.scale(f.determinant());
[
[p.col0.x, p.col1.x, p.col2.x].map(Fix128::to_f64),
[p.col0.y, p.col1.y, p.col2.y].map(Fix128::to_f64),
[p.col0.z, p.col1.z, p.col2.z].map(Fix128::to_f64),
]
}
#[test]
fn small_strain_moduli_read_the_offset_per_model() {
let got: Vec<_> = laws().iter().map(|(_, m)| small_strain_moduli(m)).collect();
assert_eq!(got[0], (Fix128::ZERO, fx(2.0)), "neo-hookean");
assert_eq!(
got[1],
(fx(1.0), fx(3.5)),
"mooney-rivlin: 4*C01, 2(C10+C01)"
);
assert_eq!(got[2], (fx(2.0), fx(2.0)), "yeoh: 8*C2, 2*C1");
}
#[test]
fn volumetric_modulus_is_lambda_minus_offset_and_refuses_a_negative_result() {
let [(_, nh), (_, mr), (_, yeoh)] = laws();
assert_eq!(volumetric_modulus(&nh, fx(10.0)), Some(fx(10.0)));
assert_eq!(volumetric_modulus(&mr, fx(10.0)), Some(fx(9.0)));
assert_eq!(volumetric_modulus(&yeoh, fx(10.0)), Some(fx(8.0)));
assert_eq!(volumetric_modulus(&mr, fx(1.0)), Some(Fix128::ZERO));
assert_eq!(volumetric_modulus(&mr, fx(0.75)), None);
assert_eq!(volumetric_modulus(&yeoh, fx(1.5)), None);
}
#[test]
fn uniaxial_strain_slope_is_the_linear_solid_for_every_model() {
let lambda = 8.0;
for (name, model) in laws() {
let (_, mu_eff) = small_strain_moduli(&model);
let kappa = volumetric_modulus(&model, fx(lambda)).expect("kappa >= 0");
let gaps = |e: f64| {
let f = Mat3Fix::diagonal(fx(1.0 + e), Fix128::ONE, Fix128::ONE);
let p = piola(&model, kappa, f);
(
(p[0][0] / e - (lambda + 2.0 * mu_eff.to_f64())).abs(),
(p[1][1] / e - lambda).abs(),
)
};
let (a1, a2) = gaps(1.0 / 64.0);
let (b1, b2) = gaps(1.0 / 128.0);
assert!(
a1 < 0.05 * lambda && a2 < 0.05 * lambda,
"{name}: gap at e=2^-6 is ({a1}, {a2})"
);
assert!(b1 < 0.6 * a1, "{name}: P11 gap {a1} -> {b1} does not halve");
assert!(b2 < 0.6 * a2, "{name}: P22 gap {a2} -> {b2} does not halve");
}
}
#[test]
fn dilation_slope_is_two_mu_plus_three_lambda_for_every_model() {
let lambda = 8.0;
for (name, model) in laws() {
let (_, mu_eff) = small_strain_moduli(&model);
let kappa = volumetric_modulus(&model, fx(lambda)).expect("kappa >= 0");
let want = 2.0 * mu_eff.to_f64() + 3.0 * lambda;
let gap = |e: f64| {
let s = fx(1.0 + e);
let p = piola(&model, kappa, Mat3Fix::diagonal(s, s, s));
(p[0][0] / e - want).abs()
};
let (a, b) = (gap(1.0 / 64.0), gap(1.0 / 128.0));
assert!(a < 0.1 * want, "{name}: dilation slope gap {a}");
assert!(
b < 0.6 * a,
"{name}: dilation gap {a} -> {b} does not halve"
);
}
}
#[test]
fn uniform_compression_has_a_barrier() {
let model = HyperelasticModel::NeoHookean { mu_mpa: fx(2.0) };
let kappa = fx(40.0);
let at = |s: f64| piola(&model, kappa, Mat3Fix::diagonal(fx(s), fx(s), fx(s)))[0][0];
let (p8, p64) = (at(1.0 / 8.0), at(1.0 / 64.0));
assert!(p64 < p8 && p8 < 0.0, "monotone: {p8} {p64}");
assert!(p64 < -100.0, "no barrier: P(1/64) = {p64}");
}
#[test]
fn i2_incompressible_uniaxial() {
let s = Stretch::uniaxial(Fix128::from_int(2));
let expected = Fix128::from_ratio(425, 100);
assert!(approx_eq(s.i2(), expected, Fix128::from_ratio(1, 100)));
}
}