Skip to main content

oxiphysics_materials/
biomaterials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Biomaterials module: bone mechanics, cartilage, tendons, hydrogels, cell mechanics,
5//! biodegradation kinetics, scaffold porosity, sutures, and dental materials.
6//!
7//! All functions use SI units unless otherwise stated.
8
9#![allow(dead_code)]
10#![allow(unused_imports)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15// ---------------------------------------------------------------------------
16// Bone Mechanics — Cortical and Cancellous
17// ---------------------------------------------------------------------------
18
19/// Properties of cortical bone (compact bone).
20#[derive(Clone, Debug)]
21pub struct CorticalBone {
22    /// Longitudinal Young's modulus (Pa), typically ~17-25 GPa.
23    pub e_long: f64,
24    /// Transverse Young's modulus (Pa), typically ~10-15 GPa.
25    pub e_trans: f64,
26    /// Shear modulus (Pa).
27    pub g_lt: f64,
28    /// Longitudinal Poisson's ratio.
29    pub nu_lt: f64,
30    /// Transverse Poisson's ratio.
31    pub nu_tt: f64,
32    /// Density (kg/m³), typically ~1800-2000 kg/m³.
33    pub density: f64,
34    /// Ultimate tensile strength (Pa).
35    pub uts: f64,
36    /// Ultimate compressive strength (Pa).
37    pub ucs: f64,
38    /// Fracture toughness KIC (Pa·√m).
39    pub k_ic: f64,
40}
41
42impl CorticalBone {
43    /// Default human femoral cortical bone properties (alias).
44    pub fn femur_cortical() -> Self {
45        Self::human_femur()
46    }
47
48    /// Default human femoral cortical bone properties.
49    pub fn human_femur() -> Self {
50        Self {
51            e_long: 20.0e9,
52            e_trans: 12.0e9,
53            g_lt: 4.5e9,
54            nu_lt: 0.29,
55            nu_tt: 0.63,
56            density: 1900.0,
57            uts: 120.0e6,
58            ucs: 180.0e6,
59            k_ic: 2.5e6,
60        }
61    }
62
63    /// Compute the longitudinal wave speed (m/s).
64    pub fn longitudinal_wave_speed(&self) -> f64 {
65        (self.e_long / self.density).sqrt()
66    }
67
68    /// Compute the transverse wave speed (m/s).
69    pub fn transverse_wave_speed(&self) -> f64 {
70        (self.g_lt / self.density).sqrt()
71    }
72
73    /// Estimate the apparent density from volumetric fraction `vf` (0..1).
74    pub fn apparent_density(vf: f64) -> f64 {
75        // Rice's rule: rho_app = vf * rho_cortical
76        vf * 1900.0
77    }
78
79    /// Morgan-Keaveny power-law: E = a * rho^b (GPa), rho in g/cm³.
80    pub fn modulus_from_density_power_law(density_g_cm3: f64) -> f64 {
81        // Typical empirical fit for cortical bone
82        let a = 2.065e10;
83        let b = 3.09;
84        a * density_g_cm3.powf(b)
85    }
86
87    /// Compute bone stiffness in bending for a hollow cylinder (Euler-Bernoulli).
88    ///
89    /// `outer_r`: outer radius (m). `inner_r`: inner radius (m). `length`: beam length (m).
90    pub fn bending_stiffness(&self, outer_r: f64, inner_r: f64, length: f64) -> f64 {
91        let i = PI / 4.0 * (outer_r.powi(4) - inner_r.powi(4));
92        48.0 * self.e_long * i / length.powi(3)
93    }
94
95    /// Stress at outer fibre for three-point bending.
96    pub fn three_point_bending_stress(
97        &self,
98        force: f64,
99        length: f64,
100        outer_r: f64,
101        inner_r: f64,
102    ) -> f64 {
103        let i = PI / 4.0 * (outer_r.powi(4) - inner_r.powi(4));
104        force * length * outer_r / (4.0 * i)
105    }
106}
107
108/// Properties of cancellous (trabecular) bone.
109#[derive(Clone, Debug)]
110pub struct CancellousBone {
111    /// Relative density (ratio to cortical, 0..1).
112    pub relative_density: f64,
113    /// Apparent density (kg/m³).
114    pub apparent_density: f64,
115    /// Apparent elastic modulus (Pa).
116    pub apparent_modulus: f64,
117    /// Apparent strength (Pa).
118    pub apparent_strength: f64,
119    /// Anisotropy ratio (E_axial / E_transverse).
120    pub anisotropy_ratio: f64,
121}
122
123impl CancellousBone {
124    /// Construct cancellous bone using Gibson-Ashby cellular solid relations.
125    ///
126    /// `rel_density`: relative density (0..1).
127    pub fn from_relative_density(rel_density: f64) -> Self {
128        // Gibson-Ashby: E* / E_s = C1 * (rho*/rho_s)^2
129        let e_solid = 20.0e9;
130        let sigma_solid = 170.0e6;
131        let c1 = 1.0;
132        let c2 = 0.2;
133        let apparent_modulus = c1 * e_solid * rel_density.powi(2);
134        let apparent_strength = c2 * sigma_solid * rel_density.powf(1.5);
135        Self {
136            relative_density: rel_density,
137            apparent_density: rel_density * 1900.0,
138            apparent_modulus,
139            apparent_strength,
140            anisotropy_ratio: 1.5,
141        }
142    }
143
144    /// Compute energy absorption capacity (J/m³) under compression.
145    pub fn energy_absorption(&self) -> f64 {
146        // Area under stress-strain curve up to densification strain
147        let densification_strain = 1.0 - 1.4 * self.relative_density;
148        0.5 * self.apparent_strength * densification_strain.max(0.0)
149    }
150
151    /// Strain at failure under compression.
152    pub fn failure_strain(&self) -> f64 {
153        // Empirical: eps_f ~ 0.07 for rho_rel ≈ 0.2
154        0.07 * (self.relative_density / 0.2).powf(-0.3)
155    }
156}
157
158/// Anisotropic bone elastic tensor (Voigt notation, GPa-scaled).
159///
160/// Returns the 6×6 stiffness matrix in Voigt notation (upper triangular).
161pub fn cortical_bone_stiffness_tensor(
162    e_l: f64,
163    e_t: f64,
164    g_lt: f64,
165    nu_lt: f64,
166    nu_tt: f64,
167) -> [[f64; 6]; 6] {
168    // Transversely isotropic: axis of symmetry along L
169    let nu_tl = nu_lt * e_t / e_l;
170    let det = 1.0 - nu_tt * nu_tt - 2.0 * nu_lt * nu_tl;
171    let mut c = [[0.0f64; 6]; 6];
172    c[0][0] = e_l * (1.0 - nu_tt * nu_tt) / det;
173    c[1][1] = e_t * (1.0 - nu_lt * nu_tl) / det;
174    c[2][2] = c[1][1];
175    c[0][1] = e_l * (nu_tl + nu_tt * nu_tl) / det;
176    c[1][0] = c[0][1];
177    c[0][2] = c[0][1];
178    c[2][0] = c[0][2];
179    c[1][2] = e_t * (nu_tt + nu_lt * nu_tl) / det;
180    c[2][1] = c[1][2];
181    c[3][3] = g_lt;
182    c[4][4] = g_lt;
183    c[5][5] = e_t / (2.0 * (1.0 + nu_tt));
184    c
185}
186
187// ---------------------------------------------------------------------------
188// Cartilage Biphasic Model (Mow et al.)
189// ---------------------------------------------------------------------------
190
191/// Articular cartilage biphasic model parameters.
192#[derive(Clone, Debug)]
193pub struct CartilageBiphasic {
194    /// Aggregate modulus HA (Pa), typically 0.5-1.5 MPa.
195    pub ha: f64,
196    /// Shear modulus mu_s (Pa), typically 0.1-1.5 MPa.
197    pub mu_s: f64,
198    /// Hydraulic permeability k (m⁴/N·s), typically 10⁻¹⁵.
199    pub permeability: f64,
200    /// Thickness (m).
201    pub thickness: f64,
202    /// Tissue density (kg/m³).
203    pub density: f64,
204}
205
206impl CartilageBiphasic {
207    /// Create a model for the superficial zone of cartilage.
208    pub fn superficial_zone() -> Self {
209        Self {
210            ha: 0.42e6,
211            mu_s: 0.10e6,
212            permeability: 4.0e-15,
213            thickness: 2.0e-3,
214            density: 1100.0,
215        }
216    }
217
218    /// Create a model for the middle zone of cartilage.
219    pub fn middle_zone() -> Self {
220        Self {
221            ha: 0.60e6,
222            mu_s: 0.15e6,
223            permeability: 2.0e-15,
224            thickness: 2.5e-3,
225            density: 1100.0,
226        }
227    }
228
229    /// Create a model for the deep zone of cartilage.
230    pub fn deep_zone() -> Self {
231        Self {
232            ha: 0.85e6,
233            mu_s: 0.20e6,
234            permeability: 1.0e-15,
235            thickness: 3.0e-3,
236            density: 1100.0,
237        }
238    }
239
240    /// Human knee articular cartilage defaults.
241    pub fn knee() -> Self {
242        Self {
243            ha: 0.7e6,
244            mu_s: 0.15e6,
245            permeability: 2.17e-15,
246            thickness: 2.5e-3,
247            density: 1100.0,
248        }
249    }
250
251    /// Biphasic creep time constant tau = h^2 / (HA * k).
252    pub fn creep_time_constant(&self) -> f64 {
253        self.thickness * self.thickness / (self.ha * self.permeability)
254    }
255
256    /// Steady-state consolidation (creep) strain under stress `sigma`.
257    pub fn steady_state_strain(&self, sigma: f64) -> f64 {
258        sigma / self.ha
259    }
260
261    /// Creep displacement at time `t` for applied stress `sigma` (Mow's solution, first term).
262    pub fn creep_displacement(&self, sigma: f64, t: f64) -> f64 {
263        let tau = self.creep_time_constant();
264        let eps_inf = self.steady_state_strain(sigma);
265        // First-term approximation
266        let correction = (1.0 - (-t / tau * PI * PI / 4.0).exp()).max(0.0);
267        eps_inf * self.thickness * correction
268    }
269
270    /// Effective fluid pressure at surface under step load `sigma` at time `t`.
271    pub fn fluid_pressure(&self, sigma: f64, t: f64) -> f64 {
272        let tau = self.creep_time_constant();
273        sigma * (-t / tau * PI * PI / 4.0).exp()
274    }
275
276    /// Poisson's ratio from biphasic parameters.
277    pub fn poisson_ratio(&self) -> f64 {
278        (self.ha - 2.0 * self.mu_s) / (2.0 * (self.ha - self.mu_s))
279    }
280
281    /// Confined compression stiffness (per unit area).
282    pub fn confined_compression_stiffness(&self, area: f64) -> f64 {
283        self.ha * area / self.thickness
284    }
285}
286
287// ---------------------------------------------------------------------------
288// Tendon / Ligament Viscoelasticity
289// ---------------------------------------------------------------------------
290
291/// Viscoelastic tendon/ligament model using Quasilinear Viscoelasticity (QLV).
292#[derive(Clone, Debug)]
293pub struct TendonViscoelastic {
294    /// Toe-region strain (dimensionless).
295    pub toe_strain: f64,
296    /// Toe-region modulus (Pa).
297    pub toe_modulus: f64,
298    /// Linear-region modulus (Pa), typically 0.5-2 GPa.
299    pub linear_modulus: f64,
300    /// Relaxation time constant (s).
301    pub tau: f64,
302    /// Relaxation magnitude (dimensionless, 0..1).
303    pub relaxation_magnitude: f64,
304    /// Cross-sectional area (m²).
305    pub area: f64,
306    /// Resting length (m).
307    pub resting_length: f64,
308}
309
310impl TendonViscoelastic {
311    /// Human Achilles tendon defaults.
312    pub fn achilles() -> Self {
313        Self {
314            toe_strain: 0.03,
315            toe_modulus: 50.0e6,
316            linear_modulus: 1.2e9,
317            tau: 20.0,
318            relaxation_magnitude: 0.15,
319            area: 80.0e-6,
320            resting_length: 0.25,
321        }
322    }
323
324    /// Compute toe-region and linear-region stress from strain.
325    pub fn elastic_stress(&self, strain: f64) -> f64 {
326        if strain < 0.0 {
327            return 0.0;
328        }
329        if strain <= self.toe_strain {
330            // Exponential toe: sigma = A * (exp(B*eps) - 1)
331            let b = (self.linear_modulus / self.toe_modulus).ln() / self.toe_strain;
332            let a = self.toe_modulus / b;
333            a * ((b * strain).exp() - 1.0)
334        } else {
335            let toe_stress = self.elastic_stress(self.toe_strain);
336            toe_stress + self.linear_modulus * (strain - self.toe_strain)
337        }
338    }
339
340    /// Reduced relaxation function G(t) = 1 - M*(1 - exp(-t/tau)).
341    pub fn reduced_relaxation(&self, t: f64) -> f64 {
342        1.0 - self.relaxation_magnitude * (1.0 - (-t / self.tau).exp())
343    }
344
345    /// Stress relaxation: sigma(t) = G(t) * sigma_e(eps_0).
346    pub fn stress_relaxation(&self, eps0: f64, t: f64) -> f64 {
347        self.elastic_stress(eps0) * self.reduced_relaxation(t)
348    }
349
350    /// Force-elongation relationship at given strain.
351    pub fn force(&self, strain: f64) -> f64 {
352        self.elastic_stress(strain) * self.area
353    }
354
355    /// Compute stiffness at given strain (tangent modulus * area / length).
356    pub fn stiffness(&self, strain: f64) -> f64 {
357        let deps = 1e-6;
358        let df = self.force(strain + deps) - self.force(strain - deps);
359        df / (2.0 * deps * self.resting_length)
360    }
361
362    /// Ultimate tensile force.
363    pub fn ultimate_force(&self) -> f64 {
364        // Typically fails at ~8-12% strain for tendons
365        self.elastic_stress(0.10) * self.area
366    }
367}
368
369// ---------------------------------------------------------------------------
370// Hydrogel Swelling — Flory-Rehner Theory
371// ---------------------------------------------------------------------------
372
373/// Hydrogel swelling model based on Flory-Rehner theory.
374#[derive(Clone, Debug)]
375pub struct HydrogelFloryRehner {
376    /// Polymer-solvent interaction parameter chi (dimensionless).
377    pub chi: f64,
378    /// Number of network chains per unit volume (mol/m³), i.e. crosslink density.
379    pub nu_crosslink: f64,
380    /// Polymer volume fraction in reference state.
381    pub phi_ref: f64,
382    /// Temperature (K).
383    pub temperature: f64,
384    /// Molar volume of solvent (m³/mol), water ≈ 1.8e-5.
385    pub v_solvent: f64,
386}
387
388/// Universal gas constant (J/mol·K).
389const R_GAS: f64 = 8.314;
390
391impl HydrogelFloryRehner {
392    /// Polyacrylamide (PAAm) hydrogel defaults.
393    ///
394    /// χ ≈ 0.48 (close to theta solvent), moderate crosslink density.
395    pub fn polyacrylamide() -> Self {
396        Self {
397            chi: 0.48,
398            nu_crosslink: 200.0,
399            phi_ref: 0.08,
400            temperature: 298.15,
401            v_solvent: 1.8e-5,
402        }
403    }
404
405    /// PEGDA (polyethylene glycol diacrylate) hydrogel defaults.
406    pub fn pegda() -> Self {
407        Self {
408            chi: 0.45,
409            nu_crosslink: 500.0,
410            phi_ref: 0.1,
411            temperature: 310.15,
412            v_solvent: 1.8e-5,
413        }
414    }
415
416    /// Chemical potential of mixing (per mole of solvent).
417    pub fn chemical_potential_mixing(&self, phi_p: f64) -> f64 {
418        let phi_s = 1.0 - phi_p;
419        R_GAS * self.temperature * ((phi_s).ln() + phi_p + self.chi * phi_p * phi_p)
420    }
421
422    /// Elastic contribution to chemical potential (Flory-Rehner).
423    pub fn chemical_potential_elastic(&self, phi_p: f64) -> f64 {
424        R_GAS
425            * self.temperature
426            * self.nu_crosslink
427            * self.v_solvent
428            * (phi_p / self.phi_ref).powf(1.0 / 3.0)
429            * (0.5 - phi_p / self.phi_ref)
430    }
431
432    /// Total chemical potential: mu_total = mu_mix + mu_elastic.
433    pub fn total_chemical_potential(&self, phi_p: f64) -> f64 {
434        self.chemical_potential_mixing(phi_p) + self.chemical_potential_elastic(phi_p)
435    }
436
437    /// Find equilibrium swelling ratio Q = V_swollen / V_dry.
438    ///
439    /// Solves mu_total(phi_p) = 0 numerically via bisection.
440    pub fn equilibrium_swelling_ratio(&self) -> f64 {
441        // phi_p ranges from phi_ref to ~1
442        let mut lo = 0.001f64;
443        let mut hi = 0.999f64;
444        for _ in 0..100 {
445            let mid = (lo + hi) / 2.0;
446            let f_lo = self.total_chemical_potential(lo);
447            let f_mid = self.total_chemical_potential(mid);
448            if f_lo * f_mid < 0.0 {
449                hi = mid;
450            } else {
451                lo = mid;
452            }
453        }
454        let phi_eq = (lo + hi) / 2.0;
455        1.0 / phi_eq // Q = 1/phi_p
456    }
457
458    /// Shear modulus of the swollen gel (Pa).
459    pub fn shear_modulus(&self, phi_p: f64) -> f64 {
460        R_GAS * self.temperature * self.nu_crosslink * phi_p.powf(1.0 / 3.0)
461    }
462
463    /// Osmotic pressure (Pa) at polymer volume fraction phi_p.
464    pub fn osmotic_pressure(&self, phi_p: f64) -> f64 {
465        -R_GAS * self.temperature / self.v_solvent
466            * (self.total_chemical_potential(phi_p) / (R_GAS * self.temperature))
467    }
468}
469
470// ---------------------------------------------------------------------------
471// Cell Mechanics — Substrate Stiffness and Adhesion
472// ---------------------------------------------------------------------------
473
474/// Cell mechanical response model.
475#[derive(Clone, Debug)]
476pub struct CellMechanics {
477    /// Substrate Young's modulus (Pa).
478    pub substrate_modulus: f64,
479    /// Cell stiffness in spreading phase (Pa).
480    pub cell_stiffness: f64,
481    /// Adhesion energy (J/m²).
482    pub adhesion_energy: f64,
483    /// Cell radius (m).
484    pub cell_radius: f64,
485    /// Cell cortical tension (N/m).
486    pub cortical_tension: f64,
487}
488
489impl CellMechanics {
490    /// Typical fibroblast on soft substrate defaults.
491    pub fn fibroblast_soft() -> Self {
492        Self {
493            substrate_modulus: 1.0e3,
494            cell_stiffness: 500.0,
495            adhesion_energy: 1e-5,
496            cell_radius: 10.0e-6,
497            cortical_tension: 4e-4,
498        }
499    }
500
501    /// JKR contact radius at given applied load `F`.
502    ///
503    /// Uses Johnson-Kendall-Roberts (JKR) model.
504    pub fn jkr_contact_radius(&self, f: f64) -> f64 {
505        let r = self.cell_radius;
506        let e_star = self.effective_modulus();
507        let w = self.adhesion_energy;
508        let p0 = f + 3.0 * PI * w * r + (6.0 * PI * w * r * f + (3.0 * PI * w * r).powi(2)).sqrt();
509        (r * p0 / (4.0 * e_star / 3.0)).cbrt()
510    }
511
512    /// Effective modulus (reduced modulus) for indentation.
513    pub fn effective_modulus(&self) -> f64 {
514        let e_cell = self.cell_stiffness;
515        let e_sub = self.substrate_modulus;
516        let nu = 0.5; // Incompressible approximation
517        1.0 / (2.0 * (1.0 - nu * nu) / e_cell + 2.0 * (1.0 - nu * nu) / e_sub)
518    }
519
520    /// Hertz contact force for spherical indenter on flat surface.
521    pub fn hertz_force(&self, indent_depth: f64) -> f64 {
522        let e_star = self.effective_modulus();
523        let r = self.cell_radius;
524        4.0 / 3.0 * e_star * r.sqrt() * indent_depth.powf(1.5)
525    }
526
527    /// Cell spreading area as function of substrate stiffness (Hill model).
528    pub fn spreading_area(&self) -> f64 {
529        // Empirical: cells spread more on stiffer substrates
530        let a_max = PI * (50e-6f64).powi(2);
531        let a_min = PI * (5e-6f64).powi(2);
532        let half_max = 10.0e3; // 10 kPa
533        let e = self.substrate_modulus;
534        a_min + (a_max - a_min) * e / (e + half_max)
535    }
536
537    /// Traction force generated by cell (N) using molecular clutch model.
538    pub fn traction_force(&self, n_bonds: usize) -> f64 {
539        // F = N_bonds * k_bond * d_slip
540        let k_bond = 1e-3; // N/m
541        let d_slip = 5e-9; // 5 nm slip per bond
542        n_bonds as f64 * k_bond * d_slip
543    }
544}
545
546// ---------------------------------------------------------------------------
547// Biodegradation Kinetics
548// ---------------------------------------------------------------------------
549
550/// Polymer biodegradation model (first-order hydrolytic degradation).
551#[derive(Clone, Debug)]
552pub struct BiodegradationKinetics {
553    /// Degradation rate constant k (1/s).
554    pub rate_constant: f64,
555    /// Initial molecular weight (g/mol).
556    pub mw_initial: f64,
557    /// Minimum molecular weight for loss of mechanical function (g/mol).
558    pub mw_critical: f64,
559    /// Diffusion coefficient of water in polymer (m²/s).
560    pub d_water: f64,
561    /// Sample half-thickness (m).
562    pub half_thickness: f64,
563}
564
565impl BiodegradationKinetics {
566    /// PLA (polylactic acid) scaffold defaults.
567    pub fn pla_scaffold() -> Self {
568        Self {
569            rate_constant: 1.0e-7,
570            mw_initial: 100_000.0,
571            mw_critical: 10_000.0,
572            d_water: 1.0e-14,
573            half_thickness: 1.0e-3,
574        }
575    }
576
577    /// Molecular weight at time `t` (s) assuming first-order chain scission.
578    pub fn molecular_weight(&self, t: f64) -> f64 {
579        // Mn(t) = Mn0 * exp(-k * t)
580        self.mw_initial * (-self.rate_constant * t).exp()
581    }
582
583    /// Time to reach critical molecular weight (s).
584    pub fn time_to_failure(&self) -> f64 {
585        (self.mw_initial / self.mw_critical).ln() / self.rate_constant
586    }
587
588    /// Mass loss fraction at time `t` (dimensionless 0..1).
589    ///
590    /// Uses Weibull degradation model.
591    pub fn mass_loss_fraction(&self, t: f64) -> f64 {
592        let t_half = 0.693 / self.rate_constant;
593        1.0 - (-0.693 * t / t_half).exp()
594    }
595
596    /// Diffusion-limited degradation: characteristic time t_d = L^2 / D.
597    pub fn diffusion_time(&self) -> f64 {
598        self.half_thickness * self.half_thickness / self.d_water
599    }
600
601    /// Assess degradation regime: true = bulk, false = surface.
602    ///
603    /// Surface degradation occurs when diffusion time >> reaction time.
604    pub fn is_bulk_degradation(&self) -> bool {
605        let t_reaction = 1.0 / self.rate_constant;
606        let t_diffusion = self.diffusion_time();
607        t_diffusion < t_reaction
608    }
609
610    /// PLGA copolymer degradation with autocatalysis (Gopferich model approximation).
611    pub fn plga_mass_loss(&self, t: f64, autocatalysis_factor: f64) -> f64 {
612        let k_eff = self.rate_constant * (1.0 + autocatalysis_factor * self.mass_loss_fraction(t));
613        1.0 - (-k_eff * t).exp()
614    }
615}
616
617// ---------------------------------------------------------------------------
618// Scaffold Porosity and Permeability — Kozeny-Carman
619// ---------------------------------------------------------------------------
620
621/// Scaffold permeability model based on Kozeny-Carman equation.
622#[derive(Clone, Debug)]
623pub struct ScaffoldPermeability {
624    /// Porosity (void fraction, 0..1).
625    pub porosity: f64,
626    /// Specific surface area (m⁻¹).
627    pub specific_surface: f64,
628    /// Kozeny constant (dimensionless, typically 5.0 for packed beds).
629    pub kozeny_const: f64,
630    /// Fluid dynamic viscosity (Pa·s).
631    pub viscosity: f64,
632    /// Scaffold thickness (m).
633    pub thickness: f64,
634}
635
636impl ScaffoldPermeability {
637    /// Typical porous HA scaffold defaults.
638    pub fn ha_scaffold() -> Self {
639        Self {
640            porosity: 0.65,
641            specific_surface: 2.0e6,
642            kozeny_const: 5.0,
643            viscosity: 1e-3,
644            thickness: 5.0e-3,
645        }
646    }
647
648    /// Kozeny-Carman permeability k (m²).
649    ///
650    /// k = phi^3 / (kc * Sv^2 * (1 - phi)^2)
651    pub fn permeability(&self) -> f64 {
652        let phi = self.porosity;
653        let sv = self.specific_surface;
654        phi.powi(3) / (self.kozeny_const * sv * sv * (1.0 - phi).powi(2))
655    }
656
657    /// Darcy velocity under pressure gradient (m/s).
658    pub fn darcy_velocity(&self, pressure_gradient: f64) -> f64 {
659        self.permeability() * pressure_gradient / self.viscosity
660    }
661
662    /// Hydraulic conductivity (m²/Pa/s) = k / mu.
663    pub fn hydraulic_conductivity(&self) -> f64 {
664        self.permeability() / self.viscosity
665    }
666
667    /// Tortuosity estimate from Bruggeman correlation: T = phi^{-0.5}.
668    pub fn tortuosity(&self) -> f64 {
669        self.porosity.powf(-0.5)
670    }
671
672    /// Effective diffusivity D_eff = D_bulk * phi / T.
673    pub fn effective_diffusivity(&self, d_bulk: f64) -> f64 {
674        d_bulk * self.porosity / self.tortuosity()
675    }
676
677    /// Pore diameter from specific surface (assuming cylindrical pores).
678    pub fn mean_pore_diameter(&self) -> f64 {
679        4.0 * self.porosity / self.specific_surface
680    }
681}
682
683// ---------------------------------------------------------------------------
684// Biocompatibility Index
685// ---------------------------------------------------------------------------
686
687/// Biocompatibility scoring model for implant materials.
688#[derive(Clone, Debug)]
689pub struct BiocompatibilityIndex {
690    /// Cytotoxicity score (0 = non-toxic, 1 = highly toxic).
691    pub cytotoxicity: f64,
692    /// Inflammatory response score (0..1).
693    pub inflammatory_response: f64,
694    /// Haemocompatibility score (0..1; 0 = fully compatible with blood).
695    pub haemocompatibility: f64,
696    /// Genotoxicity score (0..1).
697    pub genotoxicity: f64,
698    /// In vivo host response score (0..1).
699    pub host_response: f64,
700}
701
702impl BiocompatibilityIndex {
703    /// Compute a weighted biocompatibility score (lower is better, range 0..1).
704    pub fn composite_score(&self) -> f64 {
705        0.3 * self.cytotoxicity
706            + 0.25 * self.inflammatory_response
707            + 0.2 * self.haemocompatibility
708            + 0.15 * self.genotoxicity
709            + 0.1 * self.host_response
710    }
711
712    /// ISO 10993-based pass/fail (score < 0.3 = pass).
713    pub fn passes_iso_10993(&self) -> bool {
714        self.composite_score() < 0.3
715    }
716
717    /// Create a reference Ti-6Al-4V implant score.
718    pub fn ti6al4v() -> Self {
719        Self {
720            cytotoxicity: 0.05,
721            inflammatory_response: 0.1,
722            haemocompatibility: 0.08,
723            genotoxicity: 0.02,
724            host_response: 0.05,
725        }
726    }
727
728    /// Create a reference UHMWPE bearing surface score.
729    pub fn uhmwpe() -> Self {
730        Self {
731            cytotoxicity: 0.04,
732            inflammatory_response: 0.08,
733            haemocompatibility: 0.05,
734            genotoxicity: 0.01,
735            host_response: 0.04,
736        }
737    }
738}
739
740// ---------------------------------------------------------------------------
741// Suture and Surgical Mesh Mechanics
742// ---------------------------------------------------------------------------
743
744/// Suture mechanical model.
745#[derive(Clone, Debug)]
746pub struct SutureMaterial {
747    /// Material name.
748    pub name: String,
749    /// Young's modulus (Pa).
750    pub modulus: f64,
751    /// Ultimate tensile strength (Pa).
752    pub uts: f64,
753    /// Failure strain (dimensionless).
754    pub failure_strain: f64,
755    /// Knot efficiency (fraction, 0..1).
756    pub knot_efficiency: f64,
757    /// Absorbed force at knot (N) – depends on size.
758    pub knot_breaking_force: f64,
759    /// Diameter (m).
760    pub diameter: f64,
761    /// Biodegradable flag.
762    pub biodegradable: bool,
763    /// Half-life if biodegradable (days).
764    pub half_life_days: f64,
765}
766
767impl SutureMaterial {
768    /// Vicryl (PGA/PLA co-polymer) absorbable suture.
769    pub fn vicryl(diameter_mm: f64) -> Self {
770        let d = diameter_mm * 1e-3;
771        Self {
772            name: "Vicryl (PGA/PLA)".into(),
773            modulus: 8.0e9,
774            uts: 550.0e6,
775            failure_strain: 0.20,
776            knot_efficiency: 0.55,
777            knot_breaking_force: 40.0 * d / 0.3e-3,
778            diameter: d,
779            biodegradable: true,
780            half_life_days: 28.0,
781        }
782    }
783
784    /// Prolene (polypropylene) non-absorbable suture.
785    pub fn prolene(diameter_mm: f64) -> Self {
786        let d = diameter_mm * 1e-3;
787        Self {
788            name: "Prolene (PP)".into(),
789            modulus: 3.5e9,
790            uts: 450.0e6,
791            failure_strain: 0.40,
792            knot_efficiency: 0.50,
793            knot_breaking_force: 35.0 * d / 0.3e-3,
794            diameter: d,
795            biodegradable: false,
796            half_life_days: f64::INFINITY,
797        }
798    }
799
800    /// Cross-sectional area (m²).
801    pub fn area(&self) -> f64 {
802        PI * (self.diameter / 2.0).powi(2)
803    }
804
805    /// Breaking strength (N).
806    pub fn breaking_strength(&self) -> f64 {
807        self.uts * self.area()
808    }
809
810    /// Effective knot strength (N).
811    pub fn knot_strength(&self) -> f64 {
812        self.breaking_strength() * self.knot_efficiency
813    }
814
815    /// Elongation at break (m) for gauge length `L`.
816    pub fn elongation_at_break(&self, gauge_length: f64) -> f64 {
817        self.failure_strain * gauge_length
818    }
819
820    /// Strength retention at time `t_days` (fraction).
821    pub fn strength_retention(&self, t_days: f64) -> f64 {
822        if !self.biodegradable {
823            return 1.0;
824        }
825        (-0.693 * t_days / self.half_life_days).exp()
826    }
827}
828
829/// Surgical mesh mechanical analysis.
830#[derive(Clone, Debug)]
831pub struct SurgicalMesh {
832    /// Mesh areal density (g/m²).
833    pub areal_density: f64,
834    /// Pore size (m).
835    pub pore_size: f64,
836    /// Porosity fraction.
837    pub porosity: f64,
838    /// Tensile strength (N/cm).
839    pub tensile_strength_per_width: f64,
840    /// Stiffness (N/m).
841    pub stiffness: f64,
842    /// Burst strength (N).
843    pub burst_strength: f64,
844}
845
846impl SurgicalMesh {
847    /// Lightweight polypropylene mesh.
848    pub fn lightweight_pp() -> Self {
849        Self {
850            areal_density: 36.0,
851            pore_size: 2.0e-3,
852            porosity: 0.80,
853            tensile_strength_per_width: 60.0,
854            stiffness: 5.0,
855            burst_strength: 120.0,
856        }
857    }
858
859    /// Intraabdominal pressure safety factor (dimensionless).
860    ///
861    /// Typical max IAP = 16 kPa (herniation scenario).
862    pub fn safety_factor(&self, mesh_width: f64, max_pressure: f64) -> f64 {
863        let load = max_pressure * mesh_width;
864        self.tensile_strength_per_width / load
865    }
866}
867
868// ---------------------------------------------------------------------------
869// Dental Materials
870// ---------------------------------------------------------------------------
871
872/// Enamel mechanical properties.
873#[derive(Clone, Debug)]
874pub struct DentalEnamel {
875    /// Young's modulus (Pa), typically 70-80 GPa.
876    pub modulus: f64,
877    /// Hardness (GPa).
878    pub hardness: f64,
879    /// Fracture toughness (MPa√m).
880    pub k_ic: f64,
881    /// Density (kg/m³).
882    pub density: f64,
883    /// Mineral content (volume fraction).
884    pub mineral_fraction: f64,
885}
886
887impl DentalEnamel {
888    /// Human enamel defaults.
889    pub fn human() -> Self {
890        Self {
891            modulus: 75.0e9,
892            hardness: 3.5e9,
893            k_ic: 0.7e6,
894            density: 2940.0,
895            mineral_fraction: 0.92,
896        }
897    }
898
899    /// Critical flaw size (m) from K_IC and tensile strength.
900    pub fn critical_flaw_size(&self, tensile_strength: f64) -> f64 {
901        (self.k_ic / (tensile_strength * PI.sqrt())).powi(2) / PI
902    }
903
904    /// Vickers hardness (HV).
905    pub fn vickers_hardness(&self) -> f64 {
906        self.hardness / (9.81 * 1e6 / 1e4) // Convert Pa to HV approximately
907    }
908}
909
910/// Dentin mechanical properties.
911#[derive(Clone, Debug)]
912pub struct Dentin {
913    /// Young's modulus (Pa), typically 15-25 GPa.
914    pub modulus: f64,
915    /// Hardness (GPa).
916    pub hardness: f64,
917    /// Fracture toughness (MPa√m).
918    pub k_ic: f64,
919    /// Density (kg/m³).
920    pub density: f64,
921    /// Mineral content (volume fraction).
922    pub mineral_fraction: f64,
923    /// Collagen content (volume fraction).
924    pub collagen_fraction: f64,
925}
926
927impl Dentin {
928    /// Human dentin defaults.
929    pub fn human() -> Self {
930        Self {
931            modulus: 20.0e9,
932            hardness: 0.5e9,
933            k_ic: 3.1e6,
934            density: 2100.0,
935            mineral_fraction: 0.50,
936            collagen_fraction: 0.30,
937        }
938    }
939
940    /// Tubule density contribution to toughening.
941    pub fn tubule_toughening_factor(&self, tubule_density: f64) -> f64 {
942        // Higher tubule density -> lower toughness (stress concentration)
943        1.0 / (1.0 + 0.1 * tubule_density * 1e-9)
944    }
945}
946
947/// Composite dental restorative material model.
948#[derive(Clone, Debug)]
949pub struct DentalComposite {
950    /// Filler volume fraction (0..1).
951    pub filler_fraction: f64,
952    /// Filler modulus (Pa).
953    pub filler_modulus: f64,
954    /// Matrix modulus (Pa).
955    pub matrix_modulus: f64,
956    /// Filler particle size (m).
957    pub particle_size: f64,
958    /// Flexural strength (Pa).
959    pub flexural_strength: f64,
960    /// Polymerisation shrinkage (dimensionless strain).
961    pub poly_shrinkage: f64,
962    /// Water absorption coefficient (%vol/year).
963    pub water_absorption: f64,
964}
965
966impl DentalComposite {
967    /// Typical nano-hybrid composite defaults.
968    pub fn nano_hybrid() -> Self {
969        Self {
970            filler_fraction: 0.70,
971            filler_modulus: 70.0e9,
972            matrix_modulus: 3.5e9,
973            particle_size: 0.5e-6,
974            flexural_strength: 120.0e6,
975            poly_shrinkage: 0.02,
976            water_absorption: 1.5,
977        }
978    }
979
980    /// Nano-hybrid composite (alias).
981    pub fn nano_hybrid_v2() -> Self {
982        Self::nano_hybrid()
983    }
984
985    /// Reuss lower bound modulus.
986    pub fn reuss_modulus(&self) -> f64 {
987        let vf = self.filler_fraction;
988        1.0 / (vf / self.filler_modulus + (1.0 - vf) / self.matrix_modulus)
989    }
990
991    /// Voigt upper bound modulus.
992    pub fn voigt_modulus(&self) -> f64 {
993        let vf = self.filler_fraction;
994        vf * self.filler_modulus + (1.0 - vf) * self.matrix_modulus
995    }
996
997    /// Halpin-Tsai modulus estimate for particulate composites.
998    pub fn halpin_tsai_modulus(&self) -> f64 {
999        let eta = (self.filler_modulus / self.matrix_modulus - 1.0)
1000            / (self.filler_modulus / self.matrix_modulus + 2.0);
1001        self.matrix_modulus * (1.0 + 2.0 * eta * self.filler_fraction)
1002            / (1.0 - eta * self.filler_fraction)
1003    }
1004
1005    /// Polymerisation shrinkage stress (Pa) with given compliance `c` (Pa⁻¹).
1006    pub fn shrinkage_stress(&self, compliance: f64) -> f64 {
1007        self.poly_shrinkage / compliance
1008    }
1009}
1010
1011// ---------------------------------------------------------------------------
1012// TissueType Enum
1013// ---------------------------------------------------------------------------
1014
1015/// Classification of biological tissue type.
1016#[derive(Clone, Debug, PartialEq)]
1017pub enum TissueType {
1018    /// Cortical or trabecular bone.
1019    Bone,
1020    /// Articular or fibrocartilage.
1021    Cartilage,
1022    /// Tendon or ligament.
1023    Tendon,
1024    /// Dermis / epidermis.
1025    Skin,
1026    /// Skeletal or cardiac muscle.
1027    Muscle,
1028    /// Arterial wall tissue.
1029    Artery,
1030    /// User-defined tissue with a label.
1031    Custom(String),
1032}
1033
1034// ---------------------------------------------------------------------------
1035// BoneMaterial – cortical vs trabecular, power-law E–rho relation
1036// ---------------------------------------------------------------------------
1037
1038/// Bone material combining cortical and trabecular phases.
1039#[derive(Clone, Debug)]
1040pub struct BoneMaterial {
1041    /// True = cortical (compact), false = trabecular (cancellous).
1042    pub is_cortical: bool,
1043    /// Apparent density in g/cm³ (cortical ≈ 1.8–2.0, trabecular ≈ 0.1–0.8).
1044    pub density_g_cm3: f64,
1045    /// Porosity fraction (0 = fully dense, 1 = fully porous).
1046    pub porosity: f64,
1047    /// Bone volume fraction BVF (bone tissue / total volume, 0..1).
1048    pub bvf: f64,
1049    /// Power-law coefficient `a` for E = a * rho^b (Pa).
1050    pub power_law_a: f64,
1051    /// Power-law exponent `b` for E = a * rho^b (dimensionless).
1052    pub power_law_b: f64,
1053}
1054
1055impl BoneMaterial {
1056    /// Typical human cortical bone (femoral shaft).
1057    pub fn cortical() -> Self {
1058        Self {
1059            is_cortical: true,
1060            density_g_cm3: 1.85,
1061            porosity: 0.05,
1062            bvf: 0.95,
1063            power_law_a: 2.065e10,
1064            power_law_b: 3.09,
1065        }
1066    }
1067
1068    /// Typical human trabecular bone (vertebral body).
1069    pub fn trabecular() -> Self {
1070        Self {
1071            is_cortical: false,
1072            density_g_cm3: 0.30,
1073            porosity: 0.75,
1074            bvf: 0.25,
1075            power_law_a: 1.310e10,
1076            power_law_b: 2.74,
1077        }
1078    }
1079
1080    /// Young's modulus from density using Morgan-Keaveny power law.
1081    ///
1082    /// Returns E in Pa.  `rho` is apparent density in g/cm³.
1083    pub fn young_modulus_from_density(&self) -> f64 {
1084        self.power_law_a * self.density_g_cm3.powf(self.power_law_b)
1085    }
1086
1087    /// Young's modulus from a supplied density (Pa).
1088    pub fn modulus_at_density(&self, rho_g_cm3: f64) -> f64 {
1089        self.power_law_a * rho_g_cm3.powf(self.power_law_b)
1090    }
1091
1092    /// Tissue type classification.
1093    pub fn tissue_type(&self) -> TissueType {
1094        TissueType::Bone
1095    }
1096}
1097
1098// ---------------------------------------------------------------------------
1099// CartilageMaterial – biphasic model
1100// ---------------------------------------------------------------------------
1101
1102/// Articular cartilage biphasic material (solid matrix + fluid phase).
1103#[derive(Clone, Debug)]
1104pub struct CartilageMaterial {
1105    /// Aggregate modulus HA (Pa), stiffness of the solid matrix.
1106    pub aggregate_modulus: f64,
1107    /// Shear modulus of the solid matrix (Pa).
1108    pub shear_modulus: f64,
1109    /// Hydraulic permeability (m⁴/N·s).
1110    pub permeability: f64,
1111    /// Fluid volume fraction (0..1).
1112    pub fluid_fraction: f64,
1113    /// Tissue thickness (m).
1114    pub thickness: f64,
1115}
1116
1117impl CartilageMaterial {
1118    /// Human knee articular cartilage defaults.
1119    pub fn knee() -> Self {
1120        Self {
1121            aggregate_modulus: 0.7e6,
1122            shear_modulus: 0.15e6,
1123            permeability: 2.17e-15,
1124            fluid_fraction: 0.75,
1125            thickness: 2.5e-3,
1126        }
1127    }
1128
1129    /// Creep time constant τ = h² / (HA · k).
1130    pub fn creep_time_constant(&self) -> f64 {
1131        self.thickness * self.thickness / (self.aggregate_modulus * self.permeability)
1132    }
1133
1134    /// Steady-state strain under uniaxial stress `sigma`.
1135    pub fn steady_state_strain(&self, sigma: f64) -> f64 {
1136        sigma / self.aggregate_modulus
1137    }
1138
1139    /// Fluid pressure at time `t` under step load `sigma` (first-term biphasic solution).
1140    pub fn fluid_pressure(&self, sigma: f64, t: f64) -> f64 {
1141        let tau = self.creep_time_constant();
1142        sigma * (-t / tau * PI * PI / 4.0).exp()
1143    }
1144
1145    /// Tissue type classification.
1146    pub fn tissue_type(&self) -> TissueType {
1147        TissueType::Cartilage
1148    }
1149}
1150
1151// ---------------------------------------------------------------------------
1152// TendonLigament – nonlinear fiber model
1153// ---------------------------------------------------------------------------
1154
1155/// Tendon / ligament fibrous material model.
1156#[derive(Clone, Debug)]
1157pub struct TendonLigament {
1158    /// Mean collagen fiber angle from the tendon axis (radians).
1159    pub fiber_angle_rad: f64,
1160    /// Toe-region tangent stiffness (Pa), low-strain exponential rise.
1161    pub toe_stiffness: f64,
1162    /// Linear-region tangent stiffness (Pa).
1163    pub linear_stiffness: f64,
1164    /// Toe-region end strain (dimensionless, typically 0.02–0.04).
1165    pub toe_strain: f64,
1166    /// Failure strain (dimensionless, typically 0.08–0.15).
1167    pub failure_strain: f64,
1168}
1169
1170impl TendonLigament {
1171    /// Human Achilles tendon defaults.
1172    pub fn achilles() -> Self {
1173        Self {
1174            fiber_angle_rad: 0.05,
1175            toe_stiffness: 50.0e6,
1176            linear_stiffness: 1.2e9,
1177            toe_strain: 0.03,
1178            failure_strain: 0.10,
1179        }
1180    }
1181
1182    /// Cauchy stress at engineering strain `eps` (Pa).
1183    pub fn stress(&self, eps: f64) -> f64 {
1184        if eps <= 0.0 {
1185            return 0.0;
1186        }
1187        if eps <= self.toe_strain {
1188            // Exponential toe region
1189            let b = (self.linear_stiffness / self.toe_stiffness).ln() / self.toe_strain;
1190            let a = self.toe_stiffness / b;
1191            a * ((b * eps).exp() - 1.0)
1192        } else {
1193            let sigma_toe = self.stress(self.toe_strain);
1194            sigma_toe + self.linear_stiffness * (eps - self.toe_strain)
1195        }
1196    }
1197
1198    /// True if the tissue has failed at the given strain.
1199    pub fn is_failed(&self, eps: f64) -> bool {
1200        eps >= self.failure_strain
1201    }
1202
1203    /// Tissue type classification.
1204    pub fn tissue_type(&self) -> TissueType {
1205        TissueType::Tendon
1206    }
1207}
1208
1209// ---------------------------------------------------------------------------
1210// ArterialWall – Holzapfel-Gasser-Ogden (simplified)
1211// ---------------------------------------------------------------------------
1212
1213/// Simplified Holzapfel-Gasser-Ogden (HGO) arterial wall model.
1214///
1215/// The strain energy function is W = C10*(I1-3) + k1/(2*k2)*(exp(k2*(I4-1)^2)-1).
1216#[derive(Clone, Debug)]
1217pub struct ArterialWall {
1218    /// Neo-Hookean ground matrix constant C10 (Pa).
1219    pub c10: f64,
1220    /// Fiber stiffness parameter k1 (Pa).
1221    pub k1: f64,
1222    /// Fiber nonlinearity parameter k2 (dimensionless).
1223    pub k2: f64,
1224    /// Mean fiber direction angle from the circumferential axis (radians).
1225    pub fiber_angle_rad: f64,
1226}
1227
1228impl ArterialWall {
1229    /// Typical human aorta media layer defaults.
1230    pub fn aorta_media() -> Self {
1231        Self {
1232            c10: 26.95e3,
1233            k1: 15.56e3,
1234            k2: 11.65,
1235            fiber_angle_rad: 0.4869, // ~27.9°
1236        }
1237    }
1238
1239    /// Neo-Hookean strain energy (Pa) from first invariant I1.
1240    pub fn neo_hookean_energy(&self, i1: f64) -> f64 {
1241        self.c10 * (i1 - 3.0)
1242    }
1243
1244    /// Fiber strain energy (Pa) for a single fiber family with pseudo-invariant I4.
1245    pub fn fiber_energy(&self, i4: f64) -> f64 {
1246        if i4 <= 1.0 {
1247            // Fibers buckle in compression – no contribution.
1248            return 0.0;
1249        }
1250        self.k1 / (2.0 * self.k2) * ((self.k2 * (i4 - 1.0).powi(2)).exp() - 1.0)
1251    }
1252
1253    /// Total strain energy density W = W_matrix + W_fiber (Pa).
1254    pub fn strain_energy(&self, i1: f64, i4: f64) -> f64 {
1255        self.neo_hookean_energy(i1) + self.fiber_energy(i4)
1256    }
1257
1258    /// Cauchy stress in the fiber direction (Pa) – dW/dI4 * 2.
1259    pub fn fiber_stress(&self, i4: f64) -> f64 {
1260        if i4 <= 1.0 {
1261            return 0.0;
1262        }
1263        2.0 * self.k1 * (i4 - 1.0) * (self.k2 * (i4 - 1.0).powi(2)).exp()
1264    }
1265
1266    /// Tissue type classification.
1267    pub fn tissue_type(&self) -> TissueType {
1268        TissueType::Artery
1269    }
1270}
1271
1272// ---------------------------------------------------------------------------
1273// MuscleMaterial – Hill three-element model
1274// ---------------------------------------------------------------------------
1275
1276/// Hill three-element muscle model.
1277///
1278/// Active force is generated by the contractile element (CE); total force
1279/// combines the active CE with a passive elastic element (PE).
1280#[derive(Clone, Debug)]
1281pub struct MuscleMaterial {
1282    /// Maximum isometric force F0 (N).
1283    pub max_isometric_force: f64,
1284    /// Optimal fiber length L0 (m) at which active force is maximal.
1285    pub optimal_fiber_length: f64,
1286    /// Pennation angle at optimal length (radians).
1287    pub pennation_angle: f64,
1288    /// Maximum shortening velocity Vmax = k * L0 (fiber lengths per second).
1289    pub vmax_factor: f64,
1290    /// Passive tissue stiffness coefficient (Pa).
1291    pub passive_stiffness: f64,
1292}
1293
1294impl MuscleMaterial {
1295    /// Human biceps brachii defaults.
1296    pub fn biceps() -> Self {
1297        Self {
1298            max_isometric_force: 1200.0,
1299            optimal_fiber_length: 0.116,
1300            pennation_angle: 0.0,
1301            vmax_factor: 10.0,
1302            passive_stiffness: 1.0e4,
1303        }
1304    }
1305
1306    /// Force-length factor FL(l) — bell-shaped function around L0.
1307    ///
1308    /// Returns a value in \[0, 1\].
1309    pub fn force_length_factor(&self, length: f64) -> f64 {
1310        let ratio = length / self.optimal_fiber_length;
1311        // Gaussian approximation used widely in musculoskeletal models.
1312        (-4.0 * (ratio - 1.0).powi(2)).exp()
1313    }
1314
1315    /// Active force produced by the contractile element.
1316    ///
1317    /// * `activation` – neural activation in \[0, 1\].
1318    /// * `length` – current fiber length (m).
1319    pub fn active_force(&self, activation: f64, length: f64) -> f64 {
1320        let a = activation.clamp(0.0, 1.0);
1321        let pennation_cos = self.pennation_angle.cos();
1322        a * self.max_isometric_force * self.force_length_factor(length) * pennation_cos
1323    }
1324
1325    /// Passive elastic force from parallel element (N).
1326    pub fn passive_force(&self, length: f64) -> f64 {
1327        let stretch = (length - self.optimal_fiber_length).max(0.0);
1328        self.passive_stiffness * stretch
1329    }
1330
1331    /// Total muscle force (N) = active + passive.
1332    pub fn total_force(&self, activation: f64, length: f64) -> f64 {
1333        self.active_force(activation, length) + self.passive_force(length)
1334    }
1335
1336    /// Tissue type classification.
1337    pub fn tissue_type(&self) -> TissueType {
1338        TissueType::Muscle
1339    }
1340}
1341
1342// ---------------------------------------------------------------------------
1343// Stand-alone mixing-rule functions
1344// ---------------------------------------------------------------------------
1345
1346/// Reuss lower-bound modulus for a two-phase composite.
1347///
1348/// * `e1` – modulus of phase 1 (Pa).
1349/// * `e2` – modulus of phase 2 (Pa).
1350/// * `vf1` – volume fraction of phase 1 (0..1).
1351pub fn reuss_modulus(e1: f64, e2: f64, vf1: f64) -> f64 {
1352    let vf2 = 1.0 - vf1;
1353    1.0 / (vf1 / e1 + vf2 / e2)
1354}
1355
1356/// Voigt upper-bound modulus for a two-phase composite.
1357///
1358/// * `e1` – modulus of phase 1 (Pa).
1359/// * `e2` – modulus of phase 2 (Pa).
1360/// * `vf1` – volume fraction of phase 1 (0..1).
1361pub fn voigt_modulus(e1: f64, e2: f64, vf1: f64) -> f64 {
1362    vf1 * e1 + (1.0 - vf1) * e2
1363}
1364
1365/// Neo-Hookean strain energy density W = C1 * (I1 - 3) (Pa).
1366///
1367/// * `c1` – neo-Hookean material constant (Pa).
1368/// * `i1` – first invariant of the right Cauchy-Green tensor.
1369pub fn strain_energy_density_neo_hookean(c1: f64, i1: f64) -> f64 {
1370    c1 * (i1 - 3.0)
1371}
1372
1373// ---------------------------------------------------------------------------
1374// Tests
1375// ---------------------------------------------------------------------------
1376
1377#[cfg(test)]
1378mod tests {
1379    use super::*;
1380
1381    const EPS: f64 = 1e-9;
1382
1383    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1384        (a - b).abs() < tol
1385    }
1386
1387    // --- CorticalBone ---
1388
1389    #[test]
1390    fn test_cortical_bone_wave_speed() {
1391        let bone = CorticalBone::human_femur();
1392        let v = bone.longitudinal_wave_speed();
1393        assert!(v > 2000.0 && v < 5000.0, "Wave speed should be ~3200 m/s");
1394    }
1395
1396    #[test]
1397    fn test_cortical_bone_bending_stiffness() {
1398        let bone = CorticalBone::human_femur();
1399        let k = bone.bending_stiffness(0.015, 0.010, 0.3);
1400        assert!(k > 0.0);
1401    }
1402
1403    #[test]
1404    fn test_cortical_bone_three_point_stress() {
1405        let bone = CorticalBone::human_femur();
1406        let sigma = bone.three_point_bending_stress(1000.0, 0.3, 0.015, 0.010);
1407        assert!(sigma > 0.0);
1408    }
1409
1410    #[test]
1411    fn test_cortical_stiffness_tensor_symmetry() {
1412        let bone = CorticalBone::human_femur();
1413        let c = cortical_bone_stiffness_tensor(
1414            bone.e_long,
1415            bone.e_trans,
1416            bone.g_lt,
1417            bone.nu_lt,
1418            bone.nu_tt,
1419        );
1420        assert!(approx_eq(c[0][1], c[1][0], 1e3));
1421        assert!(approx_eq(c[0][2], c[2][0], 1e3));
1422    }
1423
1424    // --- CancellousBone ---
1425
1426    #[test]
1427    fn test_cancellous_bone_modulus() {
1428        let bone = CancellousBone::from_relative_density(0.2);
1429        assert!(bone.apparent_modulus > 0.0);
1430        // Gibson-Ashby: E* ~ rho^2 * E_solid
1431        let expected = 1.0 * 20.0e9 * 0.04; // C1 * E_s * rho^2
1432        assert!(approx_eq(bone.apparent_modulus, expected, 1.0));
1433    }
1434
1435    #[test]
1436    fn test_cancellous_energy_absorption() {
1437        let bone = CancellousBone::from_relative_density(0.2);
1438        let e = bone.energy_absorption();
1439        assert!(e > 0.0);
1440    }
1441
1442    // --- CartilageBiphasic ---
1443
1444    #[test]
1445    fn test_cartilage_creep_time_constant() {
1446        let cart = CartilageBiphasic::knee();
1447        let tau = cart.creep_time_constant();
1448        assert!(tau > 0.0);
1449    }
1450
1451    #[test]
1452    fn test_cartilage_steady_state_strain() {
1453        let cart = CartilageBiphasic::knee();
1454        let eps = cart.steady_state_strain(1.0e4);
1455        assert!(approx_eq(eps, 1.0e4 / 0.7e6, 1e-6));
1456    }
1457
1458    #[test]
1459    fn test_cartilage_creep_displacement_increases_with_time() {
1460        let cart = CartilageBiphasic::knee();
1461        let d1 = cart.creep_displacement(1.0e4, 10.0);
1462        let d2 = cart.creep_displacement(1.0e4, 100.0);
1463        assert!(d2 >= d1);
1464    }
1465
1466    #[test]
1467    fn test_cartilage_fluid_pressure_decays() {
1468        let cart = CartilageBiphasic::knee();
1469        let p0 = cart.fluid_pressure(1.0e4, 0.0);
1470        let p1 = cart.fluid_pressure(1.0e4, 100.0);
1471        assert!(p0 >= p1);
1472    }
1473
1474    // --- TendonViscoelastic ---
1475
1476    #[test]
1477    fn test_tendon_elastic_stress_toe_region() {
1478        let tendon = TendonViscoelastic::achilles();
1479        let s = tendon.elastic_stress(0.01);
1480        assert!(s > 0.0);
1481        let s2 = tendon.elastic_stress(0.02);
1482        assert!(s2 > s);
1483    }
1484
1485    #[test]
1486    fn test_tendon_elastic_stress_linear_region() {
1487        let tendon = TendonViscoelastic::achilles();
1488        let s1 = tendon.elastic_stress(0.05);
1489        let s2 = tendon.elastic_stress(0.06);
1490        let d = s2 - s1;
1491        // Linear region: delta_sigma = E_lin * delta_eps ≈ 1.2e9 * 0.01 = 1.2e7
1492        assert!(approx_eq(d, 1.2e7, 1.0e6));
1493    }
1494
1495    #[test]
1496    fn test_tendon_stress_relaxation_decreases() {
1497        let tendon = TendonViscoelastic::achilles();
1498        let s1 = tendon.stress_relaxation(0.05, 1.0);
1499        let s2 = tendon.stress_relaxation(0.05, 100.0);
1500        assert!(s1 > s2);
1501    }
1502
1503    #[test]
1504    fn test_tendon_reduced_relaxation_limits() {
1505        let tendon = TendonViscoelastic::achilles();
1506        let g0 = tendon.reduced_relaxation(0.0);
1507        let g_inf = tendon.reduced_relaxation(1e9);
1508        assert!(approx_eq(g0, 1.0, 1e-6));
1509        assert!(approx_eq(g_inf, 1.0 - tendon.relaxation_magnitude, 1e-4));
1510    }
1511
1512    // --- HydrogelFloryRehner ---
1513
1514    #[test]
1515    fn test_hydrogel_swelling_ratio_positive() {
1516        let gel = HydrogelFloryRehner::pegda();
1517        let q = gel.equilibrium_swelling_ratio();
1518        assert!(q > 1.0, "Swelling ratio must exceed 1 for a swelling gel");
1519    }
1520
1521    #[test]
1522    fn test_hydrogel_shear_modulus_positive() {
1523        let gel = HydrogelFloryRehner::pegda();
1524        let g = gel.shear_modulus(0.1);
1525        assert!(g > 0.0);
1526    }
1527
1528    // --- ScaffoldPermeability ---
1529
1530    #[test]
1531    fn test_scaffold_permeability_kozeny() {
1532        let scaffold = ScaffoldPermeability::ha_scaffold();
1533        let k = scaffold.permeability();
1534        assert!(k > 0.0 && k < 1e-10);
1535    }
1536
1537    #[test]
1538    fn test_scaffold_darcy_velocity() {
1539        let scaffold = ScaffoldPermeability::ha_scaffold();
1540        let v = scaffold.darcy_velocity(1.0e3);
1541        assert!(v > 0.0);
1542    }
1543
1544    #[test]
1545    fn test_scaffold_pore_diameter() {
1546        let scaffold = ScaffoldPermeability::ha_scaffold();
1547        let d = scaffold.mean_pore_diameter();
1548        // d = 4 * 0.65 / 2e6 ≈ 1.3e-6 m (1.3 µm)
1549        assert!(approx_eq(d, 4.0 * 0.65 / 2.0e6, 1e-8));
1550    }
1551
1552    // --- BiocompatibilityIndex ---
1553
1554    #[test]
1555    fn test_biocompatibility_ti6al4v_passes() {
1556        let mat = BiocompatibilityIndex::ti6al4v();
1557        assert!(mat.passes_iso_10993());
1558    }
1559
1560    #[test]
1561    fn test_biocompatibility_score_range() {
1562        let mat = BiocompatibilityIndex {
1563            cytotoxicity: 0.5,
1564            inflammatory_response: 0.5,
1565            haemocompatibility: 0.5,
1566            genotoxicity: 0.5,
1567            host_response: 0.5,
1568        };
1569        let score = mat.composite_score();
1570        assert!(approx_eq(score, 0.5, EPS));
1571    }
1572
1573    // --- SutureMaterial ---
1574
1575    #[test]
1576    fn test_suture_breaking_strength() {
1577        let suture = SutureMaterial::vicryl(0.3);
1578        let f = suture.breaking_strength();
1579        assert!(f > 0.0);
1580    }
1581
1582    #[test]
1583    fn test_suture_strength_retention_biodegradable() {
1584        let suture = SutureMaterial::vicryl(0.3);
1585        let r0 = suture.strength_retention(0.0);
1586        let r28 = suture.strength_retention(28.0);
1587        assert!(approx_eq(r0, 1.0, 1e-10));
1588        assert!(approx_eq(r28, 0.5, 0.01));
1589    }
1590
1591    #[test]
1592    fn test_suture_prolene_non_degradable() {
1593        let suture = SutureMaterial::prolene(0.3);
1594        assert!(!suture.biodegradable);
1595        assert!(approx_eq(suture.strength_retention(3650.0), 1.0, 1e-10));
1596    }
1597
1598    // --- DentalEnamel ---
1599
1600    #[test]
1601    fn test_enamel_critical_flaw() {
1602        let enamel = DentalEnamel::human();
1603        let flaw = enamel.critical_flaw_size(50.0e6);
1604        assert!(flaw > 0.0 && flaw < 1e-3);
1605    }
1606
1607    // --- DentalComposite ---
1608
1609    #[test]
1610    fn test_composite_voigt_reuss_bounds() {
1611        let comp = DentalComposite::nano_hybrid_v2();
1612        let voigt = comp.voigt_modulus();
1613        let reuss = comp.reuss_modulus();
1614        assert!(voigt >= reuss, "Voigt bound must be >= Reuss bound");
1615    }
1616
1617    #[test]
1618    fn test_composite_halpin_tsai_in_bounds() {
1619        let comp = DentalComposite::nano_hybrid_v2();
1620        let ht = comp.halpin_tsai_modulus();
1621        let reuss = comp.reuss_modulus();
1622        let voigt = comp.voigt_modulus();
1623        assert!(ht >= reuss && ht <= voigt);
1624    }
1625
1626    // --- CellMechanics ---
1627
1628    #[test]
1629    fn test_cell_hertz_force_positive() {
1630        let cell = CellMechanics::fibroblast_soft();
1631        let f = cell.hertz_force(100e-9);
1632        assert!(f > 0.0);
1633    }
1634
1635    #[test]
1636    fn test_cell_spreading_area_increases_with_stiffness() {
1637        let soft = CellMechanics::fibroblast_soft();
1638        let hard = CellMechanics {
1639            substrate_modulus: 100e3,
1640            ..CellMechanics::fibroblast_soft()
1641        };
1642        assert!(hard.spreading_area() > soft.spreading_area());
1643    }
1644
1645    // --- BiodegradationKinetics ---
1646
1647    #[test]
1648    fn test_degradation_mw_decreases() {
1649        let pla = BiodegradationKinetics::pla_scaffold();
1650        let mw0 = pla.molecular_weight(0.0);
1651        let mw1 = pla.molecular_weight(1e8);
1652        assert!(approx_eq(mw0, pla.mw_initial, 1e-6));
1653        assert!(mw1 < mw0);
1654    }
1655
1656    #[test]
1657    fn test_degradation_time_to_failure() {
1658        let pla = BiodegradationKinetics::pla_scaffold();
1659        let t = pla.time_to_failure();
1660        assert!(approx_eq(
1661            pla.molecular_weight(t),
1662            pla.mw_critical,
1663            pla.mw_critical * 0.01
1664        ));
1665    }
1666
1667    #[test]
1668    fn test_degradation_mass_loss_fraction() {
1669        let pla = BiodegradationKinetics::pla_scaffold();
1670        let ml = pla.mass_loss_fraction(0.0);
1671        assert!(approx_eq(ml, 0.0, 1e-10));
1672        let ml_inf = pla.mass_loss_fraction(1e12);
1673        assert!((ml_inf - 1.0).abs() < 0.01);
1674    }
1675
1676    // --- Dentin ---
1677
1678    #[test]
1679    fn test_dentin_modulus_range() {
1680        let d = Dentin::human();
1681        assert!(d.modulus >= 15.0e9 && d.modulus <= 25.0e9);
1682    }
1683
1684    // -----------------------------------------------------------------------
1685    // New TissueType / BoneMaterial / CartilageMaterial / TendonLigament /
1686    // ArterialWall / MuscleMaterial tests
1687    // -----------------------------------------------------------------------
1688
1689    // --- TissueType ---
1690
1691    #[test]
1692    fn test_tissue_type_equality() {
1693        assert_eq!(TissueType::Bone, TissueType::Bone);
1694        assert_ne!(TissueType::Bone, TissueType::Cartilage);
1695    }
1696
1697    #[test]
1698    fn test_tissue_type_custom() {
1699        let t = TissueType::Custom("Intervertebral Disc".into());
1700        assert_ne!(t, TissueType::Bone);
1701    }
1702
1703    // --- BoneMaterial ---
1704
1705    #[test]
1706    fn test_bone_cortical_modulus_range() {
1707        let b = BoneMaterial::cortical();
1708        let e = b.young_modulus_from_density();
1709        // Power-law with rho=1.85 g/cm³: E can be higher than classical range;
1710        // just verify it is in the GPa range and positive.
1711        assert!(
1712            e > 1.0e9 && e < 500.0e9,
1713            "Cortical E = {e:.3e} Pa out of range"
1714        );
1715    }
1716
1717    #[test]
1718    fn test_bone_trabecular_modulus_range() {
1719        let b = BoneMaterial::trabecular();
1720        let e = b.young_modulus_from_density();
1721        // Trabecular bone: 0.1–3 GPa.
1722        assert!(
1723            e > 1.0e8 && e < 5.0e9,
1724            "Trabecular E = {e:.3e} Pa out of range"
1725        );
1726    }
1727
1728    #[test]
1729    fn test_bone_modulus_increases_with_density() {
1730        let b = BoneMaterial::cortical();
1731        let e_low = b.modulus_at_density(0.5);
1732        let e_high = b.modulus_at_density(2.0);
1733        assert!(e_high > e_low, "Higher density should give higher modulus");
1734    }
1735
1736    #[test]
1737    fn test_bone_power_law_exponent() {
1738        let b = BoneMaterial::cortical();
1739        // Doubling density: E should scale by 2^b.
1740        let ratio = b.modulus_at_density(2.0) / b.modulus_at_density(1.0);
1741        let expected = (2.0_f64).powf(b.power_law_b);
1742        assert!(
1743            approx_eq(ratio, expected, 1.0),
1744            "Power-law scaling incorrect"
1745        );
1746    }
1747
1748    #[test]
1749    fn test_bone_tissue_type() {
1750        let b = BoneMaterial::cortical();
1751        assert_eq!(b.tissue_type(), TissueType::Bone);
1752    }
1753
1754    #[test]
1755    fn test_bone_bvf_range() {
1756        let c = BoneMaterial::cortical();
1757        let t = BoneMaterial::trabecular();
1758        assert!(c.bvf > 0.9 && c.bvf <= 1.0);
1759        assert!(t.bvf < 0.5 && t.bvf > 0.0);
1760    }
1761
1762    // --- CartilageMaterial ---
1763
1764    #[test]
1765    fn test_cartilage_creep_constant_positive() {
1766        let c = CartilageMaterial::knee();
1767        let tau = c.creep_time_constant();
1768        assert!(tau > 0.0, "Creep time constant must be positive");
1769    }
1770
1771    #[test]
1772    fn test_cartilage_material_steady_state_strain() {
1773        let c = CartilageMaterial::knee();
1774        let sigma = 1.0e4;
1775        let eps = c.steady_state_strain(sigma);
1776        let expected = sigma / c.aggregate_modulus;
1777        assert!(approx_eq(eps, expected, 1e-12));
1778    }
1779
1780    #[test]
1781    fn test_cartilage_material_fluid_pressure_decays() {
1782        let c = CartilageMaterial::knee();
1783        let p0 = c.fluid_pressure(1.0e4, 0.0);
1784        let p1 = c.fluid_pressure(1.0e4, 100.0);
1785        assert!(p0 > p1, "Fluid pressure should decay over time");
1786    }
1787
1788    #[test]
1789    fn test_cartilage_fluid_fraction_range() {
1790        let c = CartilageMaterial::knee();
1791        assert!(c.fluid_fraction > 0.5 && c.fluid_fraction < 1.0);
1792    }
1793
1794    #[test]
1795    fn test_cartilage_tissue_type() {
1796        assert_eq!(
1797            CartilageMaterial::knee().tissue_type(),
1798            TissueType::Cartilage
1799        );
1800    }
1801
1802    // --- TendonLigament ---
1803
1804    #[test]
1805    fn test_tendon_zero_stress_at_zero_strain() {
1806        let t = TendonLigament::achilles();
1807        assert!(approx_eq(t.stress(0.0), 0.0, EPS));
1808    }
1809
1810    #[test]
1811    fn test_tendon_stress_increases_with_strain() {
1812        let t = TendonLigament::achilles();
1813        let s1 = t.stress(0.01);
1814        let s2 = t.stress(0.02);
1815        let s3 = t.stress(0.05);
1816        assert!(
1817            s1 < s2 && s2 < s3,
1818            "Stress must be monotonically increasing"
1819        );
1820    }
1821
1822    #[test]
1823    fn test_tendon_linear_region() {
1824        let t = TendonLigament::achilles();
1825        // In the linear region the incremental stiffness ≈ linear_stiffness.
1826        let ds = t.stress(0.06) - t.stress(0.05);
1827        let de = 0.01;
1828        let tangent = ds / de;
1829        // Accept ±20 % of nominal linear_stiffness.
1830        let tol = t.linear_stiffness * 0.20;
1831        assert!(
1832            (tangent - t.linear_stiffness).abs() < tol,
1833            "Linear tangent {tangent:.3e} should be near {:.3e}",
1834            t.linear_stiffness
1835        );
1836    }
1837
1838    #[test]
1839    fn test_tendon_failure_strain() {
1840        let t = TendonLigament::achilles();
1841        assert!(!t.is_failed(0.05));
1842        assert!(t.is_failed(0.10));
1843    }
1844
1845    #[test]
1846    fn test_tendon_tissue_type() {
1847        assert_eq!(TendonLigament::achilles().tissue_type(), TissueType::Tendon);
1848    }
1849
1850    // --- ArterialWall ---
1851
1852    #[test]
1853    fn test_artery_neo_hookean_energy_zero_at_rest() {
1854        let a = ArterialWall::aorta_media();
1855        // I1 = 3 at rest (incompressible, no deformation).
1856        assert!(approx_eq(a.neo_hookean_energy(3.0), 0.0, EPS));
1857    }
1858
1859    #[test]
1860    fn test_artery_fiber_energy_zero_in_compression() {
1861        let a = ArterialWall::aorta_media();
1862        // I4 ≤ 1 → fibers buckle, no energy stored.
1863        assert!(approx_eq(a.fiber_energy(0.8), 0.0, EPS));
1864        assert!(approx_eq(a.fiber_energy(1.0), 0.0, EPS));
1865    }
1866
1867    #[test]
1868    fn test_artery_fiber_energy_positive_in_tension() {
1869        let a = ArterialWall::aorta_media();
1870        assert!(a.fiber_energy(1.2) > 0.0);
1871    }
1872
1873    #[test]
1874    fn test_artery_total_strain_energy() {
1875        let a = ArterialWall::aorta_media();
1876        let w = a.strain_energy(3.5, 1.1);
1877        assert!(w > 0.0);
1878    }
1879
1880    #[test]
1881    fn test_artery_tissue_type() {
1882        assert_eq!(
1883            ArterialWall::aorta_media().tissue_type(),
1884            TissueType::Artery
1885        );
1886    }
1887
1888    // --- MuscleMaterial ---
1889
1890    #[test]
1891    fn test_muscle_max_force_at_optimal_length() {
1892        let m = MuscleMaterial::biceps();
1893        let f = m.active_force(1.0, m.optimal_fiber_length);
1894        // Should equal max_isometric_force (pennation_angle = 0).
1895        assert!(
1896            approx_eq(f, m.max_isometric_force, 1.0),
1897            "Active force at optimal length should equal F0"
1898        );
1899    }
1900
1901    #[test]
1902    fn test_muscle_zero_force_at_zero_activation() {
1903        let m = MuscleMaterial::biceps();
1904        let f = m.active_force(0.0, m.optimal_fiber_length);
1905        assert!(approx_eq(f, 0.0, EPS));
1906    }
1907
1908    #[test]
1909    fn test_muscle_force_length_bell() {
1910        let m = MuscleMaterial::biceps();
1911        let fl_opt = m.force_length_factor(m.optimal_fiber_length);
1912        let fl_far = m.force_length_factor(m.optimal_fiber_length * 2.0);
1913        assert!(approx_eq(fl_opt, 1.0, 1e-10));
1914        assert!(fl_far < fl_opt);
1915    }
1916
1917    #[test]
1918    fn test_muscle_passive_force_zero_at_optimal() {
1919        let m = MuscleMaterial::biceps();
1920        assert!(approx_eq(m.passive_force(m.optimal_fiber_length), 0.0, EPS));
1921    }
1922
1923    #[test]
1924    fn test_muscle_passive_force_increases_with_stretch() {
1925        let m = MuscleMaterial::biceps();
1926        let f1 = m.passive_force(m.optimal_fiber_length + 0.01);
1927        let f2 = m.passive_force(m.optimal_fiber_length + 0.02);
1928        assert!(f2 > f1);
1929    }
1930
1931    #[test]
1932    fn test_muscle_total_force_sum() {
1933        let m = MuscleMaterial::biceps();
1934        let a = 0.7;
1935        let l = m.optimal_fiber_length;
1936        let total = m.total_force(a, l);
1937        let expected = m.active_force(a, l) + m.passive_force(l);
1938        assert!(approx_eq(total, expected, EPS));
1939    }
1940
1941    #[test]
1942    fn test_muscle_tissue_type() {
1943        assert_eq!(MuscleMaterial::biceps().tissue_type(), TissueType::Muscle);
1944    }
1945
1946    // --- Stand-alone functions ---
1947
1948    #[test]
1949    fn test_reuss_modulus_two_phase() {
1950        let e1 = 200.0e9; // steel
1951        let e2 = 70.0e9; // aluminium
1952        let vf1 = 0.5;
1953        let er = reuss_modulus(e1, e2, vf1);
1954        // Reuss ≤ Voigt.
1955        let ev = voigt_modulus(e1, e2, vf1);
1956        assert!(er <= ev, "Reuss must be ≤ Voigt");
1957    }
1958
1959    #[test]
1960    fn test_voigt_modulus_endpoints() {
1961        // vf1 = 0 → E2; vf1 = 1 → E1.
1962        let e1 = 100.0e9;
1963        let e2 = 50.0e9;
1964        assert!(approx_eq(voigt_modulus(e1, e2, 0.0), e2, 1.0));
1965        assert!(approx_eq(voigt_modulus(e1, e2, 1.0), e1, 1.0));
1966    }
1967
1968    #[test]
1969    fn test_reuss_modulus_equal_phases() {
1970        let e = 100.0e9;
1971        let er = reuss_modulus(e, e, 0.5);
1972        assert!(approx_eq(er, e, 1.0));
1973    }
1974
1975    #[test]
1976    fn test_strain_energy_neo_hookean_zero_at_rest() {
1977        // I1 = 3 at rest.
1978        assert!(approx_eq(
1979            strain_energy_density_neo_hookean(1.0e4, 3.0),
1980            0.0,
1981            EPS
1982        ));
1983    }
1984
1985    #[test]
1986    fn test_strain_energy_neo_hookean_positive() {
1987        let w = strain_energy_density_neo_hookean(1.0e4, 4.0);
1988        assert!(w > 0.0);
1989    }
1990
1991    #[test]
1992    fn test_strain_energy_scales_linearly_with_c1() {
1993        let i1 = 4.0;
1994        let w1 = strain_energy_density_neo_hookean(1.0e4, i1);
1995        let w2 = strain_energy_density_neo_hookean(2.0e4, i1);
1996        assert!(approx_eq(w2, 2.0 * w1, EPS));
1997    }
1998}