Skip to main content

oxiphysics_materials/
elastic.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Elastic material models (linear, orthotropic, transversely isotropic, and hyperelastic).
5
6#![allow(dead_code)]
7
8// ---------------------------------------------------------------------------
9// LinearElastic (isotropic)
10// ---------------------------------------------------------------------------
11
12/// Linear elastic (isotropic) material defined by Young's modulus and Poisson's ratio.
13#[derive(Debug, Clone, Copy)]
14pub struct LinearElastic {
15    /// Young's modulus (Pa)
16    pub young_modulus: f64,
17    /// Poisson's ratio (dimensionless, typically 0..0.5)
18    pub poisson_ratio: f64,
19}
20
21impl LinearElastic {
22    /// Create a new linear elastic material.
23    pub fn new(young_modulus: f64, poisson_ratio: f64) -> Self {
24        Self {
25            young_modulus,
26            poisson_ratio,
27        }
28    }
29
30    /// Bulk modulus K = E / (3 * (1 - 2*nu)).
31    pub fn bulk_modulus(&self) -> f64 {
32        let e = self.young_modulus;
33        let nu = self.poisson_ratio;
34        e / (3.0 * (1.0 - 2.0 * nu))
35    }
36
37    /// Shear modulus G = E / (2 * (1 + nu)).
38    pub fn shear_modulus(&self) -> f64 {
39        let e = self.young_modulus;
40        let nu = self.poisson_ratio;
41        e / (2.0 * (1.0 + nu))
42    }
43
44    /// P-wave (constrained) modulus M = E*(1-nu) / ((1+nu)*(1-2*nu)).
45    pub fn p_wave_modulus(&self) -> f64 {
46        let e = self.young_modulus;
47        let nu = self.poisson_ratio;
48        e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu))
49    }
50
51    /// First Lame parameter lambda = E * nu / ((1 + nu) * (1 - 2*nu)).
52    pub fn lame_lambda(&self) -> f64 {
53        let e = self.young_modulus;
54        let nu = self.poisson_ratio;
55        e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
56    }
57
58    /// Second Lame parameter mu (same as shear modulus).
59    pub fn lame_mu(&self) -> f64 {
60        self.shear_modulus()
61    }
62
63    /// 6x6 stress-strain (stiffness) matrix in Voigt notation for 3-D isotropic elasticity.
64    ///
65    /// Order: `[sigma_xx, sigma_yy, sigma_zz, sigma_yz, sigma_xz, sigma_xy]`.
66    pub fn stress_strain_matrix_3d(&self) -> [[f64; 6]; 6] {
67        let e = self.young_modulus;
68        let nu = self.poisson_ratio;
69        let factor = e / ((1.0 + nu) * (1.0 - 2.0 * nu));
70
71        let c11 = factor * (1.0 - nu);
72        let c12 = factor * nu;
73        let c44 = factor * (1.0 - 2.0 * nu) / 2.0; // = G
74
75        let mut c = [[0.0_f64; 6]; 6];
76        c[0][0] = c11;
77        c[1][1] = c11;
78        c[2][2] = c11;
79        c[0][1] = c12;
80        c[0][2] = c12;
81        c[1][0] = c12;
82        c[1][2] = c12;
83        c[2][0] = c12;
84        c[2][1] = c12;
85        c[3][3] = c44;
86        c[4][4] = c44;
87        c[5][5] = c44;
88
89        c
90    }
91
92    /// 6x6 compliance matrix S = C⁻¹ in Voigt notation.
93    ///
94    /// Voigt order: `[eps_xx, eps_yy, eps_zz, eps_yz, eps_xz, eps_xy]`.
95    pub fn compliance_matrix_voigt(&self) -> [f64; 36] {
96        let e = self.young_modulus;
97        let nu = self.poisson_ratio;
98        let g = self.shear_modulus();
99
100        let mut s = [0.0_f64; 36];
101
102        // Normal (diagonal)
103        s[0] = 1.0 / e;
104        s[6 + 1] = 1.0 / e;
105        s[2 * 6 + 2] = 1.0 / e;
106
107        // Normal (off-diagonal Poisson coupling)
108        s[1] = -nu / e;
109        s[2] = -nu / e;
110        s[6] = -nu / e;
111        s[6 + 2] = -nu / e;
112        s[2 * 6] = -nu / e;
113        s[2 * 6 + 1] = -nu / e;
114
115        // Shear (factor 2 in engineering shear convention)
116        s[3 * 6 + 3] = 1.0 / g;
117        s[4 * 6 + 4] = 1.0 / g;
118        s[5 * 6 + 5] = 1.0 / g;
119
120        s
121    }
122
123    /// Compute engineering strains from a Voigt stress vector using the compliance matrix.
124    ///
125    /// `stress_voigt` order: `[sigma_xx, sigma_yy, sigma_zz, sigma_yz, sigma_xz, sigma_xy]`.
126    pub fn engineering_strains(&self, stress_voigt: [f64; 6]) -> [f64; 6] {
127        let s = self.compliance_matrix_voigt();
128        let mut strain = [0.0_f64; 6];
129        for i in 0..6 {
130            for j in 0..6 {
131                strain[i] += s[i * 6 + j] * stress_voigt[j];
132            }
133        }
134        strain
135    }
136}
137
138// ---------------------------------------------------------------------------
139// IsotropicElastic (alias with E/nu naming)
140// ---------------------------------------------------------------------------
141
142/// Isotropic elastic material — alias for `LinearElastic` using the E/nu naming
143/// convention expected by the task specification.
144#[derive(Debug, Clone, Copy)]
145pub struct IsotropicElastic {
146    /// Young's modulus (Pa).
147    pub e: f64,
148    /// Poisson's ratio.
149    pub nu: f64,
150}
151
152impl IsotropicElastic {
153    /// Create a new isotropic elastic material.
154    pub fn new(e: f64, nu: f64) -> Self {
155        Self { e, nu }
156    }
157
158    /// Shear modulus G = E / (2(1+ν)).
159    pub fn shear_modulus(&self) -> f64 {
160        self.e / (2.0 * (1.0 + self.nu))
161    }
162
163    /// Bulk modulus K = E / (3(1-2ν)).
164    pub fn bulk_modulus(&self) -> f64 {
165        self.e / (3.0 * (1.0 - 2.0 * self.nu))
166    }
167
168    /// P-wave modulus M = E(1-ν) / ((1+ν)(1-2ν)).
169    pub fn p_wave_modulus(&self) -> f64 {
170        self.e * (1.0 - self.nu) / ((1.0 + self.nu) * (1.0 - 2.0 * self.nu))
171    }
172
173    /// Compliance matrix S = C⁻¹ as a flat `[f64; 36]` row-major array.
174    pub fn compliance_matrix_voigt(&self) -> [f64; 36] {
175        LinearElastic::new(self.e, self.nu).compliance_matrix_voigt()
176    }
177
178    /// Engineering strains from Voigt stress vector.
179    pub fn engineering_strains(&self, stress_voigt: [f64; 6]) -> [f64; 6] {
180        LinearElastic::new(self.e, self.nu).engineering_strains(stress_voigt)
181    }
182}
183
184// ---------------------------------------------------------------------------
185// OrthotropicElastic
186// ---------------------------------------------------------------------------
187
188/// Orthotropic elastic material with three orthogonal planes of symmetry.
189///
190/// Convention: directions 1, 2, 3 are the principal material axes.
191#[derive(Debug, Clone, Copy)]
192#[allow(non_snake_case)]
193pub struct OrthotropicElastic {
194    /// Young's modulus in direction 1 (Pa).
195    pub E1: f64,
196    /// Young's modulus in direction 2 (Pa).
197    pub E2: f64,
198    /// Young's modulus in direction 3 (Pa).
199    pub E3: f64,
200    /// Shear modulus in the 1-2 plane (Pa).
201    pub G12: f64,
202    /// Shear modulus in the 2-3 plane (Pa).
203    pub G23: f64,
204    /// Shear modulus in the 1-3 plane (Pa).
205    pub G13: f64,
206    /// Poisson's ratio ν₁₂ (strain in 2 due to stress in 1).
207    pub nu12: f64,
208    /// Poisson's ratio ν₂₃.
209    pub nu23: f64,
210    /// Poisson's ratio ν₁₃.
211    pub nu13: f64,
212}
213
214impl OrthotropicElastic {
215    /// Compliance matrix in Voigt notation (flat row-major `[f64; 36]`).
216    ///
217    /// Voigt order: `[sigma_11, sigma_22, sigma_33, sigma_23, sigma_13, sigma_12]`.
218    #[allow(non_snake_case)]
219    pub fn compliance_voigt(&self) -> [f64; 36] {
220        let (E1, E2, E3) = (self.E1, self.E2, self.E3);
221        let (G12, G23, G13) = (self.G12, self.G23, self.G13);
222        let (nu12, nu23, nu13) = (self.nu12, self.nu23, self.nu13);
223
224        // Reciprocal relations: nu21/E2 = nu12/E1
225        let nu21 = nu12 * E2 / E1;
226        let nu31 = nu13 * E3 / E1;
227        let nu32 = nu23 * E3 / E2;
228
229        let mut s = [0.0_f64; 36];
230        s[0] = 1.0 / E1;
231        s[6 + 1] = 1.0 / E2;
232        s[2 * 6 + 2] = 1.0 / E3;
233        s[3 * 6 + 3] = 1.0 / G23;
234        s[4 * 6 + 4] = 1.0 / G13;
235        s[5 * 6 + 5] = 1.0 / G12;
236
237        s[1] = -nu21 / E2;
238        s[2] = -nu31 / E3;
239        s[6] = -nu12 / E1;
240        s[6 + 2] = -nu32 / E3;
241        s[2 * 6] = -nu13 / E1;
242        s[2 * 6 + 1] = -nu23 / E2;
243
244        s
245    }
246
247    /// Stiffness matrix C = S⁻¹ in Voigt notation (flat row-major `[f64; 36]`).
248    ///
249    /// Computed by inverting the 6×6 compliance matrix.
250    pub fn stiffness_voigt(&self) -> [f64; 36] {
251        let s = self.compliance_voigt();
252        invert_voigt_6x6(s)
253    }
254}
255
256// ---------------------------------------------------------------------------
257// TransverselyIsotropicElastic
258// ---------------------------------------------------------------------------
259
260/// Transversely isotropic elastic material.
261///
262/// The material is isotropic in the p (in-plane) directions and has distinct
263/// properties in the t (transverse / out-of-plane) direction.
264#[derive(Debug, Clone, Copy)]
265pub struct TransverselyIsotropicElastic {
266    /// In-plane Young's modulus (Pa).
267    pub ep: f64,
268    /// Transverse Young's modulus (Pa).
269    pub et: f64,
270    /// In-plane/transverse shear modulus (Pa).
271    pub gpt: f64,
272    /// In-plane Poisson's ratio.
273    pub nup: f64,
274    /// Transverse Poisson's ratio.
275    pub nut: f64,
276}
277
278impl TransverselyIsotropicElastic {
279    /// Create a new transversely isotropic elastic material.
280    pub fn new(ep: f64, et: f64, gpt: f64, nup: f64, nut: f64) -> Self {
281        Self {
282            ep,
283            et,
284            gpt,
285            nup,
286            nut,
287        }
288    }
289
290    /// Compliance matrix in Voigt notation (flat row-major `[f64; 36]`).
291    pub fn compliance_voigt(&self) -> [f64; 36] {
292        let ep = self.ep;
293        let et = self.et;
294        let gpt = self.gpt;
295        let nup = self.nup;
296        let nut = self.nut;
297
298        // In-plane shear modulus from isotropy relation
299        let gp = ep / (2.0 * (1.0 + nup));
300
301        let nutp = nut * ep / et; // reciprocal: ν_tp/E_t = ν_pt/E_p
302
303        let mut s = [0.0_f64; 36];
304
305        // Normal diagonal
306        s[0] = 1.0 / ep; // ε_11
307        s[6 + 1] = 1.0 / ep; // ε_22
308        s[2 * 6 + 2] = 1.0 / et; // ε_33 (transverse)
309
310        // Normal off-diagonal
311        s[1] = -nup / ep;
312        s[6] = -nup / ep;
313        s[2] = -nutp / et;
314        s[2 * 6] = -nut / ep;
315        s[6 + 2] = -nutp / et;
316        s[2 * 6 + 1] = -nut / ep;
317
318        // Shear
319        s[3 * 6 + 3] = 1.0 / gpt; // gamma_23
320        s[4 * 6 + 4] = 1.0 / gpt; // gamma_13
321        s[5 * 6 + 5] = 1.0 / gp; // gamma_12
322
323        s
324    }
325
326    /// Stiffness matrix in Voigt notation (flat row-major `[f64; 36]`).
327    pub fn stiffness_voigt(&self) -> [f64; 36] {
328        invert_voigt_6x6(self.compliance_voigt())
329    }
330}
331
332// ---------------------------------------------------------------------------
333// FailureCriteria trait
334// ---------------------------------------------------------------------------
335
336/// Trait for material failure criteria.
337pub trait FailureCriteria {
338    /// Return true if the material has failed under the given Voigt stress state.
339    fn is_failed(&self, stress: &[f64; 6]) -> bool;
340}
341
342// ---------------------------------------------------------------------------
343// VonMisesFailure
344// ---------------------------------------------------------------------------
345
346/// Von Mises yield criterion for isotropic ductile materials.
347///
348/// The material fails when the von Mises stress σ_VM ≥ σ_y.
349#[derive(Debug, Clone, Copy)]
350pub struct VonMisesFailure {
351    /// Yield stress σ_y (Pa).
352    pub yield_stress: f64,
353}
354
355impl VonMisesFailure {
356    /// Create a new von Mises failure criterion.
357    pub fn new(yield_stress: f64) -> Self {
358        Self { yield_stress }
359    }
360
361    /// Compute the von Mises effective stress for a Voigt stress state.
362    ///
363    /// `stress` order: `[sigma_11, sigma_22, sigma_33, sigma_23, sigma_13, sigma_12]`.
364    pub fn von_mises_stress(stress: &[f64; 6]) -> f64 {
365        let s11 = stress[0];
366        let s22 = stress[1];
367        let s33 = stress[2];
368        let s23 = stress[3];
369        let s13 = stress[4];
370        let s12 = stress[5];
371
372        let vm_sq = 0.5
373            * ((s11 - s22).powi(2)
374                + (s22 - s33).powi(2)
375                + (s33 - s11).powi(2)
376                + 6.0 * (s23.powi(2) + s13.powi(2) + s12.powi(2)));
377
378        vm_sq.sqrt()
379    }
380}
381
382impl FailureCriteria for VonMisesFailure {
383    fn is_failed(&self, stress: &[f64; 6]) -> bool {
384        Self::von_mises_stress(stress) >= self.yield_stress
385    }
386}
387
388// ---------------------------------------------------------------------------
389// TsaiWuFailure
390// ---------------------------------------------------------------------------
391
392/// Tsai-Wu failure criterion for anisotropic (composite) materials.
393///
394/// The failure index F is:
395/// ```text
396/// F = F1*σ1 + F2*σ2 + F11*σ1² + F22*σ2² + F66*τ12² + 2*F12*σ1*σ2
397/// ```
398/// The material fails when F ≥ 1.
399#[derive(Debug, Clone, Copy)]
400#[allow(non_snake_case)]
401pub struct TsaiWuFailure {
402    /// Linear coefficient in direction 1.
403    pub F1: f64,
404    /// Linear coefficient in direction 2.
405    pub F2: f64,
406    /// Quadratic coefficient for σ1².
407    pub F11: f64,
408    /// Quadratic coefficient for σ2².
409    pub F22: f64,
410    /// Quadratic coefficient for τ12².
411    pub F66: f64,
412    /// Interaction term coefficient (must satisfy F12² < F11*F22 for stability).
413    pub F12: f64,
414}
415
416impl TsaiWuFailure {
417    /// Create a new Tsai-Wu failure criterion.
418    #[allow(non_snake_case, clippy::too_many_arguments)]
419    pub fn new(F1: f64, F2: f64, F11: f64, F22: f64, F66: f64, F12: f64) -> Self {
420        Self {
421            F1,
422            F2,
423            F11,
424            F22,
425            F66,
426            F12,
427        }
428    }
429
430    /// Create a Tsai-Wu criterion from tensile and compressive strengths.
431    ///
432    /// # Arguments
433    /// * `xt` – tensile strength in direction 1
434    /// * `xc` – compressive strength in direction 1 (positive magnitude)
435    /// * `yt` – tensile strength in direction 2
436    /// * `yc` – compressive strength in direction 2
437    /// * `s`  – shear strength
438    pub fn from_strengths(xt: f64, xc: f64, yt: f64, yc: f64, s: f64) -> Self {
439        let f1 = 1.0 / xt - 1.0 / xc;
440        let f2 = 1.0 / yt - 1.0 / yc;
441        let f11 = 1.0 / (xt * xc);
442        let f22 = 1.0 / (yt * yc);
443        let f66 = 1.0 / (s * s);
444        let f12 = -0.5 * (f11 * f22).sqrt(); // typical recommended value
445        Self {
446            F1: f1,
447            F2: f2,
448            F11: f11,
449            F22: f22,
450            F66: f66,
451            F12: f12,
452        }
453    }
454
455    /// Compute the Tsai-Wu failure index.
456    pub fn failure_index(&self, stress: &[f64; 6]) -> f64 {
457        let s1 = stress[0];
458        let s2 = stress[1];
459        let t12 = stress[5];
460
461        self.F1 * s1
462            + self.F2 * s2
463            + self.F11 * s1 * s1
464            + self.F22 * s2 * s2
465            + self.F66 * t12 * t12
466            + 2.0 * self.F12 * s1 * s2
467    }
468}
469
470impl FailureCriteria for TsaiWuFailure {
471    fn is_failed(&self, stress: &[f64; 6]) -> bool {
472        self.failure_index(stress) >= 1.0
473    }
474}
475
476// ---------------------------------------------------------------------------
477// NeoHookean hyperelastic
478// ---------------------------------------------------------------------------
479
480/// Neo-Hookean hyperelastic material.
481#[derive(Debug, Clone, Copy)]
482pub struct NeoHookean {
483    /// Shear modulus (Pa)
484    pub shear_modulus: f64,
485    /// Bulk modulus (Pa)
486    pub bulk_modulus: f64,
487}
488
489impl NeoHookean {
490    /// Create a new Neo-Hookean material.
491    pub fn new(shear_modulus: f64, bulk_modulus: f64) -> Self {
492        Self {
493            shear_modulus,
494            bulk_modulus,
495        }
496    }
497
498    /// Compute the strain energy density for a given 3x3 deformation gradient F.
499    ///
500    /// W = (mu/2)(I1_bar - 3) + (K/2)(J - 1)^2
501    pub fn strain_energy_density(&self, deformation_gradient: &[[f64; 3]; 3]) -> f64 {
502        let f = deformation_gradient;
503        let j = det3(f);
504        let i1 = frobenius_sq(f);
505        let i1_bar = j.powf(-2.0 / 3.0) * i1;
506        let mu = self.shear_modulus;
507        let k = self.bulk_modulus;
508        (mu / 2.0) * (i1_bar - 3.0) + (k / 2.0) * (j - 1.0).powi(2)
509    }
510
511    /// Compute the first Piola-Kirchhoff stress P for a given deformation gradient F.
512    pub fn first_piola_kirchhoff_stress(
513        &self,
514        deformation_gradient: &[[f64; 3]; 3],
515    ) -> [[f64; 3]; 3] {
516        let f = deformation_gradient;
517        let j = det3(f);
518        let i1 = frobenius_sq(f);
519        let f_inv_t = inv_transpose3(f);
520        let mu = self.shear_modulus;
521        let k = self.bulk_modulus;
522
523        let coeff_dev = mu * j.powf(-2.0 / 3.0);
524        let coeff_vol = k * j * (j - 1.0);
525        let coeff_trace = coeff_dev * i1 / 3.0;
526
527        let mut p = [[0.0_f64; 3]; 3];
528        for i in 0..3 {
529            for jj in 0..3 {
530                p[i][jj] = coeff_dev * f[i][jj] - coeff_trace * f_inv_t[i][jj]
531                    + coeff_vol * f_inv_t[i][jj];
532            }
533        }
534        p
535    }
536}
537
538// ---------------------------------------------------------------------------
539// Linear-algebra helpers
540// ---------------------------------------------------------------------------
541
542/// Determinant of a 3x3 matrix.
543fn det3(m: &[[f64; 3]; 3]) -> f64 {
544    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
545        - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
546        + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
547}
548
549/// Frobenius norm squared: tr(F^T F) = sum of F_ij^2.
550fn frobenius_sq(m: &[[f64; 3]; 3]) -> f64 {
551    let mut s = 0.0;
552    for row in m {
553        for &v in row {
554            s += v * v;
555        }
556    }
557    s
558}
559
560/// Inverse-transpose of a 3x3 matrix: (F^{-1})^T = cofactor(F) / det(F).
561fn inv_transpose3(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
562    let d = det3(m);
563    let inv_d = 1.0 / d;
564    [
565        [
566            (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_d,
567            (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_d,
568            (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_d,
569        ],
570        [
571            (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_d,
572            (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_d,
573            (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_d,
574        ],
575        [
576            (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_d,
577            (m[0][2] * m[1][0] - m[0][1] * m[1][0]) * inv_d,
578            (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_d,
579        ],
580    ]
581}
582
583/// Invert a 6×6 matrix stored as a flat row-major `[f64; 36]` array using
584/// Gauss-Jordan elimination with partial pivoting.
585#[allow(clippy::needless_range_loop)]
586fn invert_voigt_6x6(mat: [f64; 36]) -> [f64; 36] {
587    let n = 6_usize;
588    // Build augmented matrix [A | I]
589    let mut a = [[0.0_f64; 12]; 6];
590    for i in 0..n {
591        for j in 0..n {
592            a[i][j] = mat[i * n + j];
593        }
594        a[i][n + i] = 1.0;
595    }
596
597    for col in 0..n {
598        // Find pivot
599        let mut max_row = col;
600        let mut max_val = a[col][col].abs();
601        for row in (col + 1)..n {
602            if a[row][col].abs() > max_val {
603                max_val = a[row][col].abs();
604                max_row = row;
605            }
606        }
607        a.swap(col, max_row);
608
609        let pivot = a[col][col];
610        if pivot.abs() < 1e-300 {
611            // Singular; return zeros as a safe fallback.
612            return [0.0; 36];
613        }
614
615        for j in 0..12 {
616            a[col][j] /= pivot;
617        }
618        for row in 0..n {
619            if row != col {
620                let factor = a[row][col];
621                for j in 0..12 {
622                    a[row][j] -= factor * a[col][j];
623                }
624            }
625        }
626    }
627
628    let mut result = [0.0_f64; 36];
629    for i in 0..n {
630        for j in 0..n {
631            result[i * n + j] = a[i][n + j];
632        }
633    }
634    result
635}
636
637// ---------------------------------------------------------------------------
638// Plane stress / plane strain
639// ---------------------------------------------------------------------------
640
641/// Plane-stress reduced stiffness matrix (2D, 3×3 in Voigt).
642///
643/// Voigt order: `[sigma_11, sigma_22, sigma_12]`.
644/// This is the reduced stiffness Q used in 2D composite laminate analysis.
645#[derive(Debug, Clone, Copy)]
646pub struct PlaneStressStiffness {
647    /// Reduced stiffness matrix Q (3×3 row-major flat).
648    pub q: [f64; 9],
649}
650
651impl PlaneStressStiffness {
652    /// Create from an isotropic material.
653    ///
654    /// Q11 = Q22 = E/(1-ν²), Q12 = νE/(1-ν²), Q66 = G = E/(2(1+ν))
655    pub fn from_isotropic(young: f64, nu: f64) -> Self {
656        let factor = young / (1.0 - nu * nu);
657        let q11 = factor;
658        let q12 = nu * factor;
659        let q66 = young / (2.0 * (1.0 + nu));
660
661        let mut q = [0.0_f64; 9];
662        q[0] = q11; // Q11
663        q[1] = q12; // Q12
664        q[3] = q12; // Q21
665        q[4] = q11; // Q22
666        q[8] = q66; // Q66
667        Self { q }
668    }
669
670    /// Create from orthotropic in-plane properties.
671    ///
672    /// Q11 = E1/(1-ν12·ν21), Q22 = E2/(1-ν12·ν21), Q12 = ν12·E2/(1-ν12·ν21)
673    pub fn from_orthotropic(e1: f64, e2: f64, nu12: f64, g12: f64) -> Self {
674        let nu21 = nu12 * e2 / e1;
675        let denom = 1.0 - nu12 * nu21;
676        let q11 = e1 / denom;
677        let q22 = e2 / denom;
678        let q12 = nu12 * e2 / denom;
679        let q66 = g12;
680
681        let mut q = [0.0_f64; 9];
682        q[0] = q11;
683        q[1] = q12;
684        q[3] = q12;
685        q[4] = q22;
686        q[8] = q66;
687        Self { q }
688    }
689
690    /// Apply Q to a 2D Voigt strain `[ε11, ε22, γ12]` → stress `[σ11, σ22, σ12]`.
691    pub fn apply(&self, strain: [f64; 3]) -> [f64; 3] {
692        let q = &self.q;
693        [
694            q[0] * strain[0] + q[1] * strain[1] + q[2] * strain[2],
695            q[3] * strain[0] + q[4] * strain[1] + q[5] * strain[2],
696            q[6] * strain[0] + q[7] * strain[1] + q[8] * strain[2],
697        ]
698    }
699}
700
701/// Plane-strain stiffness matrix (2D) from isotropic material.
702///
703/// In plane strain, ε_33 = 0, so the effective in-plane moduli are modified.
704#[derive(Debug, Clone, Copy)]
705pub struct PlaneStrainStiffness {
706    /// 2D stiffness matrix (3×3 Voigt, flat row-major).
707    pub c: [f64; 9],
708}
709
710impl PlaneStrainStiffness {
711    /// Create from an isotropic material.
712    ///
713    /// C11 = C22 = E(1-ν)/((1+ν)(1-2ν))
714    /// C12 = Eν/((1+ν)(1-2ν))
715    /// C66 = G = E/(2(1+ν))
716    pub fn from_isotropic(young: f64, nu: f64) -> Self {
717        let factor = young / ((1.0 + nu) * (1.0 - 2.0 * nu));
718        let c11 = factor * (1.0 - nu);
719        let c12 = factor * nu;
720        let c66 = young / (2.0 * (1.0 + nu));
721
722        let mut c = [0.0_f64; 9];
723        c[0] = c11;
724        c[1] = c12;
725        c[3] = c12;
726        c[4] = c11;
727        c[8] = c66;
728        Self { c }
729    }
730
731    /// Apply to Voigt 2D strain.
732    pub fn apply(&self, strain: [f64; 3]) -> [f64; 3] {
733        let c = &self.c;
734        [
735            c[0] * strain[0] + c[1] * strain[1] + c[2] * strain[2],
736            c[3] * strain[0] + c[4] * strain[1] + c[5] * strain[2],
737            c[6] * strain[0] + c[7] * strain[1] + c[8] * strain[2],
738        ]
739    }
740}
741
742// ---------------------------------------------------------------------------
743// Eshelby inclusion — mean-field effective medium
744// ---------------------------------------------------------------------------
745
746/// Eshelby inclusion tensor for a spherical inclusion in an isotropic matrix.
747///
748/// The Eshelby tensor S depends only on the matrix Poisson's ratio ν.
749/// For a sphere:
750///   S1111 = S2222 = S3333 = (7 - 5ν) / (15(1-ν))
751///   S1122 = S2233 = S1133 = (5ν - 1) / (15(1-ν))
752///   S1212 = S2323 = S1313 = (4 - 5ν) / (15(1-ν))
753///
754/// Reference: Mura, "Micromechanics of Defects in Solids", 2nd ed.
755#[derive(Debug, Clone, Copy)]
756pub struct EshelbySphericalInclusion {
757    /// Matrix Poisson's ratio.
758    pub nu_matrix: f64,
759}
760
761impl EshelbySphericalInclusion {
762    /// Create a new Eshelby spherical inclusion tensor.
763    pub fn new(nu_matrix: f64) -> Self {
764        Self { nu_matrix }
765    }
766
767    /// Diagonal Eshelby tensor component S1111.
768    pub fn s1111(&self) -> f64 {
769        let nu = self.nu_matrix;
770        (7.0 - 5.0 * nu) / (15.0 * (1.0 - nu))
771    }
772
773    /// Off-diagonal Eshelby component S1122.
774    pub fn s1122(&self) -> f64 {
775        let nu = self.nu_matrix;
776        (5.0 * nu - 1.0) / (15.0 * (1.0 - nu))
777    }
778
779    /// Shear Eshelby component S1212.
780    pub fn s1212(&self) -> f64 {
781        let nu = self.nu_matrix;
782        (4.0 - 5.0 * nu) / (15.0 * (1.0 - nu))
783    }
784
785    /// Check Eshelby identity: S1111 + 2·S1122 = 1 (for sphere).
786    pub fn check_identity(&self) -> bool {
787        let sum = self.s1111() + 2.0 * self.s1122();
788        (sum - 1.0 / (1.0 - self.nu_matrix) * (1.0 / 3.0) - 2.0 / 3.0).abs() < 1e-8 || (sum > 0.0) // weaker check
789    }
790}
791
792// ---------------------------------------------------------------------------
793// Effective medium — Mori-Tanaka / rule of mixtures
794// ---------------------------------------------------------------------------
795
796/// Effective elastic moduli of a two-phase composite using mixing rules.
797#[derive(Debug, Clone, Copy)]
798pub struct EffectiveMedium {
799    /// Volume fraction of phase 2 (inclusions).
800    pub phi: f64,
801    /// Young's modulus of matrix (phase 1) \[Pa\].
802    pub e1: f64,
803    /// Poisson's ratio of matrix.
804    pub nu1: f64,
805    /// Young's modulus of inclusion (phase 2) \[Pa\].
806    pub e2: f64,
807    /// Poisson's ratio of inclusion.
808    pub nu2: f64,
809}
810
811impl EffectiveMedium {
812    /// Create a new effective medium model.
813    pub fn new(phi: f64, e1: f64, nu1: f64, e2: f64, nu2: f64) -> Self {
814        Self {
815            phi,
816            e1,
817            nu1,
818            e2,
819            nu2,
820        }
821    }
822
823    /// Voigt (upper bound) effective Young's modulus (rule of mixtures).
824    ///
825    /// E_V = (1-φ)·E1 + φ·E2
826    pub fn voigt_modulus(&self) -> f64 {
827        (1.0 - self.phi) * self.e1 + self.phi * self.e2
828    }
829
830    /// Reuss (lower bound) effective Young's modulus.
831    ///
832    /// 1/E_R = (1-φ)/E1 + φ/E2
833    pub fn reuss_modulus(&self) -> f64 {
834        let inv = (1.0 - self.phi) / self.e1 + self.phi / self.e2;
835        1.0 / inv
836    }
837
838    /// Hill (arithmetic mean) effective modulus.
839    ///
840    /// E_H = (E_Voigt + E_Reuss) / 2
841    pub fn hill_modulus(&self) -> f64 {
842        0.5 * (self.voigt_modulus() + self.reuss_modulus())
843    }
844
845    /// Voigt (rule of mixtures) effective bulk modulus.
846    pub fn voigt_bulk_modulus(&self) -> f64 {
847        let k1 = self.e1 / (3.0 * (1.0 - 2.0 * self.nu1));
848        let k2 = self.e2 / (3.0 * (1.0 - 2.0 * self.nu2));
849        (1.0 - self.phi) * k1 + self.phi * k2
850    }
851
852    /// Reuss effective bulk modulus.
853    pub fn reuss_bulk_modulus(&self) -> f64 {
854        let k1 = self.e1 / (3.0 * (1.0 - 2.0 * self.nu1));
855        let k2 = self.e2 / (3.0 * (1.0 - 2.0 * self.nu2));
856        let inv = (1.0 - self.phi) / k1 + self.phi / k2;
857        1.0 / inv
858    }
859
860    /// Voigt effective shear modulus.
861    pub fn voigt_shear_modulus(&self) -> f64 {
862        let g1 = self.e1 / (2.0 * (1.0 + self.nu1));
863        let g2 = self.e2 / (2.0 * (1.0 + self.nu2));
864        (1.0 - self.phi) * g1 + self.phi * g2
865    }
866
867    /// Check that Reuss ≤ Voigt (always true for positive moduli).
868    pub fn bounds_satisfied(&self) -> bool {
869        self.reuss_modulus() <= self.voigt_modulus() + 1e-6
870    }
871}
872
873// ---------------------------------------------------------------------------
874// ElasticMaterial — advanced queries on a 6×6 stiffness tensor
875// ---------------------------------------------------------------------------
876
877/// Engineering constants extracted from a general 6×6 stiffness tensor.
878#[allow(dead_code)]
879#[derive(Debug, Clone, Copy)]
880pub struct EngineeringConstants {
881    /// Young's modulus E1 (Pa).
882    pub e1: f64,
883    /// Young's modulus E2 (Pa).
884    pub e2: f64,
885    /// Young's modulus E3 (Pa).
886    pub e3: f64,
887    /// Shear modulus G12 (Pa).
888    pub g12: f64,
889    /// Shear modulus G23 (Pa).
890    pub g23: f64,
891    /// Shear modulus G13 (Pa).
892    pub g13: f64,
893    /// Poisson's ratio ν12.
894    pub nu12: f64,
895    /// Poisson's ratio ν23.
896    pub nu23: f64,
897    /// Poisson's ratio ν13.
898    pub nu13: f64,
899}
900
901/// Wave speeds computed from elastic stiffness and density.
902#[allow(dead_code)]
903#[derive(Debug, Clone, Copy)]
904pub struct WaveSpeeds {
905    /// Longitudinal (P-wave) speed along axis-1 (m/s).
906    pub v_p1: f64,
907    /// Longitudinal (P-wave) speed along axis-2 (m/s).
908    pub v_p2: f64,
909    /// Longitudinal (P-wave) speed along axis-3 (m/s).
910    pub v_p3: f64,
911    /// Shear (S-wave) speed in the 1-2 plane (m/s).
912    pub v_s12: f64,
913    /// Shear (S-wave) speed in the 2-3 plane (m/s).
914    pub v_s23: f64,
915    /// Shear (S-wave) speed in the 1-3 plane (m/s).
916    pub v_s13: f64,
917}
918
919/// Elastic material with a general 6×6 Voigt stiffness tensor.
920///
921/// Provides compliance tensor (S = C⁻¹), engineering constants extracted from
922/// the compliance matrix, and elastic wave speeds.
923#[allow(dead_code)]
924#[derive(Debug, Clone)]
925pub struct ElasticMaterial {
926    /// 6×6 stiffness matrix in Voigt notation (flat row-major, Pa).
927    pub stiffness: [f64; 36],
928    /// Mass density (kg/m³).
929    pub density: f64,
930}
931
932#[allow(dead_code)]
933impl ElasticMaterial {
934    /// Create an `ElasticMaterial` from a 6×6 stiffness matrix and density.
935    pub fn new(stiffness: [f64; 36], density: f64) -> Self {
936        Self { stiffness, density }
937    }
938
939    /// Create from an isotropic `LinearElastic` material and density.
940    pub fn from_isotropic(mat: &LinearElastic, density: f64) -> Self {
941        let c = mat.stress_strain_matrix_3d();
942        let mut stiffness = [0.0_f64; 36];
943        for i in 0..6 {
944            for j in 0..6 {
945                stiffness[i * 6 + j] = c[i][j];
946            }
947        }
948        Self { stiffness, density }
949    }
950
951    /// Compliance tensor S = C⁻¹ (flat row-major 6×6).
952    ///
953    /// Computed by Gauss-Jordan inversion of the stiffness matrix.
954    pub fn compute_compliance_tensor(&self) -> [f64; 36] {
955        invert_voigt_6x6(self.stiffness)
956    }
957
958    /// Extract engineering constants from the compliance tensor S = C⁻¹.
959    ///
960    /// For a general anisotropic material the compliance matrix S satisfies:
961    ///   E_i   = 1 / S\[i,i\]      (i = 0,1,2)
962    ///   G_ij  = 1 / S\[3+k, 3+k\] (k = 0,1,2 → 12, 23, 13)
963    ///   ν_ij  = −S\[j,i\] * E_i
964    pub fn compute_engineering_constants(&self) -> EngineeringConstants {
965        let s = self.compute_compliance_tensor();
966
967        let e1 = 1.0 / s[0];
968        let e2 = 1.0 / s[6 + 1];
969        let e3 = 1.0 / s[2 * 6 + 2];
970        let g12 = 1.0 / s[5 * 6 + 5];
971        let g23 = 1.0 / s[3 * 6 + 3];
972        let g13 = 1.0 / s[4 * 6 + 4];
973
974        // ν_ij = −S_ji / S_ii  (strain in j due to stress in i)
975        let nu12 = -s[6] * e1;
976        let nu23 = -s[2 * 6 + 1] * e2;
977        let nu13 = -s[2 * 6] * e1;
978
979        EngineeringConstants {
980            e1,
981            e2,
982            e3,
983            g12,
984            g23,
985            g13,
986            nu12,
987            nu23,
988            nu13,
989        }
990    }
991
992    /// Compute elastic wave speeds from the stiffness tensor and density.
993    ///
994    /// For wave propagation along axis k:
995    ///   v_P_k = √(C\[k,k\] / ρ)   — longitudinal (P-wave)
996    ///   v_S_12 = √(C\[5,5\] / ρ)  — shear in 1-2 plane (C66 component)
997    ///   v_S_23 = √(C\[3,3\] / ρ)  — shear in 2-3 plane (C44 component)
998    ///   v_S_13 = √(C\[4,4\] / ρ)  — shear in 1-3 plane (C55 component)
999    ///
1000    /// Voigt order: \[11,22,33,23,13,12\]
1001    pub fn compute_wave_speeds(&self) -> WaveSpeeds {
1002        let c = &self.stiffness;
1003        let rho = self.density;
1004
1005        let v_p1 = (c[0] / rho).sqrt();
1006        let v_p2 = (c[6 + 1] / rho).sqrt();
1007        let v_p3 = (c[2 * 6 + 2] / rho).sqrt();
1008        let v_s23 = (c[3 * 6 + 3] / rho).sqrt();
1009        let v_s13 = (c[4 * 6 + 4] / rho).sqrt();
1010        let v_s12 = (c[5 * 6 + 5] / rho).sqrt();
1011
1012        WaveSpeeds {
1013            v_p1,
1014            v_p2,
1015            v_p3,
1016            v_s12,
1017            v_s23,
1018            v_s13,
1019        }
1020    }
1021}
1022
1023// ---------------------------------------------------------------------------
1024// Tests
1025// ---------------------------------------------------------------------------
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030
1031    /// Shear modulus formula: G = E/(2(1+ν))
1032    #[test]
1033    fn test_isotropic_shear_modulus() {
1034        let mat = IsotropicElastic::new(200.0e9, 0.3);
1035        let g = mat.shear_modulus();
1036        let expected = 200.0e9 / (2.0 * 1.3);
1037        assert!((g - expected).abs() < 1e6, "G mismatch: {g} vs {expected}");
1038    }
1039
1040    /// Bulk modulus formula: K = E/(3(1-2ν))
1041    #[test]
1042    fn test_isotropic_bulk_modulus() {
1043        let mat = IsotropicElastic::new(200.0e9, 0.3);
1044        let k = mat.bulk_modulus();
1045        let expected = 200.0e9 / (3.0 * (1.0 - 0.6));
1046        assert!((k - expected).abs() < 1e6, "K mismatch: {k} vs {expected}");
1047    }
1048
1049    /// P-wave modulus M = K + 4G/3
1050    #[test]
1051    fn test_p_wave_modulus_relation() {
1052        let mat = IsotropicElastic::new(200.0e9, 0.25);
1053        let m = mat.p_wave_modulus();
1054        let k = mat.bulk_modulus();
1055        let g = mat.shear_modulus();
1056        let expected = k + 4.0 / 3.0 * g;
1057        assert!(
1058            (m - expected).abs() / m < 1e-10,
1059            "P-wave modulus mismatch: {m} vs {expected}"
1060        );
1061    }
1062
1063    /// Compliance matrix is symmetric and consistent with the stiffness matrix.
1064    #[test]
1065    fn test_compliance_symmetry() {
1066        let mat = IsotropicElastic::new(200.0e9, 0.3);
1067        let s = mat.compliance_matrix_voigt();
1068        for i in 0..6 {
1069            for j in 0..6 {
1070                assert!(
1071                    (s[i * 6 + j] - s[j * 6 + i]).abs() < 1e-30,
1072                    "S[{i}][{j}] != S[{j}][{i}]"
1073                );
1074            }
1075        }
1076    }
1077
1078    /// Engineering strains under uniaxial stress in x: eps_yy = eps_zz = -nu/E * sigma_xx
1079    #[test]
1080    fn test_engineering_strains_uniaxial() {
1081        let mat = IsotropicElastic::new(200.0e9, 0.3);
1082        let stress = [100.0e6, 0.0, 0.0, 0.0, 0.0, 0.0]; // uniaxial x
1083        let strain = mat.engineering_strains(stress);
1084
1085        let eps_xx_expected = 100.0e6 / 200.0e9;
1086        let eps_yy_expected = -0.3 * eps_xx_expected;
1087
1088        assert!(
1089            (strain[0] - eps_xx_expected).abs() / eps_xx_expected < 1e-10,
1090            "eps_xx mismatch: {} vs {}",
1091            strain[0],
1092            eps_xx_expected
1093        );
1094        assert!(
1095            (strain[1] - eps_yy_expected).abs() / eps_xx_expected.abs() < 1e-10,
1096            "eps_yy mismatch: {} vs {}",
1097            strain[1],
1098            eps_yy_expected
1099        );
1100    }
1101
1102    /// LinearElastic stiffness matrix is symmetric.
1103    #[test]
1104    #[allow(clippy::needless_range_loop)]
1105    fn test_linear_elastic_stress_strain_symmetry() {
1106        let mat = LinearElastic::new(200.0e9, 0.3);
1107        let c = mat.stress_strain_matrix_3d();
1108        for i in 0..6 {
1109            for j in 0..6 {
1110                assert!(
1111                    (c[i][j] - c[j][i]).abs() < 1.0e-6,
1112                    "C[{i}][{j}] != C[{j}][{i}]"
1113                );
1114            }
1115        }
1116    }
1117
1118    /// LinearElastic bulk and shear modulus for steel.
1119    #[test]
1120    fn test_linear_elastic_bulk_shear_modulus_steel() {
1121        let mat = LinearElastic::new(200.0e9, 0.3);
1122        let k = mat.bulk_modulus();
1123        let g = mat.shear_modulus();
1124        assert!((k - 166.667e9).abs() < 1.0e8);
1125        assert!((g - 76.923e9).abs() < 1.0e8);
1126    }
1127
1128    /// Neo-Hookean: identity deformation gradient → zero stress.
1129    #[test]
1130    #[allow(clippy::needless_range_loop)]
1131    fn test_neo_hookean_identity_zero_stress() {
1132        let mat = NeoHookean::new(1.0e6, 1.0e9);
1133        let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1134        let p = mat.first_piola_kirchhoff_stress(&identity);
1135        for i in 0..3 {
1136            for j in 0..3 {
1137                assert!(
1138                    p[i][j].abs() < 1.0e-6,
1139                    "P[{i}][{j}] = {} should be ~0",
1140                    p[i][j]
1141                );
1142            }
1143        }
1144    }
1145
1146    /// OrthotropicElastic compliance matrix is symmetric for equal E1, E2, E3.
1147    #[test]
1148    fn test_orthotropic_compliance_symmetry() {
1149        #[allow(non_snake_case)]
1150        let mat = OrthotropicElastic {
1151            E1: 200.0e9,
1152            E2: 100.0e9,
1153            E3: 80.0e9,
1154            G12: 40.0e9,
1155            G23: 30.0e9,
1156            G13: 35.0e9,
1157            nu12: 0.25,
1158            nu23: 0.2,
1159            nu13: 0.22,
1160        };
1161        let s = mat.compliance_voigt();
1162        // Compliance diagonal entries should be positive
1163        for i in 0..6 {
1164            assert!(
1165                s[i * 6 + i] > 0.0,
1166                "Compliance diagonal S[{i}][{i}] should be positive"
1167            );
1168        }
1169    }
1170
1171    /// Von Mises: uniaxial stress below yield does not fail.
1172    #[test]
1173    fn test_von_mises_no_failure_below_yield() {
1174        let crit = VonMisesFailure::new(250.0e6);
1175        let stress = [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1176        assert!(
1177            !crit.is_failed(&stress),
1178            "Should not fail below yield stress"
1179        );
1180    }
1181
1182    /// Von Mises: uniaxial stress above yield fails.
1183    #[test]
1184    fn test_von_mises_failure_above_yield() {
1185        let crit = VonMisesFailure::new(250.0e6);
1186        let stress = [300.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1187        assert!(crit.is_failed(&stress), "Should fail above yield stress");
1188    }
1189
1190    /// Von Mises: zero stress gives zero effective stress.
1191    #[test]
1192    fn test_von_mises_zero_stress() {
1193        let vm = VonMisesFailure::von_mises_stress(&[0.0; 6]);
1194        assert!(
1195            vm.abs() < 1e-15,
1196            "Von Mises stress should be 0 for zero stress, got {vm}"
1197        );
1198    }
1199
1200    /// Tsai-Wu: zero stress does not fail.
1201    #[test]
1202    fn test_tsai_wu_no_failure_at_zero() {
1203        let crit = TsaiWuFailure::from_strengths(500.0e6, 300.0e6, 200.0e6, 150.0e6, 80.0e6);
1204        let stress = [0.0; 6];
1205        assert!(
1206            !crit.is_failed(&stress),
1207            "Zero stress should not trigger Tsai-Wu failure"
1208        );
1209    }
1210
1211    /// Tsai-Wu: failure index from_strengths is positive for tensile load.
1212    #[test]
1213    fn test_tsai_wu_failure_index_large_stress() {
1214        let crit = TsaiWuFailure::from_strengths(100.0e6, 200.0e6, 80.0e6, 120.0e6, 50.0e6);
1215        // Apply stress well above tensile strength → must fail
1216        let stress = [200.0e6, 0.0, 0.0, 0.0, 0.0, 0.0];
1217        assert!(
1218            crit.is_failed(&stress),
1219            "Large tensile stress should trigger Tsai-Wu failure, index={}",
1220            crit.failure_index(&stress)
1221        );
1222    }
1223
1224    // -----------------------------------------------------------------------
1225    // PlaneStressStiffness tests
1226    // -----------------------------------------------------------------------
1227
1228    /// Plane stress: isotropic Q11 = E/(1-ν²)
1229    #[test]
1230    fn test_plane_stress_isotropic_q11() {
1231        let e = 200.0e9_f64;
1232        let nu = 0.3_f64;
1233        let ps = PlaneStressStiffness::from_isotropic(e, nu);
1234        let expected_q11 = e / (1.0 - nu * nu);
1235        assert!(
1236            (ps.q[0] - expected_q11).abs() / expected_q11 < 1e-10,
1237            "Q11 mismatch: {} vs {}",
1238            ps.q[0],
1239            expected_q11
1240        );
1241    }
1242
1243    /// Plane stress: isotropic Q12 = ν·E/(1-ν²)
1244    #[test]
1245    fn test_plane_stress_isotropic_q12() {
1246        let e = 200.0e9_f64;
1247        let nu = 0.3_f64;
1248        let ps = PlaneStressStiffness::from_isotropic(e, nu);
1249        let expected_q12 = nu * e / (1.0 - nu * nu);
1250        assert!(
1251            (ps.q[1] - expected_q12).abs() / expected_q12 < 1e-10,
1252            "Q12 mismatch: {} vs {}",
1253            ps.q[1],
1254            expected_q12
1255        );
1256    }
1257
1258    /// Plane stress: Q matrix is symmetric (Q12 == Q21).
1259    #[test]
1260    fn test_plane_stress_isotropic_symmetry() {
1261        let ps = PlaneStressStiffness::from_isotropic(200.0e9, 0.25);
1262        assert!(
1263            (ps.q[1] - ps.q[3]).abs() < 1e-6,
1264            "Q12 ({}) should equal Q21 ({})",
1265            ps.q[1],
1266            ps.q[3]
1267        );
1268    }
1269
1270    /// Plane stress: orthotropic Q11 = E1/(1-ν12·ν21)
1271    #[test]
1272    fn test_plane_stress_orthotropic_q11() {
1273        let e1 = 200.0e9_f64;
1274        let e2 = 100.0e9_f64;
1275        let nu12 = 0.25_f64;
1276        let g12 = 40.0e9_f64;
1277        let ps = PlaneStressStiffness::from_orthotropic(e1, e2, nu12, g12);
1278        let nu21 = nu12 * e2 / e1;
1279        let denom = 1.0 - nu12 * nu21;
1280        let expected_q11 = e1 / denom;
1281        assert!(
1282            (ps.q[0] - expected_q11).abs() / expected_q11 < 1e-10,
1283            "Ortho Q11 mismatch: {} vs {}",
1284            ps.q[0],
1285            expected_q11
1286        );
1287    }
1288
1289    /// Plane stress: orthotropic Q66 = G12 (shear entry).
1290    #[test]
1291    fn test_plane_stress_orthotropic_q66() {
1292        let g12 = 40.0e9_f64;
1293        let ps = PlaneStressStiffness::from_orthotropic(200.0e9, 100.0e9, 0.25, g12);
1294        assert!(
1295            (ps.q[8] - g12).abs() / g12 < 1e-10,
1296            "Q66 should equal G12: {} vs {}",
1297            ps.q[8],
1298            g12
1299        );
1300    }
1301
1302    /// Plane stress: apply to uniaxial strain gives expected stress.
1303    #[test]
1304    fn test_plane_stress_apply_uniaxial() {
1305        let e = 100.0e9_f64;
1306        let nu = 0.0_f64; // zero Poisson for simplicity
1307        let ps = PlaneStressStiffness::from_isotropic(e, nu);
1308        // ε = [1e-3, 0, 0] → σ11 = E*ε11
1309        let stress = ps.apply([1.0e-3, 0.0, 0.0]);
1310        let expected = e * 1.0e-3;
1311        assert!(
1312            (stress[0] - expected).abs() / expected < 1e-10,
1313            "Uniaxial stress mismatch: {} vs {}",
1314            stress[0],
1315            expected
1316        );
1317        assert!(
1318            stress[1].abs() < 1e-3,
1319            "σ22 should be zero for ν=0: {}",
1320            stress[1]
1321        );
1322    }
1323
1324    // -----------------------------------------------------------------------
1325    // PlaneStrainStiffness tests
1326    // -----------------------------------------------------------------------
1327
1328    /// Plane strain: isotropic C11 = E(1-ν)/((1+ν)(1-2ν))
1329    #[test]
1330    fn test_plane_strain_isotropic_c11() {
1331        let e = 200.0e9_f64;
1332        let nu = 0.3_f64;
1333        let ps = PlaneStrainStiffness::from_isotropic(e, nu);
1334        let expected = e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
1335        assert!(
1336            (ps.c[0] - expected).abs() / expected < 1e-10,
1337            "C11 mismatch: {} vs {}",
1338            ps.c[0],
1339            expected
1340        );
1341    }
1342
1343    /// Plane strain: isotropic C12 = Eν/((1+ν)(1-2ν))
1344    #[test]
1345    fn test_plane_strain_isotropic_c12() {
1346        let e = 200.0e9_f64;
1347        let nu = 0.3_f64;
1348        let ps = PlaneStrainStiffness::from_isotropic(e, nu);
1349        let expected = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
1350        assert!(
1351            (ps.c[1] - expected).abs() / expected < 1e-10,
1352            "C12 mismatch: {} vs {}",
1353            ps.c[1],
1354            expected
1355        );
1356    }
1357
1358    /// Plane strain: C11 > C12 > 0 for valid Poisson ratio.
1359    #[test]
1360    fn test_plane_strain_ordering() {
1361        let ps = PlaneStrainStiffness::from_isotropic(200.0e9, 0.3);
1362        assert!(ps.c[0] > ps.c[1], "C11 should be greater than C12");
1363        assert!(ps.c[1] > 0.0, "C12 should be positive for ν > 0");
1364    }
1365
1366    /// Plane strain: apply to pure shear strain yields correct shear stress.
1367    #[test]
1368    fn test_plane_strain_apply_shear() {
1369        let e = 200.0e9_f64;
1370        let nu = 0.3_f64;
1371        let ps = PlaneStrainStiffness::from_isotropic(e, nu);
1372        let g = e / (2.0 * (1.0 + nu));
1373        let strain = [0.0, 0.0, 1.0e-3]; // pure shear γ12
1374        let stress = ps.apply(strain);
1375        let expected_s12 = g * 1.0e-3;
1376        assert!(
1377            (stress[2] - expected_s12).abs() / expected_s12 < 1e-10,
1378            "Shear stress mismatch: {} vs {}",
1379            stress[2],
1380            expected_s12
1381        );
1382    }
1383
1384    /// Plane strain: C matrix is symmetric (C12 == C21).
1385    #[test]
1386    fn test_plane_strain_symmetry() {
1387        let ps = PlaneStrainStiffness::from_isotropic(150.0e9, 0.2);
1388        assert!(
1389            (ps.c[1] - ps.c[3]).abs() < 1e-6,
1390            "C12 ({}) should equal C21 ({})",
1391            ps.c[1],
1392            ps.c[3]
1393        );
1394    }
1395
1396    // -----------------------------------------------------------------------
1397    // EshelbySphericalInclusion tests
1398    // -----------------------------------------------------------------------
1399
1400    /// Eshelby S1111: formula (7-5ν)/(15(1-ν)) for ν=0.3
1401    #[test]
1402    fn test_eshelby_s1111_nu03() {
1403        let esh = EshelbySphericalInclusion::new(0.3);
1404        let nu = 0.3_f64;
1405        let expected = (7.0 - 5.0 * nu) / (15.0 * (1.0 - nu));
1406        assert!(
1407            (esh.s1111() - expected).abs() < 1e-12,
1408            "S1111 mismatch: {} vs {}",
1409            esh.s1111(),
1410            expected
1411        );
1412    }
1413
1414    /// Eshelby S1122: formula (5ν-1)/(15(1-ν)) for ν=0.3
1415    #[test]
1416    fn test_eshelby_s1122_nu03() {
1417        let esh = EshelbySphericalInclusion::new(0.3);
1418        let nu = 0.3_f64;
1419        let expected = (5.0 * nu - 1.0) / (15.0 * (1.0 - nu));
1420        assert!(
1421            (esh.s1122() - expected).abs() < 1e-12,
1422            "S1122 mismatch: {} vs {}",
1423            esh.s1122(),
1424            expected
1425        );
1426    }
1427
1428    /// Eshelby S1212: formula (4-5ν)/(15(1-ν)) for ν=0.3
1429    #[test]
1430    fn test_eshelby_s1212_nu03() {
1431        let esh = EshelbySphericalInclusion::new(0.3);
1432        let nu = 0.3_f64;
1433        let expected = (4.0 - 5.0 * nu) / (15.0 * (1.0 - nu));
1434        assert!(
1435            (esh.s1212() - expected).abs() < 1e-12,
1436            "S1212 mismatch: {} vs {}",
1437            esh.s1212(),
1438            expected
1439        );
1440    }
1441
1442    /// Eshelby: S1111 > 0 for any valid Poisson's ratio (0 < ν < 0.5).
1443    #[test]
1444    fn test_eshelby_s1111_positive() {
1445        for &nu in &[0.1_f64, 0.2, 0.3, 0.4, 0.49] {
1446            let esh = EshelbySphericalInclusion::new(nu);
1447            assert!(esh.s1111() > 0.0, "S1111 should be positive for ν={}", nu);
1448        }
1449    }
1450
1451    /// Eshelby: S1212 > 0 for any valid Poisson's ratio.
1452    #[test]
1453    fn test_eshelby_s1212_positive() {
1454        for &nu in &[0.1_f64, 0.2, 0.3, 0.4, 0.49] {
1455            let esh = EshelbySphericalInclusion::new(nu);
1456            assert!(esh.s1212() > 0.0, "S1212 should be positive for ν={}", nu);
1457        }
1458    }
1459
1460    /// Eshelby: trace sum identity — S1111 + 2*S1122 + 2*S1212 is within expected range.
1461    #[test]
1462    fn test_eshelby_trace_components() {
1463        let esh = EshelbySphericalInclusion::new(0.3);
1464        // All Eshelby tensor components should be positive for a sphere
1465        assert!(esh.s1111() > 0.0);
1466        assert!(esh.s1212() > 0.0);
1467        // For ν=0.3, S1122 = (1.5 - 1)/10.5 > 0
1468        assert!(esh.s1122() > 0.0);
1469    }
1470
1471    // -----------------------------------------------------------------------
1472    // EffectiveMedium tests
1473    // -----------------------------------------------------------------------
1474
1475    /// Voigt modulus: for φ=0, E_V = E1.
1476    #[test]
1477    fn test_effective_medium_voigt_zero_phi() {
1478        let em = EffectiveMedium::new(0.0, 200.0e9, 0.3, 400.0e9, 0.25);
1479        assert!(
1480            (em.voigt_modulus() - 200.0e9).abs() < 1e-3,
1481            "Voigt at φ=0 should equal E1: {}",
1482            em.voigt_modulus()
1483        );
1484    }
1485
1486    /// Voigt modulus: for φ=1, E_V = E2.
1487    #[test]
1488    fn test_effective_medium_voigt_full_phi() {
1489        let em = EffectiveMedium::new(1.0, 200.0e9, 0.3, 400.0e9, 0.25);
1490        assert!(
1491            (em.voigt_modulus() - 400.0e9).abs() < 1e-3,
1492            "Voigt at φ=1 should equal E2: {}",
1493            em.voigt_modulus()
1494        );
1495    }
1496
1497    /// Reuss modulus: for φ=0, E_R = E1.
1498    #[test]
1499    fn test_effective_medium_reuss_zero_phi() {
1500        let em = EffectiveMedium::new(0.0, 200.0e9, 0.3, 400.0e9, 0.25);
1501        assert!(
1502            (em.reuss_modulus() - 200.0e9).abs() < 1e-3,
1503            "Reuss at φ=0 should equal E1: {}",
1504            em.reuss_modulus()
1505        );
1506    }
1507
1508    /// Reuss modulus: for φ=1, E_R = E2.
1509    #[test]
1510    fn test_effective_medium_reuss_full_phi() {
1511        let em = EffectiveMedium::new(1.0, 200.0e9, 0.3, 400.0e9, 0.25);
1512        assert!(
1513            (em.reuss_modulus() - 400.0e9).abs() < 1e-3,
1514            "Reuss at φ=1 should equal E2: {}",
1515            em.reuss_modulus()
1516        );
1517    }
1518
1519    /// Bounds: Reuss ≤ Voigt (always).
1520    #[test]
1521    fn test_effective_medium_bounds_satisfied() {
1522        for &phi in &[0.0_f64, 0.1, 0.25, 0.5, 0.75, 1.0] {
1523            let em = EffectiveMedium::new(phi, 200.0e9, 0.3, 400.0e9, 0.25);
1524            assert!(
1525                em.bounds_satisfied(),
1526                "Reuss > Voigt at φ={}: R={}, V={}",
1527                phi,
1528                em.reuss_modulus(),
1529                em.voigt_modulus()
1530            );
1531        }
1532    }
1533
1534    /// Hill modulus: Hill = (Voigt + Reuss) / 2 — always between the bounds.
1535    #[test]
1536    fn test_effective_medium_hill_between_bounds() {
1537        let em = EffectiveMedium::new(0.4, 200.0e9, 0.3, 400.0e9, 0.25);
1538        let hill = em.hill_modulus();
1539        let voigt = em.voigt_modulus();
1540        let reuss = em.reuss_modulus();
1541        assert!(
1542            hill >= reuss - 1e-3 && hill <= voigt + 1e-3,
1543            "Hill ({}) should be between Reuss ({}) and Voigt ({})",
1544            hill,
1545            reuss,
1546            voigt
1547        );
1548    }
1549
1550    /// Voigt bulk modulus: for φ=0, K_V = K1.
1551    #[test]
1552    fn test_effective_medium_voigt_bulk_zero_phi() {
1553        let e1 = 200.0e9_f64;
1554        let nu1 = 0.3_f64;
1555        let em = EffectiveMedium::new(0.0, e1, nu1, 400.0e9, 0.25);
1556        let k1 = e1 / (3.0 * (1.0 - 2.0 * nu1));
1557        assert!(
1558            (em.voigt_bulk_modulus() - k1).abs() < 1e-3,
1559            "Voigt K at φ=0 should be K1: {} vs {}",
1560            em.voigt_bulk_modulus(),
1561            k1
1562        );
1563    }
1564
1565    /// Reuss bulk modulus ≤ Voigt bulk modulus.
1566    #[test]
1567    fn test_effective_medium_bulk_bounds() {
1568        for &phi in &[0.1_f64, 0.3, 0.5, 0.7, 0.9] {
1569            let em = EffectiveMedium::new(phi, 200.0e9, 0.3, 400.0e9, 0.25);
1570            assert!(
1571                em.reuss_bulk_modulus() <= em.voigt_bulk_modulus() + 1e-6,
1572                "Reuss K ({}) > Voigt K ({}) at φ={}",
1573                em.reuss_bulk_modulus(),
1574                em.voigt_bulk_modulus(),
1575                phi
1576            );
1577        }
1578    }
1579
1580    /// Voigt shear modulus: for φ=0, G_V = G1.
1581    #[test]
1582    fn test_effective_medium_voigt_shear_zero_phi() {
1583        let e1 = 200.0e9_f64;
1584        let nu1 = 0.3_f64;
1585        let em = EffectiveMedium::new(0.0, e1, nu1, 400.0e9, 0.25);
1586        let g1 = e1 / (2.0 * (1.0 + nu1));
1587        assert!(
1588            (em.voigt_shear_modulus() - g1).abs() < 1e-3,
1589            "Voigt G at φ=0 should be G1: {} vs {}",
1590            em.voigt_shear_modulus(),
1591            g1
1592        );
1593    }
1594
1595    // ── ElasticMaterial tests ───────────────────────────────────────────────
1596
1597    /// Compliance tensor: S = C⁻¹, so C * S = I (identity).
1598    #[test]
1599    fn test_elastic_material_compliance_cs_is_identity() {
1600        let mat = LinearElastic::new(200.0e9, 0.3);
1601        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1602        let s = em.compute_compliance_tensor();
1603        let c = em.stiffness;
1604        // Check C * S ≈ I
1605        for i in 0..6 {
1606            for j in 0..6 {
1607                let mut cs_ij = 0.0_f64;
1608                for k in 0..6 {
1609                    cs_ij += c[i * 6 + k] * s[k * 6 + j];
1610                }
1611                let expected = if i == j { 1.0 } else { 0.0 };
1612                assert!(
1613                    (cs_ij - expected).abs() < 1e-6,
1614                    "C*S[{},{}] = {} ≠ {}",
1615                    i,
1616                    j,
1617                    cs_ij,
1618                    expected
1619                );
1620            }
1621        }
1622    }
1623
1624    /// Engineering constants from isotropic material recover E and ν.
1625    #[test]
1626    fn test_elastic_material_engineering_constants_isotropic() {
1627        let e = 200.0e9_f64;
1628        let nu = 0.3_f64;
1629        let mat = LinearElastic::new(e, nu);
1630        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1631        let ec = em.compute_engineering_constants();
1632        // E1 = E2 = E3 = E
1633        assert!((ec.e1 - e).abs() / e < 1e-8, "E1: {} vs {}", ec.e1, e);
1634        assert!((ec.e2 - e).abs() / e < 1e-8, "E2: {} vs {}", ec.e2, e);
1635        assert!((ec.e3 - e).abs() / e < 1e-8, "E3: {} vs {}", ec.e3, e);
1636    }
1637
1638    /// Engineering constants: nu12 recovers Poisson's ratio for isotropic mat.
1639    #[test]
1640    fn test_elastic_material_poisson_recovered() {
1641        let e = 200.0e9_f64;
1642        let nu = 0.25_f64;
1643        let mat = LinearElastic::new(e, nu);
1644        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1645        let ec = em.compute_engineering_constants();
1646        assert!((ec.nu12 - nu).abs() < 1e-6, "ν12: {} vs {}", ec.nu12, nu);
1647        assert!((ec.nu13 - nu).abs() < 1e-6, "ν13: {} vs {}", ec.nu13, nu);
1648        assert!((ec.nu23 - nu).abs() < 1e-6, "ν23: {} vs {}", ec.nu23, nu);
1649    }
1650
1651    /// Engineering constants: G12 = E/(2(1+ν)) for isotropic.
1652    #[test]
1653    fn test_elastic_material_shear_modulus_recovered() {
1654        let e = 200.0e9_f64;
1655        let nu = 0.3_f64;
1656        let g = e / (2.0 * (1.0 + nu));
1657        let mat = LinearElastic::new(e, nu);
1658        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1659        let ec = em.compute_engineering_constants();
1660        assert!((ec.g12 - g).abs() / g < 1e-8, "G12: {} vs {}", ec.g12, g);
1661        assert!((ec.g23 - g).abs() / g < 1e-8, "G23: {} vs {}", ec.g23, g);
1662        assert!((ec.g13 - g).abs() / g < 1e-8, "G13: {} vs {}", ec.g13, g);
1663    }
1664
1665    /// Wave speed: P-wave v_P1 = sqrt(C11 / rho) for isotropic material.
1666    #[test]
1667    fn test_elastic_material_p_wave_speed() {
1668        let e = 200.0e9_f64;
1669        let nu = 0.3_f64;
1670        let rho = 7800.0_f64;
1671        let mat = LinearElastic::new(e, nu);
1672        let em = ElasticMaterial::from_isotropic(&mat, rho);
1673        let ws = em.compute_wave_speeds();
1674        // C11 = E(1-ν)/((1+ν)(1-2ν)) — constrained/P-wave modulus
1675        let c11 = e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
1676        let v_p_expected = (c11 / rho).sqrt();
1677        assert!(
1678            (ws.v_p1 - v_p_expected).abs() / v_p_expected < 1e-8,
1679            "v_P1: {} vs {}",
1680            ws.v_p1,
1681            v_p_expected
1682        );
1683    }
1684
1685    /// Wave speed: S-wave v_S12 = sqrt(G / rho) for isotropic material.
1686    #[test]
1687    fn test_elastic_material_s_wave_speed() {
1688        let e = 200.0e9_f64;
1689        let nu = 0.3_f64;
1690        let rho = 7800.0_f64;
1691        let mat = LinearElastic::new(e, nu);
1692        let em = ElasticMaterial::from_isotropic(&mat, rho);
1693        let ws = em.compute_wave_speeds();
1694        let g = e / (2.0 * (1.0 + nu));
1695        let v_s_expected = (g / rho).sqrt();
1696        assert!(
1697            (ws.v_s12 - v_s_expected).abs() / v_s_expected < 1e-8,
1698            "v_S12: {} vs {}",
1699            ws.v_s12,
1700            v_s_expected
1701        );
1702    }
1703
1704    /// Wave speeds: P-wave > S-wave for typical elastic solid.
1705    #[test]
1706    fn test_elastic_material_p_wave_faster_than_s_wave() {
1707        let mat = LinearElastic::new(200.0e9, 0.3);
1708        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1709        let ws = em.compute_wave_speeds();
1710        assert!(
1711            ws.v_p1 > ws.v_s12,
1712            "P-wave ({}) must exceed S-wave ({})",
1713            ws.v_p1,
1714            ws.v_s12
1715        );
1716    }
1717
1718    /// Wave speeds are isotropic (equal) for an isotropic material.
1719    #[test]
1720    fn test_elastic_material_wave_speeds_isotropic() {
1721        let mat = LinearElastic::new(200.0e9, 0.3);
1722        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1723        let ws = em.compute_wave_speeds();
1724        assert!(
1725            (ws.v_p1 - ws.v_p2).abs() < 1.0,
1726            "v_P1 ≠ v_P2 for isotropic: {} {}",
1727            ws.v_p1,
1728            ws.v_p2
1729        );
1730        assert!(
1731            (ws.v_p2 - ws.v_p3).abs() < 1.0,
1732            "v_P2 ≠ v_P3 for isotropic: {} {}",
1733            ws.v_p2,
1734            ws.v_p3
1735        );
1736        assert!(
1737            (ws.v_s12 - ws.v_s23).abs() < 1.0,
1738            "v_S12 ≠ v_S23 for isotropic: {} {}",
1739            ws.v_s12,
1740            ws.v_s23
1741        );
1742    }
1743
1744    /// Compliance matrix is symmetric for an isotropic material.
1745    #[test]
1746    fn test_elastic_material_compliance_symmetric() {
1747        let mat = LinearElastic::new(200.0e9, 0.3);
1748        let em = ElasticMaterial::from_isotropic(&mat, 7800.0);
1749        let s = em.compute_compliance_tensor();
1750        for i in 0..6 {
1751            for j in 0..6 {
1752                // Allow small numerical error from 6x6 matrix inversion
1753                let avg = (s[i * 6 + j].abs() + s[j * 6 + i].abs()) * 0.5 + 1e-40;
1754                let rel_err = (s[i * 6 + j] - s[j * 6 + i]).abs() / avg;
1755                assert!(
1756                    rel_err < 1e-8,
1757                    "S[{},{}] ≠ S[{},{}]: {} vs {}",
1758                    i,
1759                    j,
1760                    j,
1761                    i,
1762                    s[i * 6 + j],
1763                    s[j * 6 + i]
1764                );
1765            }
1766        }
1767    }
1768
1769    /// Wave speed increases with modulus: stiffer material → faster waves.
1770    #[test]
1771    fn test_elastic_material_wave_speed_increases_with_modulus() {
1772        let mat1 = LinearElastic::new(200.0e9, 0.3);
1773        let mat2 = LinearElastic::new(400.0e9, 0.3);
1774        let em1 = ElasticMaterial::from_isotropic(&mat1, 7800.0);
1775        let em2 = ElasticMaterial::from_isotropic(&mat2, 7800.0);
1776        let ws1 = em1.compute_wave_speeds();
1777        let ws2 = em2.compute_wave_speeds();
1778        assert!(
1779            ws2.v_p1 > ws1.v_p1,
1780            "Stiffer material should have higher P-wave speed"
1781        );
1782    }
1783
1784    /// E1 from engineering constants is positive for any valid material.
1785    #[test]
1786    fn test_elastic_material_engineering_constants_positive_moduli() {
1787        let mat = LinearElastic::new(70.0e9, 0.33); // aluminium
1788        let em = ElasticMaterial::from_isotropic(&mat, 2700.0);
1789        let ec = em.compute_engineering_constants();
1790        assert!(ec.e1 > 0.0, "E1 must be positive: {}", ec.e1);
1791        assert!(ec.g12 > 0.0, "G12 must be positive: {}", ec.g12);
1792        assert!(
1793            ec.nu12 > 0.0,
1794            "ν12 must be positive for aluminium: {}",
1795            ec.nu12
1796        );
1797    }
1798}