Skip to main content

solid_mechanics/
constitutive.rs

1//! Constitutive relations — Hooke's law and elasticity tensors.
2
3use nalgebra::{Matrix3, Matrix6, Vector6};
4use serde::{Deserialize, Serialize};
5
6use crate::tensor::{StressTensor, StrainTensor};
7
8/// Isotropic linear elastic material using Hooke's law.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct HookeIsotropic {
11    /// Young's modulus (Pa)
12    pub youngs_modulus: f64,
13    /// Poisson's ratio
14    pub poissons_ratio: f64,
15}
16
17impl HookeIsotropic {
18    /// Create a new isotropic elastic material.
19    pub fn new(e: f64, nu: f64) -> Self {
20        Self { youngs_modulus: e, poissons_ratio: nu }
21    }
22
23    /// Shear modulus: G = E / (2(1 + ν))
24    pub fn shear_modulus(&self) -> f64 {
25        self.youngs_modulus / (2.0 * (1.0 + self.poissons_ratio))
26    }
27
28    /// Bulk modulus: K = E / (3(1 - 2ν))
29    pub fn bulk_modulus(&self) -> f64 {
30        self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poissons_ratio))
31    }
32
33    /// Lamé's first parameter: λ = νE / ((1+ν)(1-2ν))
34    pub fn lame_lambda(&self) -> f64 {
35        let nu = self.poissons_ratio;
36        nu * self.youngs_modulus / ((1.0 + nu) * (1.0 - 2.0 * nu))
37    }
38
39    /// Lamé's second parameter (shear modulus): μ = G
40    pub fn lame_mu(&self) -> f64 {
41        self.shear_modulus()
42    }
43
44    /// Compute stress from strain using Hooke's law (3D).
45    /// σ_ij = λ ε_kk δ_ij + 2μ ε_ij
46    pub fn stress_from_strain(&self, strain: &StrainTensor) -> StressTensor {
47        let lam = self.lame_lambda();
48        let mu = self.lame_mu();
49        let tr = strain.matrix.trace();
50        let sigma = lam * tr * Matrix3::identity() + 2.0 * mu * strain.matrix;
51        StressTensor { matrix: sigma }
52    }
53
54    /// Compute strain from stress using compliance (3D).
55    pub fn strain_from_stress(&self, stress: &StressTensor) -> StrainTensor {
56        let e = self.youngs_modulus;
57        let nu = self.poissons_ratio;
58        let s = &stress.matrix;
59        let tr = s.trace();
60        let eps = ((1.0 + nu) / e) * *s - (nu / e) * tr * Matrix3::identity();
61        StrainTensor { matrix: eps }
62    }
63
64    /// 6×6 stiffness matrix (Voigt notation): [σ_xx, σ_yy, σ_zz, τ_yz, τ_xz, τ_xy]
65    pub fn stiffness_matrix_voigt(&self) -> Matrix6<f64> {
66        let nu = self.poissons_ratio;
67        let e = self.youngs_modulus;
68        let factor = e / ((1.0 + nu) * (1.0 - 2.0 * nu));
69        let lam_nu = 1.0 - nu;
70        let c12 = factor * nu;
71        let c11 = factor * lam_nu;
72        let c44 = e / (2.0 * (1.0 + nu));
73        Matrix6::from_row_slice(&[
74            c11, c12, c12, 0.0, 0.0, 0.0,
75            c12, c11, c12, 0.0, 0.0, 0.0,
76            c12, c12, c11, 0.0, 0.0, 0.0,
77            0.0, 0.0, 0.0, c44, 0.0, 0.0,
78            0.0, 0.0, 0.0, 0.0, c44, 0.0,
79            0.0, 0.0, 0.0, 0.0, 0.0, c44,
80        ])
81    }
82
83    /// 6×6 compliance matrix (Voigt notation).
84    pub fn compliance_matrix_voigt(&self) -> Matrix6<f64> {
85        let e = self.youngs_modulus;
86        let nu = self.poissons_ratio;
87        let s11 = 1.0 / e;
88        let s12 = -nu / e;
89        let s44 = 2.0 * (1.0 + nu) / e;
90        Matrix6::from_row_slice(&[
91            s11, s12, s12, 0.0, 0.0, 0.0,
92            s12, s11, s12, 0.0, 0.0, 0.0,
93            s12, s12, s11, 0.0, 0.0, 0.0,
94            0.0, 0.0, 0.0, s44, 0.0, 0.0,
95            0.0, 0.0, 0.0, 0.0, s44, 0.0,
96            0.0, 0.0, 0.0, 0.0, 0.0, s44,
97        ])
98    }
99
100    /// Axial stiffness EA.
101    pub fn axial_stiffness(&self, area: f64) -> f64 {
102        self.youngs_modulus * area
103    }
104
105    /// Flexural stiffness EI.
106    pub fn flexural_stiffness(&self, moment_of_inertia: f64) -> f64 {
107        self.youngs_modulus * moment_of_inertia
108    }
109}
110
111/// General anisotropic elasticity tensor (6×6 Voigt).
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ElasticityTensor {
114    /// Full 6×6 stiffness matrix in Voigt notation
115    pub c: Matrix6<f64>,
116}
117
118impl ElasticityTensor {
119    /// Create from a 6×6 Voigt stiffness matrix.
120    pub fn new(c: Matrix6<f64>) -> Self {
121        Self { c }
122    }
123
124    /// Create an orthotropic material from 9 independent constants.
125    pub fn orthotropic(
126        e1: f64, e2: f64, e3: f64,
127        nu12: f64, nu13: f64, nu23: f64,
128        g12: f64, g13: f64, g23: f64,
129    ) -> Self {
130        // Build compliance first, then invert
131        let nu21 = nu12 * e2 / e1;
132        let nu31 = nu13 * e3 / e1;
133        let nu32 = nu23 * e3 / e2;
134        let delta = 1.0 - nu12 * nu21 - nu23 * nu32 - nu31 * nu13 - 2.0 * nu21 * nu32 * nu13;
135
136        let _c11 = (1.0 - nu23 * nu32) / (e2 * e3 * delta);
137        let _c22 = (1.0 - nu13 * nu31) / (e1 * e3 * delta);
138        let _c33 = (1.0 - nu12 * nu21) / (e1 * e2 * delta);
139        let _c12 = (nu21 + nu31 * nu23) / (e1 * e3 * delta);
140        let _c13 = (nu31 + nu21 * nu32) / (e1 * e2 * delta);
141        let _c23 = (nu32 + nu12 * nu31) / (e2 * e1 * delta);
142
143        // Simplified: build stiffness from compliance inversion
144        let s = Matrix6::from_row_slice(&[
145            1.0/e1, -nu12/e1, -nu13/e1, 0.0, 0.0, 0.0,
146            -nu21/e2, 1.0/e2, -nu23/e2, 0.0, 0.0, 0.0,
147            -nu31/e3, -nu32/e3, 1.0/e3, 0.0, 0.0, 0.0,
148            0.0, 0.0, 0.0, 1.0/g12, 0.0, 0.0,
149            0.0, 0.0, 0.0, 0.0, 1.0/g13, 0.0,
150            0.0, 0.0, 0.0, 0.0, 0.0, 1.0/g23,
151        ]);
152
153        Self { c: s.try_inverse().unwrap_or(Matrix6::zeros()) }
154    }
155
156    /// Compute stress from strain (Voigt vectors).
157    pub fn stress_from_strain(&self, strain_voigt: &Vector6<f64>) -> Vector6<f64> {
158        self.c * strain_voigt
159    }
160
161    /// Compliance matrix (inverse of stiffness).
162    pub fn compliance(&self) -> Option<Matrix6<f64>> {
163        self.c.try_inverse()
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use approx::assert_relative_eq;
171
172    #[test]
173    fn test_hooke_shear_modulus() {
174        let h = HookeIsotropic::new(200e9, 0.3);
175        let g = 200e9 / (2.0 * 1.3);
176        assert_relative_eq!(h.shear_modulus(), g);
177    }
178
179    #[test]
180    fn test_hooke_bulk_modulus() {
181        let h = HookeIsotropic::new(200e9, 0.3);
182        let k = 200e9 / (3.0 * 0.4);
183        assert_relative_eq!(h.bulk_modulus(), k);
184    }
185
186    #[test]
187    fn test_hooke_roundtrip() {
188        let h = HookeIsotropic::new(200e9, 0.3);
189        let strain = StrainTensor::from_components(1e-3, 0.5e-3, -0.5e-3, 0.2e-3, 0.0, 0.0);
190        let stress = h.stress_from_strain(&strain);
191        let strain2 = h.strain_from_stress(&stress);
192        assert_relative_eq!(strain.matrix[(0, 0)], strain2.matrix[(0, 0)], epsilon = 1e-6);
193        assert_relative_eq!(strain.matrix[(1, 1)], strain2.matrix[(1, 1)], epsilon = 1e-6);
194    }
195
196    #[test]
197    fn test_stiffness_compliance_inverse() {
198        let h = HookeIsotropic::new(200e9, 0.3);
199        let c = h.stiffness_matrix_voigt();
200        let s = h.compliance_matrix_voigt();
201        let product = c * s;
202        let identity = Matrix6::identity();
203        for i in 0..6 {
204            for j in 0..6 {
205                assert_relative_eq!(product[(i, j)], identity[(i, j)], epsilon = 1e-3);
206            }
207        }
208    }
209
210    #[test]
211    fn test_orthotropic_creation() {
212        let ortho = ElasticityTensor::orthotropic(
213            140e9, 10e9, 10e9,  // E1, E2, E3
214            0.3, 0.3, 0.4,      // nu12, nu13, nu23
215            5e9, 5e9, 3e9,      // G12, G13, G23
216        );
217        // Verify it's not zero (i.e., inversion succeeded)
218        assert!(ortho.c.norm() > 0.0);
219    }
220}