Skip to main content

oxiphysics_materials/
biological_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Biological materials module: soft tissues, bone, cartilage, vascular walls,
5//! cell mechanics, hydrogels, and biomechanics analysis tools.
6//!
7//! All quantities use SI units (Pa, m, kg, N) unless otherwise stated.
8
9#![allow(dead_code)]
10#![allow(clippy::too_many_arguments)]
11
12use std::f64::consts::PI;
13
14// ---------------------------------------------------------------------------
15// Helper: 3×3 matrix ops (private)
16// ---------------------------------------------------------------------------
17
18fn mat3_identity() -> [[f64; 3]; 3] {
19    [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
20}
21
22fn mat3_trace(m: &[[f64; 3]; 3]) -> f64 {
23    m[0][0] + m[1][1] + m[2][2]
24}
25
26fn mat3_det(m: &[[f64; 3]; 3]) -> f64 {
27    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
28        - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
29        + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
30}
31
32fn mat3_transpose(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
33    let mut t = [[0.0f64; 3]; 3];
34    for i in 0..3 {
35        for j in 0..3 {
36            t[i][j] = m[j][i];
37        }
38    }
39    t
40}
41
42fn mat3_mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
43    let mut c = [[0.0f64; 3]; 3];
44    for i in 0..3 {
45        for j in 0..3 {
46            for k in 0..3 {
47                c[i][j] += a[i][k] * b[k][j];
48            }
49        }
50    }
51    c
52}
53
54/// Compute invariants (I1, I2, J) from deformation gradient F.
55fn invariants(f: &[[f64; 3]; 3]) -> (f64, f64, f64) {
56    let ft = mat3_transpose(f);
57    let c = mat3_mul(&ft, f);
58    let i1 = mat3_trace(&c);
59    let c2 = mat3_mul(&c, &c);
60    let i2 = 0.5 * (i1 * i1 - mat3_trace(&c2));
61    let j = mat3_det(f);
62    (i1, i2, j)
63}
64
65// ---------------------------------------------------------------------------
66// SoftTissue — fiber-reinforced hyperelastic (Fung-type + collagen/elastin)
67// ---------------------------------------------------------------------------
68
69/// Fiber-reinforced hyperelastic soft tissue model based on the Fung
70/// exponential strain energy function with embedded collagen and elastin
71/// network contributions.
72///
73/// The strain energy density is:
74/// `W = c/2 * (exp(b*(I1-3)) - 1) + k1/(2*k2) * (exp(k2*(I4-1)^2) - 1)`
75/// where I1 is the first invariant and I4 = a·Ca is the fiber pseudo-invariant.
76#[derive(Debug, Clone)]
77pub struct SoftTissue {
78    /// Matrix stiffness parameter c (Pa).
79    pub c: f64,
80    /// Matrix exponential coefficient b (dimensionless).
81    pub b: f64,
82    /// Collagen fiber stiffness k1 (Pa).
83    pub k1: f64,
84    /// Collagen exponential coefficient k2 (dimensionless).
85    pub k2: f64,
86    /// Elastin network modulus (Pa).
87    pub elastin_modulus: f64,
88    /// Collagen fiber volume fraction (0–1).
89    pub collagen_fraction: f64,
90    /// Elastin fiber volume fraction (0–1).
91    pub elastin_fraction: f64,
92    /// Fiber direction unit vector \[e0, e1, e2\].
93    pub fiber_direction: [f64; 3],
94    /// Tissue density (kg/m³).
95    pub density: f64,
96}
97
98impl SoftTissue {
99    /// Default properties for human skin dermis.
100    pub fn skin_dermis() -> Self {
101        Self {
102            c: 1200.0,
103            b: 12.0,
104            k1: 1500.0,
105            k2: 10.0,
106            elastin_modulus: 300e3,
107            collagen_fraction: 0.35,
108            elastin_fraction: 0.04,
109            fiber_direction: [1.0, 0.0, 0.0],
110            density: 1100.0,
111        }
112    }
113
114    /// Default properties for human myocardium.
115    pub fn myocardium() -> Self {
116        Self {
117            c: 500.0,
118            b: 8.0,
119            k1: 2000.0,
120            k2: 15.0,
121            elastin_modulus: 200e3,
122            collagen_fraction: 0.25,
123            elastin_fraction: 0.03,
124            fiber_direction: [1.0, 0.0, 0.0],
125            density: 1050.0,
126        }
127    }
128
129    /// Evaluate the Fung-type strain energy density (J/m³).
130    ///
131    /// `f` is the 3×3 deformation gradient.
132    pub fn strain_energy_density(&self, f: &[[f64; 3]; 3]) -> f64 {
133        let (i1, _i2, _j) = invariants(f);
134        let ft = mat3_transpose(f);
135        let c_mat = mat3_mul(&ft, f);
136        // Fiber pseudo-invariant I4 = a · C a
137        let a = &self.fiber_direction;
138        let ca = [
139            c_mat[0][0] * a[0] + c_mat[0][1] * a[1] + c_mat[0][2] * a[2],
140            c_mat[1][0] * a[0] + c_mat[1][1] * a[1] + c_mat[1][2] * a[2],
141            c_mat[2][0] * a[0] + c_mat[2][1] * a[1] + c_mat[2][2] * a[2],
142        ];
143        let i4 = a[0] * ca[0] + a[1] * ca[1] + a[2] * ca[2];
144
145        // Matrix (ground substance) term — Fung exponential
146        let w_matrix = self.c / 2.0 * ((self.b * (i1 - 3.0)).exp() - 1.0);
147
148        // Fiber term (only when stretched: I4 > 1)
149        let w_fiber = if i4 > 1.0 {
150            let xi = i4 - 1.0;
151            self.k1 / (2.0 * self.k2) * ((self.k2 * xi * xi).exp() - 1.0)
152        } else {
153            0.0
154        };
155
156        // Elastin isotropic neo-Hookean contribution
157        let w_elastin = self.elastin_modulus / 2.0 * (i1 - 3.0);
158
159        w_matrix + w_fiber + w_elastin
160    }
161
162    /// Estimate the tangent modulus at small strains (Pa).
163    pub fn small_strain_modulus(&self) -> f64 {
164        // dW/dI1 at I1 = 3 contributes 2*(c*b + elastin) to Young's modulus
165        let dw_di1 = self.c * self.b + self.elastin_modulus;
166        4.0 * dw_di1
167    }
168
169    /// Collagen network toe-region end strain (dimensionless).
170    ///
171    /// Below this stretch ratio the collagen fibers are not yet load-bearing.
172    pub fn toe_region_stretch(&self) -> f64 {
173        1.0 + (1.0 / (2.0 * self.k2)).sqrt()
174    }
175
176    /// Compute the Cauchy stress in the fiber direction at a given uniaxial
177    /// stretch `lambda` along the fiber axis.
178    pub fn uniaxial_cauchy_stress(&self, lambda: f64) -> f64 {
179        if lambda <= 0.0 {
180            return 0.0;
181        }
182        // For incompressible uniaxial: F = diag(lambda, 1/sqrt(lambda), 1/sqrt(lambda))
183        let lambda_perp = 1.0 / lambda.sqrt();
184        let i1 = lambda * lambda + 2.0 * lambda_perp * lambda_perp;
185        let i4 = lambda * lambda; // fiber aligned with stretch
186
187        let dw_di1 = self.c * self.b * (self.b * (i1 - 3.0)).exp() + self.elastin_modulus;
188
189        let dw_di4 = if i4 > 1.0 {
190            let xi = i4 - 1.0;
191            self.k1 * xi * (self.k2 * xi * xi).exp()
192        } else {
193            0.0
194        };
195
196        // Cauchy stress = 2*(lambda^2 dW/dI1 - lambda_perp^2 dW/dI1) + 2*lambda^2*dW/dI4
197
198        2.0 * dw_di1 * (lambda * lambda - lambda_perp * lambda_perp)
199            + 2.0 * lambda * lambda * dw_di4
200    }
201}
202
203// ---------------------------------------------------------------------------
204// BoneMaterial — cortical/cancellous, anisotropic elastic, Reuss–Voigt bounds
205// ---------------------------------------------------------------------------
206
207/// Anisotropic elastic model for bone (cortical and cancellous).
208///
209/// Cortical bone is treated as a transversely isotropic material with
210/// independent axial (longitudinal) and transverse stiffnesses.
211#[derive(Debug, Clone)]
212pub struct BoneMaterial {
213    /// Longitudinal (axial) Young's modulus E_L (Pa).  Cortical: ~17–25 GPa.
214    pub e_longitudinal: f64,
215    /// Transverse Young's modulus E_T (Pa).  Cortical: ~10–15 GPa.
216    pub e_transverse: f64,
217    /// Shear modulus G_LT (Pa).
218    pub g_lt: f64,
219    /// Poisson's ratio ν_LT (longitudinal–transverse).
220    pub nu_lt: f64,
221    /// Poisson's ratio ν_TT (transverse–transverse).
222    pub nu_tt: f64,
223    /// Bulk density (kg/m³).
224    pub density: f64,
225    /// Ultimate tensile strength (Pa).
226    pub uts: f64,
227    /// Ultimate compressive strength (Pa).
228    pub ucs: f64,
229    /// Volume fraction of mineralised matrix (0–1) for cancellous bone.
230    pub volume_fraction: f64,
231    /// Whether this represents cortical (`true`) or cancellous (`false`) bone.
232    pub is_cortical: bool,
233}
234
235impl BoneMaterial {
236    /// Human femoral cortical bone (mid-diaphysis).
237    pub fn cortical_femur() -> Self {
238        Self {
239            e_longitudinal: 20.0e9,
240            e_transverse: 12.0e9,
241            g_lt: 4.5e9,
242            nu_lt: 0.29,
243            nu_tt: 0.63,
244            density: 1900.0,
245            uts: 120.0e6,
246            ucs: 180.0e6,
247            volume_fraction: 1.0,
248            is_cortical: true,
249        }
250    }
251
252    /// Vertebral cancellous bone (trabecular, ~15 % volume fraction).
253    pub fn cancellous_vertebra() -> Self {
254        Self {
255            e_longitudinal: 0.3e9,
256            e_transverse: 0.15e9,
257            g_lt: 0.05e9,
258            nu_lt: 0.25,
259            nu_tt: 0.40,
260            density: 300.0,
261            uts: 5.0e6,
262            ucs: 8.0e6,
263            volume_fraction: 0.15,
264            is_cortical: false,
265        }
266    }
267
268    /// Voigt upper bound on effective longitudinal modulus for a two-phase
269    /// composite (bone matrix + marrow/pore space).
270    ///
271    /// `e_phase2` is the modulus of the second phase (Pa).
272    pub fn voigt_upper_bound(&self, e_phase2: f64) -> f64 {
273        self.volume_fraction * self.e_longitudinal + (1.0 - self.volume_fraction) * e_phase2
274    }
275
276    /// Reuss lower bound on effective longitudinal modulus.
277    ///
278    /// `e_phase2` is the modulus of the second phase (Pa).
279    pub fn reuss_lower_bound(&self, e_phase2: f64) -> f64 {
280        let vf = self.volume_fraction;
281        1.0 / (vf / self.e_longitudinal + (1.0 - vf) / e_phase2)
282    }
283
284    /// Hashin–Shtrikman arithmetic average of the two bounds.
285    pub fn hashin_shtrikman_estimate(&self, e_phase2: f64) -> f64 {
286        0.5 * (self.voigt_upper_bound(e_phase2) + self.reuss_lower_bound(e_phase2))
287    }
288
289    /// Morgan–Keaveny power-law density–modulus relationship.
290    ///
291    /// `ash_density_g_cm3` is the ash density in g/cm³.
292    pub fn modulus_from_ash_density(ash_density_g_cm3: f64) -> f64 {
293        // Keaveny & Hayes (1993) empirical fit for trabecular bone
294        6.95e9 * ash_density_g_cm3.powf(1.49)
295    }
296
297    /// Anisotropy ratio (E_L / E_T).
298    pub fn anisotropy_ratio(&self) -> f64 {
299        self.e_longitudinal / self.e_transverse
300    }
301
302    /// Longitudinal wave speed (m/s).
303    pub fn longitudinal_wave_speed(&self) -> f64 {
304        (self.e_longitudinal / self.density).sqrt()
305    }
306
307    /// Transverse (shear) wave speed (m/s).
308    pub fn shear_wave_speed(&self) -> f64 {
309        (self.g_lt / self.density).sqrt()
310    }
311
312    /// Yield strain in compression (dimensionless, ~0.7–1 % for cortical).
313    pub fn compressive_yield_strain(&self) -> f64 {
314        self.ucs / self.e_longitudinal
315    }
316}
317
318// ---------------------------------------------------------------------------
319// CartilageModel — biphasic theory, creep, permeability
320// ---------------------------------------------------------------------------
321
322/// Biphasic (solid + fluid) model for articular cartilage.
323///
324/// Based on Mow et al. (1980) biphasic theory:
325/// - Solid phase: linear elastic with Young's modulus `E_s` and Poisson's ratio `nu_s`.
326/// - Fluid phase: interstitial water with hydraulic permeability `k`.
327/// - Creep and stress relaxation arise from fluid exudation.
328#[derive(Debug, Clone)]
329pub struct CartilageModel {
330    /// Solid-phase aggregate modulus H_A = E_s*(1-nu_s)/((1+nu_s)*(1-2*nu_s)) (Pa).
331    pub aggregate_modulus: f64,
332    /// Solid-phase Young's modulus E_s (Pa).  Typical: 0.5–2 MPa.
333    pub e_solid: f64,
334    /// Solid-phase Poisson's ratio ν_s.
335    pub nu_solid: f64,
336    /// Fluid-phase hydraulic permeability k (m⁴/N·s).  Typical: ~1e-15 m⁴/N·s.
337    pub permeability: f64,
338    /// Cartilage thickness (m).
339    pub thickness: f64,
340    /// Fluid volume fraction (porosity, 0–1).  Typical: 0.65–0.80.
341    pub porosity: f64,
342    /// Fixed charge density (mEq/mL) — for swelling pressure computations.
343    pub fixed_charge_density: f64,
344    /// Tissue density (kg/m³).
345    pub density: f64,
346}
347
348impl CartilageModel {
349    /// Typical human articular cartilage (femoral condyle).
350    ///
351    /// Alias for [`Self::human_knee_cartilage`].
352    pub fn articular_cartilage() -> Self {
353        Self::human_knee_cartilage()
354    }
355
356    /// Typical human articular cartilage (femoral condyle).
357    pub fn human_knee_cartilage() -> Self {
358        let e_solid = 0.8e6;
359        let nu = 0.15;
360        let ha = e_solid * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
361        Self {
362            aggregate_modulus: ha,
363            e_solid,
364            nu_solid: nu,
365            permeability: 1.0e-15,
366            thickness: 3.0e-3,
367            porosity: 0.75,
368            fixed_charge_density: 0.1,
369            density: 1100.0,
370        }
371    }
372
373    /// Compute the aggregate modulus from E and ν.
374    pub fn compute_aggregate_modulus(e: f64, nu: f64) -> f64 {
375        e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu))
376    }
377
378    /// Characteristic time constant for creep τ = h²/(H_A * k) (s).
379    ///
380    /// `h` is the cartilage thickness (m).
381    pub fn creep_time_constant(&self) -> f64 {
382        let h = self.thickness;
383        h * h / (self.aggregate_modulus * self.permeability)
384    }
385
386    /// Biphasic creep compliance at time `t` (Pa⁻¹).
387    ///
388    /// Approximation using the first-term series solution from Mow et al. (1980).
389    pub fn creep_compliance(&self, t: f64) -> f64 {
390        let tau = self.creep_time_constant();
391        let c_inf = 1.0 / self.aggregate_modulus;
392        // First-term exponential approximation
393        let c0 = 1.0 / (self.aggregate_modulus + 4.0 / 3.0 * self.e_solid);
394        c_inf - (c_inf - c0) * (-t / tau).exp()
395    }
396
397    /// Donnan osmotic swelling pressure (Pa) at a given NaCl concentration.
398    ///
399    /// `c_ext` is the external NaCl concentration (mEq/mL).
400    pub fn donnan_swelling_pressure(&self, c_ext: f64) -> f64 {
401        // Simplified Donnan equilibrium: π = RT*(sqrt(cF^2/4 + c_ext^2) - c_ext)
402        // using RT ≈ 2436 Pa·mL/mEq at 37°C
403        let rt = 2436.0;
404        let cf = self.fixed_charge_density;
405        rt * ((cf * cf / 4.0 + c_ext * c_ext).sqrt() - c_ext)
406    }
407
408    /// Darcy velocity (fluid flux, m/s) under an applied pressure gradient `dp_dz` (Pa/m).
409    pub fn darcy_fluid_flux(&self, dp_dz: f64) -> f64 {
410        self.permeability * dp_dz
411    }
412
413    /// Strain-dependent permeability (Holmes–Mow model).
414    ///
415    /// `e` is the dilatational strain (volumetric, negative in compression).
416    pub fn strain_dependent_permeability(&self, e: f64) -> f64 {
417        // Holmes & Mow (1990): k(e) = k0 * exp(M * e)
418        let m = 4.0; // empirical parameter
419        self.permeability * (m * e).exp()
420    }
421}
422
423// ---------------------------------------------------------------------------
424// VascularWall — residual stress, opening angle, tri-layered structure
425// ---------------------------------------------------------------------------
426
427/// Model for arterial wall mechanics including residual stress, opening angle,
428/// and three-layer (intima, media, adventitia) structure.
429///
430/// Based on the Holzapfel–Gasser–Ogden (HGO) model for arterial mechanics.
431#[derive(Debug, Clone)]
432pub struct VascularWall {
433    /// Inner radius (unloaded, m).
434    pub inner_radius: f64,
435    /// Outer radius (unloaded, m).
436    pub outer_radius: f64,
437    /// Opening angle α (rad) — measures residual stress state.
438    pub opening_angle: f64,
439    /// Intima layer thickness (m).
440    pub intima_thickness: f64,
441    /// Media layer thickness (m).
442    pub media_thickness: f64,
443    /// Adventitia layer thickness (m).
444    pub adventitia_thickness: f64,
445    /// Isotropic neo-Hookean parameter c₁₀ (Pa) for the ground matrix.
446    pub c10: f64,
447    /// Fiber stiffness k1 (Pa) — HGO model.
448    pub k1: f64,
449    /// Fiber nonlinearity k2 (dimensionless) — HGO model.
450    pub k2: f64,
451    /// Mean fiber angle (rad) from the circumferential direction.
452    pub fiber_angle: f64,
453    /// Collagen fraction of wall composition (0–1).
454    pub collagen_fraction: f64,
455    /// Smooth muscle cell fraction (0–1).
456    pub smooth_muscle_fraction: f64,
457    /// Blood vessel density (kg/m³).
458    pub density: f64,
459}
460
461impl VascularWall {
462    /// Typical human aorta (thoracic).
463    pub fn human_aorta() -> Self {
464        Self {
465            inner_radius: 12.5e-3,
466            outer_radius: 15.0e-3,
467            opening_angle: 100.0_f64.to_radians(),
468            intima_thickness: 0.3e-3,
469            media_thickness: 1.5e-3,
470            adventitia_thickness: 0.7e-3,
471            c10: 3000.0,
472            k1: 2000.0,
473            k2: 11.0,
474            fiber_angle: 49.0_f64.to_radians(),
475            collagen_fraction: 0.22,
476            smooth_muscle_fraction: 0.15,
477            density: 1050.0,
478        }
479    }
480
481    /// Typical human coronary artery.
482    pub fn coronary_artery() -> Self {
483        Self {
484            inner_radius: 1.5e-3,
485            outer_radius: 2.2e-3,
486            opening_angle: 115.0_f64.to_radians(),
487            intima_thickness: 0.1e-3,
488            media_thickness: 0.5e-3,
489            adventitia_thickness: 0.1e-3,
490            c10: 5000.0,
491            k1: 3500.0,
492            k2: 14.0,
493            fiber_angle: 44.0_f64.to_radians(),
494            collagen_fraction: 0.28,
495            smooth_muscle_fraction: 0.20,
496            density: 1050.0,
497        }
498    }
499
500    /// Wall thickness (m).
501    pub fn wall_thickness(&self) -> f64 {
502        self.outer_radius - self.inner_radius
503    }
504
505    /// Circumferential residual stretch due to the opening angle.
506    ///
507    /// Returns the inner/outer residual stretch ratios at the given
508    /// radial position `r` (m).
509    pub fn residual_stretch_circumferential(&self, r: f64) -> f64 {
510        // Fung & Liu (1989) formula for opening angle model
511        let alpha = self.opening_angle;
512        let ri = self.inner_radius;
513        let ro = self.outer_radius;
514        let r_mid = 0.5 * (ri + ro);
515        // Approximate: lambda_theta = r / r_mid * (PI / (PI - alpha))
516        (r / r_mid) * (PI / (PI - alpha))
517    }
518
519    /// HGO strain energy density (J/m³) at deformation gradient F.
520    pub fn hgo_strain_energy(&self, f: &[[f64; 3]; 3]) -> f64 {
521        let (i1, _i2, j) = invariants(f);
522        let ft = mat3_transpose(f);
523        let c_mat = mat3_mul(&ft, f);
524
525        // Fiber pseudo-invariant I4 (fiber aligned at ±fiber_angle from circumferential)
526        let cos_a = self.fiber_angle.cos();
527        let sin_a = self.fiber_angle.sin();
528        let a1 = [cos_a, sin_a, 0.0];
529        let i4 = a1[0] * (c_mat[0][0] * a1[0] + c_mat[0][1] * a1[1])
530            + a1[1] * (c_mat[1][0] * a1[0] + c_mat[1][1] * a1[1]);
531
532        // Penalty term for incompressibility
533        let d = 1.0 / (2.0 * self.c10); // bulk modulus-like
534        let w_vol = (j - 1.0).powi(2) / (2.0 * d);
535
536        // Isochoric matrix
537        let j_23 = j.powf(-2.0 / 3.0);
538        let i1_bar = j_23 * i1;
539        let w_iso = self.c10 * (i1_bar - 3.0);
540
541        // Fiber (only when I4 > 1)
542        let w_fiber = if i4 > 1.0 {
543            let e = i4 - 1.0;
544            self.k1 / (2.0 * self.k2) * ((self.k2 * e * e).exp() - 1.0)
545        } else {
546            0.0
547        };
548
549        w_vol + w_iso + w_fiber
550    }
551
552    /// Laplace tension (N/m) for a thin-walled cylinder at internal pressure p (Pa).
553    pub fn laplace_tension(&self, internal_pressure: f64) -> f64 {
554        internal_pressure * self.inner_radius
555    }
556
557    /// Circumferential wall stress (Pa) at mean wall radius under Laplace.
558    pub fn circumferential_stress(&self, internal_pressure: f64) -> f64 {
559        let h = self.wall_thickness();
560        self.laplace_tension(internal_pressure) / h
561    }
562
563    /// Compliance (m/Pa) per unit length of vessel.
564    ///
565    /// Estimated from linearised pressure–radius relationship.
566    pub fn vascular_compliance(&self, e_wall: f64) -> f64 {
567        let r = 0.5 * (self.inner_radius + self.outer_radius);
568        let h = self.wall_thickness();
569        2.0 * PI * r * r * r / (e_wall * h)
570    }
571}
572
573// ---------------------------------------------------------------------------
574// CellMechanics — cortical tension, cytoskeletal stiffness, contractility
575// ---------------------------------------------------------------------------
576
577/// Mechanical model for a single eukaryotic cell including cortical tension,
578/// cytoskeletal prestress, and active actomyosin contractility.
579///
580/// Based on the tensegrity model (Ingber, 1997) and Zaman et al. (2007).
581#[derive(Debug, Clone)]
582pub struct CellMechanics {
583    /// Cortical tension (N/m) — surface tension of the cell cortex.
584    pub cortical_tension: f64,
585    /// Cell radius (m).
586    pub radius: f64,
587    /// Cytoskeletal Young's modulus (Pa).  Typical: 100–1000 Pa.
588    pub cytoskeletal_modulus: f64,
589    /// Active contractility stress (Pa) generated by actomyosin.
590    pub active_stress: f64,
591    /// Adhesion energy density (J/m²) — cell–substrate adhesion.
592    pub adhesion_energy: f64,
593    /// Cell density (kg/m³).
594    pub density: f64,
595    /// Number of actin stress fibers.
596    pub num_stress_fibers: usize,
597    /// Stress fiber prestress (Pa).
598    pub prestress: f64,
599}
600
601impl CellMechanics {
602    /// Typical adherent fibroblast.
603    pub fn fibroblast() -> Self {
604        Self {
605            cortical_tension: 0.05e-3,
606            radius: 10.0e-6,
607            cytoskeletal_modulus: 400.0,
608            active_stress: 1000.0,
609            adhesion_energy: 1.0e-4,
610            density: 1060.0,
611            num_stress_fibers: 20,
612            prestress: 500.0,
613        }
614    }
615
616    /// Typical red blood cell.
617    pub fn red_blood_cell() -> Self {
618        Self {
619            cortical_tension: 0.002e-3,
620            radius: 4.0e-6,
621            cytoskeletal_modulus: 6.0,
622            active_stress: 0.0,
623            adhesion_energy: 0.0,
624            density: 1080.0,
625            num_stress_fibers: 0,
626            prestress: 0.0,
627        }
628    }
629
630    /// Laplace pressure from cortical tension (Pa).
631    pub fn laplace_pressure(&self) -> f64 {
632        2.0 * self.cortical_tension / self.radius
633    }
634
635    /// Effective Young's modulus including active contractility (Pa).
636    pub fn effective_modulus(&self) -> f64 {
637        self.cytoskeletal_modulus + self.active_stress
638    }
639
640    /// Spreading area (m²) predicted from adhesion energy balance.
641    ///
642    /// Equates adhesion energy to cortical tension energy: A = π r² W / T_c.
643    pub fn spreading_area(&self) -> f64 {
644        let w = self.adhesion_energy;
645        let tc = self.cortical_tension;
646        if tc < 1e-20 {
647            return 0.0;
648        }
649        PI * self.radius * self.radius * w / tc
650    }
651
652    /// Bending stiffness of the cell membrane (J), approximate.
653    ///
654    /// Uses the result from the Helfrich model: κ = 25 k_B T.
655    pub fn membrane_bending_stiffness(&self) -> f64 {
656        // k_B T at 37°C ≈ 4.28e-21 J
657        let kbt = 4.28e-21_f64;
658        25.0 * kbt
659    }
660
661    /// Cell volume (m³) assuming spherical shape.
662    pub fn volume(&self) -> f64 {
663        4.0 / 3.0 * PI * self.radius.powi(3)
664    }
665
666    /// Osmotic pressure (Pa) from a small volume change `dv/v`.
667    ///
668    /// Uses a linear osmotic modulus approximation K_osm ≈ E_cyto.
669    pub fn osmotic_pressure(&self, volumetric_strain: f64) -> f64 {
670        -self.cytoskeletal_modulus * volumetric_strain
671    }
672}
673
674// ---------------------------------------------------------------------------
675// HydrogelModel — swelling, crosslink density, Flory–Rehner theory
676// ---------------------------------------------------------------------------
677
678/// Hydrogel mechanical and swelling model based on Flory–Rehner theory.
679///
680/// Combines elastic (rubber-like) network energy with Flory–Huggins
681/// polymer–solvent mixing free energy.
682#[derive(Debug, Clone)]
683pub struct HydrogelModel {
684    /// Polymer volume fraction at synthesis (reference state, 0–1).
685    pub phi0: f64,
686    /// Equilibrium polymer volume fraction in swollen state (0–1).
687    pub phi_eq: f64,
688    /// Flory–Huggins interaction parameter χ (dimensionless).  Typical: 0.4–0.6.
689    pub chi: f64,
690    /// Crosslink density (mol/m³).
691    pub crosslink_density: f64,
692    /// Dry gel modulus (Pa).
693    pub dry_modulus: f64,
694    /// Solvent molar volume v_s (m³/mol).  Water: ~1.8e-5 m³/mol.
695    pub solvent_molar_volume: f64,
696    /// Temperature (K).
697    pub temperature: f64,
698}
699
700impl HydrogelModel {
701    /// PEG hydrogel typical parameters (10 % w/w, moderate crosslinking).
702    pub fn peg_hydrogel() -> Self {
703        Self {
704            phi0: 0.10,
705            phi_eq: 0.08,
706            chi: 0.45,
707            crosslink_density: 500.0,
708            dry_modulus: 50.0e3,
709            solvent_molar_volume: 1.8e-5,
710            temperature: 310.0,
711        }
712    }
713
714    /// Hyaluronic acid hydrogel.
715    pub fn hyaluronic_acid() -> Self {
716        Self {
717            phi0: 0.02,
718            phi_eq: 0.015,
719            chi: 0.50,
720            crosslink_density: 100.0,
721            dry_modulus: 1.0e3,
722            solvent_molar_volume: 1.8e-5,
723            temperature: 310.0,
724        }
725    }
726
727    /// Gas constant R (J/mol·K).
728    const R: f64 = 8.314;
729
730    /// Rubber-elastic network contribution to the swelling stress (Pa).
731    ///
732    /// Based on affine network theory: G_el = n_c * R * T * phi_eq^(1/3).
733    pub fn network_modulus(&self) -> f64 {
734        let n_c = self.crosslink_density;
735        let phi = self.phi_eq;
736        n_c * Self::R * self.temperature * phi.powf(1.0 / 3.0)
737    }
738
739    /// Flory–Huggins mixing chemical potential of the solvent (J/mol).
740    ///
741    /// Δμ_s = RT * \[ln(1-φ) + φ + χ φ²\]
742    pub fn mixing_chemical_potential(&self) -> f64 {
743        let phi = self.phi_eq;
744        Self::R * self.temperature * ((1.0 - phi).ln() + phi + self.chi * phi * phi)
745    }
746
747    /// Osmotic (swelling) pressure Π (Pa) at equilibrium.
748    ///
749    /// Π = -R T / v_s * \[ln(1-φ) + φ + χ φ²\] + G_el*(φ^(1/3) - φ/2)
750    pub fn osmotic_pressure(&self) -> f64 {
751        let phi = self.phi_eq;
752        let rtvs = Self::R * self.temperature / self.solvent_molar_volume;
753        let pi_mix = -rtvs * ((1.0 - phi).ln() + phi + self.chi * phi * phi);
754        let pi_el = self.network_modulus() * (phi.powf(1.0 / 3.0) - phi / 2.0);
755        pi_mix + pi_el
756    }
757
758    /// Swelling ratio Q = V_swollen / V_dry = 1/φ_eq.
759    pub fn swelling_ratio(&self) -> f64 {
760        1.0 / self.phi_eq
761    }
762
763    /// Linear swelling strain ε = Q^(1/3) - 1.
764    pub fn linear_swelling_strain(&self) -> f64 {
765        self.swelling_ratio().powf(1.0 / 3.0) - 1.0
766    }
767
768    /// Gel modulus in the swollen state (Pa) via affine network theory.
769    pub fn swollen_modulus(&self) -> f64 {
770        self.network_modulus()
771    }
772
773    /// Diffusivity of solvent through the gel (m²/s) using free-volume theory.
774    ///
775    /// `d0` is the free-solution diffusivity (m²/s).
776    pub fn effective_diffusivity(&self, d0: f64) -> f64 {
777        // Obstruction model: D_eff = D0 * exp(-π r^2 L_c c / 4) simplified
778        let phi = self.phi_eq;
779        d0 * (-0.84 * phi / (1.0 - phi)).exp()
780    }
781}
782
783// ---------------------------------------------------------------------------
784// BiomechanicsAnalysis — stress shielding, failure envelope, fatigue life
785// ---------------------------------------------------------------------------
786
787/// Analysis tools for biomechanical systems: stress shielding in orthopaedic
788/// implants, failure envelopes for bone/tissue, and fatigue life estimation.
789#[derive(Debug, Clone)]
790pub struct BiomechanicsAnalysis {
791    /// Implant (or prosthesis) Young's modulus (Pa).
792    pub implant_modulus: f64,
793    /// Host bone Young's modulus (Pa).
794    pub bone_modulus: f64,
795    /// Cross-sectional area of implant (m²).
796    pub implant_area: f64,
797    /// Cross-sectional area of bone (m²).
798    pub bone_area: f64,
799    /// Applied axial load (N).
800    pub applied_load: f64,
801    /// Fatigue exponent m (material constant, typically 2–6 for bone).
802    pub fatigue_exponent: f64,
803    /// Reference fatigue stress σ₀ (Pa) at N₀ cycles.
804    pub reference_fatigue_stress: f64,
805    /// Reference cycle count N₀.
806    pub reference_cycles: f64,
807}
808
809impl BiomechanicsAnalysis {
810    /// Typical hip replacement scenario (titanium stem in femoral cortex).
811    pub fn hip_replacement() -> Self {
812        Self {
813            implant_modulus: 110.0e9, // Ti-6Al-4V
814            bone_modulus: 18.0e9,
815            implant_area: 200.0e-6,
816            bone_area: 400.0e-6,
817            applied_load: 2500.0,
818            fatigue_exponent: 6.0,
819            reference_fatigue_stress: 60.0e6,
820            reference_cycles: 1.0e6,
821        }
822    }
823
824    /// Stress shielding factor (0–1): fraction of load carried by the implant.
825    ///
826    /// A value close to 1 means the implant carries almost all the load (severe
827    /// stress shielding); 0 means no shielding.
828    pub fn stress_shielding_factor(&self) -> f64 {
829        let ea_implant = self.implant_modulus * self.implant_area;
830        let ea_bone = self.bone_modulus * self.bone_area;
831        ea_implant / (ea_implant + ea_bone)
832    }
833
834    /// Stress in the bone with the implant present (Pa).
835    pub fn bone_stress_with_implant(&self) -> f64 {
836        let f = self.stress_shielding_factor();
837        (1.0 - f) * self.applied_load / self.bone_area
838    }
839
840    /// Stress in the bone without the implant (Pa).
841    pub fn bone_stress_without_implant(&self) -> f64 {
842        self.applied_load / self.bone_area
843    }
844
845    /// Tsai–Wu failure criterion for an orthotropic material.
846    ///
847    /// Returns the failure index (≥1 means failure).
848    /// - `sigma11` axial stress (Pa)
849    /// - `sigma22` transverse stress (Pa)
850    /// - `tau12` shear stress (Pa)
851    /// - `f1t` tensile strength in 1-direction (Pa)
852    /// - `f1c` compressive strength in 1-direction (Pa)
853    /// - `f2t` tensile strength in 2-direction (Pa)
854    /// - `f2c` compressive strength in 2-direction (Pa)
855    /// - `f12` shear strength (Pa)
856    #[allow(clippy::too_many_arguments)]
857    pub fn tsai_wu_failure_index(
858        sigma11: f64,
859        sigma22: f64,
860        tau12: f64,
861        f1t: f64,
862        f1c: f64,
863        f2t: f64,
864        f2c: f64,
865        f12: f64,
866    ) -> f64 {
867        let h1 = 1.0 / f1t - 1.0 / f1c;
868        let h2 = 1.0 / f2t - 1.0 / f2c;
869        let h11 = 1.0 / (f1t * f1c);
870        let h22 = 1.0 / (f2t * f2c);
871        let h66 = 1.0 / (f12 * f12);
872        let h12 = -0.5 * (h11 * h22).sqrt(); // interactive term
873        h1 * sigma11
874            + h2 * sigma22
875            + h11 * sigma11 * sigma11
876            + h22 * sigma22 * sigma22
877            + h66 * tau12 * tau12
878            + 2.0 * h12 * sigma11 * sigma22
879    }
880
881    /// Von Mises equivalent stress (Pa).
882    pub fn von_mises_stress(s11: f64, s22: f64, s33: f64, s12: f64, s23: f64, s13: f64) -> f64 {
883        (0.5 * ((s11 - s22).powi(2)
884            + (s22 - s33).powi(2)
885            + (s33 - s11).powi(2)
886            + 6.0 * (s12 * s12 + s23 * s23 + s13 * s13)))
887            .sqrt()
888    }
889
890    /// Bone fatigue life (cycles) under cyclic stress amplitude `sigma_a` (Pa).
891    ///
892    /// Uses the power-law S-N relationship: N = N₀ * (σ₀/σ_a)^m.
893    pub fn fatigue_life(&self, sigma_a: f64) -> f64 {
894        if sigma_a <= 0.0 {
895            return f64::INFINITY;
896        }
897        self.reference_cycles
898            * (self.reference_fatigue_stress / sigma_a).powf(self.fatigue_exponent)
899    }
900
901    /// Miner's cumulative damage rule for variable-amplitude loading.
902    ///
903    /// Returns the total damage D (failure when D ≥ 1).
904    /// `cycles_at_stress` is a slice of (n_applied, sigma_a) pairs.
905    pub fn miners_damage(&self, cycles_at_stress: &[(f64, f64)]) -> f64 {
906        cycles_at_stress
907            .iter()
908            .map(|(n, sigma)| n / self.fatigue_life(*sigma))
909            .sum()
910    }
911
912    /// Maximum principal stress failure criterion.
913    ///
914    /// Returns `true` if any principal stress exceeds the material strength.
915    pub fn max_principal_stress_failure(
916        &self,
917        sigma1: f64,
918        sigma2: f64,
919        sigma3: f64,
920        tensile_strength: f64,
921        compressive_strength: f64,
922    ) -> bool {
923        sigma1 > tensile_strength
924            || sigma2 > tensile_strength
925            || sigma3 > tensile_strength
926            || sigma1 < -compressive_strength
927            || sigma2 < -compressive_strength
928            || sigma3 < -compressive_strength
929    }
930
931    /// Remodelling stimulus (dimensionless) — Huiskes et al. strain energy model.
932    ///
933    /// `sed` is the strain energy density (J/m³), `sed_ref` the reference SED.
934    /// Returns positive for bone apposition, negative for resorption.
935    pub fn remodelling_stimulus(sed: f64, sed_ref: f64, dead_zone: f64) -> f64 {
936        let ratio = (sed - sed_ref) / sed_ref;
937        if ratio.abs() < dead_zone {
938            0.0
939        } else {
940            ratio - dead_zone.copysign(ratio)
941        }
942    }
943}
944
945// ===========================================================================
946// Tests
947// ===========================================================================
948
949#[cfg(test)]
950mod tests {
951    use super::*;
952
953    const TOL: f64 = 1e-6;
954
955    // --- SoftTissue ---
956
957    #[test]
958    fn test_soft_tissue_strain_energy_at_rest() {
959        let st = SoftTissue::skin_dermis();
960        let f_id = mat3_identity();
961        let w = st.strain_energy_density(&f_id);
962        // At identity, I1=3 so W_matrix ≈ 0 and W_fiber ≈ 0 (I4=1 → no fiber term)
963        assert!(
964            w.abs() < 1e-3,
965            "strain energy at rest should be ~0, got {w}"
966        );
967    }
968
969    #[test]
970    fn test_soft_tissue_strain_energy_positive_under_stretch() {
971        let st = SoftTissue::skin_dermis();
972        let lambda = 1.2_f64;
973        let lp = 1.0 / lambda.sqrt();
974        let f = [[lambda, 0.0, 0.0], [0.0, lp, 0.0], [0.0, 0.0, lp]];
975        let w = st.strain_energy_density(&f);
976        assert!(
977            w > 0.0,
978            "strain energy should be positive under stretch, got {w}"
979        );
980    }
981
982    #[test]
983    fn test_soft_tissue_cauchy_stress_positive_stretch() {
984        let st = SoftTissue::skin_dermis();
985        let sigma = st.uniaxial_cauchy_stress(1.3);
986        assert!(sigma > 0.0, "Cauchy stress should be positive, got {sigma}");
987    }
988
989    #[test]
990    fn test_soft_tissue_cauchy_stress_at_one() {
991        let st = SoftTissue::skin_dermis();
992        let sigma = st.uniaxial_cauchy_stress(1.0);
993        // At λ=1, (λ² - λ_perp²) = 0 so stress = 0
994        assert!(
995            sigma.abs() < 1e-3,
996            "Cauchy stress at λ=1 should be ~0, got {sigma}"
997        );
998    }
999
1000    #[test]
1001    fn test_soft_tissue_small_strain_modulus_positive() {
1002        let st = SoftTissue::myocardium();
1003        let e = st.small_strain_modulus();
1004        assert!(e > 0.0, "small-strain modulus should be positive, got {e}");
1005    }
1006
1007    #[test]
1008    fn test_soft_tissue_toe_region_stretch_greater_than_one() {
1009        let st = SoftTissue::skin_dermis();
1010        let toe = st.toe_region_stretch();
1011        assert!(toe > 1.0, "toe-region stretch should be >1, got {toe}");
1012    }
1013
1014    #[test]
1015    fn test_soft_tissue_stress_increases_with_stretch() {
1016        let st = SoftTissue::skin_dermis();
1017        let s1 = st.uniaxial_cauchy_stress(1.1);
1018        let s2 = st.uniaxial_cauchy_stress(1.5);
1019        assert!(
1020            s2 > s1,
1021            "stress should increase with stretch: s1={s1}, s2={s2}"
1022        );
1023    }
1024
1025    // --- BoneMaterial ---
1026
1027    #[test]
1028    fn test_bone_anisotropy_ratio() {
1029        let bone = BoneMaterial::cortical_femur();
1030        let r = bone.anisotropy_ratio();
1031        assert!(
1032            (r - 20.0e9 / 12.0e9).abs() < TOL,
1033            "anisotropy ratio should be E_L/E_T"
1034        );
1035    }
1036
1037    #[test]
1038    fn test_bone_voigt_reuss_bounds() {
1039        let bone = BoneMaterial::cancellous_vertebra();
1040        let e2 = 1.0e6; // marrow modulus ~1 MPa
1041        let voigt = bone.voigt_upper_bound(e2);
1042        let reuss = bone.reuss_lower_bound(e2);
1043        assert!(
1044            voigt >= reuss,
1045            "Voigt bound should be ≥ Reuss bound: {voigt} vs {reuss}"
1046        );
1047    }
1048
1049    #[test]
1050    fn test_bone_hs_estimate_between_bounds() {
1051        let bone = BoneMaterial::cortical_femur();
1052        let e2 = 0.1e9;
1053        let v = bone.voigt_upper_bound(e2);
1054        let r = bone.reuss_lower_bound(e2);
1055        let hs = bone.hashin_shtrikman_estimate(e2);
1056        assert!(
1057            hs >= r && hs <= v,
1058            "HS estimate {hs} should be between Reuss {r} and Voigt {v}"
1059        );
1060    }
1061
1062    #[test]
1063    fn test_bone_wave_speeds_positive() {
1064        let bone = BoneMaterial::cortical_femur();
1065        assert!(bone.longitudinal_wave_speed() > 0.0);
1066        assert!(bone.shear_wave_speed() > 0.0);
1067    }
1068
1069    #[test]
1070    fn test_bone_compressive_yield_strain() {
1071        let bone = BoneMaterial::cortical_femur();
1072        let eps = bone.compressive_yield_strain();
1073        assert!(
1074            eps > 0.0 && eps < 0.02,
1075            "yield strain should be ~0.9 %, got {eps}"
1076        );
1077    }
1078
1079    #[test]
1080    fn test_bone_ash_density_modulus_power_law() {
1081        let e = BoneMaterial::modulus_from_ash_density(0.8);
1082        assert!(e > 0.0, "modulus should be positive, got {e}");
1083    }
1084
1085    // --- CartilageModel ---
1086
1087    #[test]
1088    fn test_cartilage_aggregate_modulus_formula() {
1089        let e = 0.8e6;
1090        let nu = 0.15;
1091        let ha = CartilageModel::compute_aggregate_modulus(e, nu);
1092        let expected = e * (1.0 - nu) / ((1.0 + nu) * (1.0 - 2.0 * nu));
1093        assert!((ha - expected).abs() < 1.0, "aggregate modulus mismatch");
1094    }
1095
1096    #[test]
1097    fn test_cartilage_creep_time_constant_positive() {
1098        let cart = CartilageModel::human_knee_cartilage();
1099        let tau = cart.creep_time_constant();
1100        assert!(
1101            tau > 0.0,
1102            "creep time constant should be positive, got {tau}"
1103        );
1104    }
1105
1106    #[test]
1107    fn test_cartilage_creep_compliance_monotone() {
1108        let cart = CartilageModel::human_knee_cartilage();
1109        let c0 = cart.creep_compliance(0.0);
1110        let c100 = cart.creep_compliance(100.0);
1111        let cinf = cart.creep_compliance(1.0e12);
1112        assert!(c0 <= c100, "compliance should increase over time");
1113        assert!(c100 <= cinf, "compliance should approach equilibrium");
1114    }
1115
1116    #[test]
1117    fn test_cartilage_darcy_flux() {
1118        let cart = CartilageModel::human_knee_cartilage();
1119        let flux = cart.darcy_fluid_flux(1.0e6);
1120        assert!(
1121            flux > 0.0,
1122            "Darcy flux should be positive for positive pressure gradient"
1123        );
1124    }
1125
1126    #[test]
1127    fn test_cartilage_donnan_pressure_positive() {
1128        let cart = CartilageModel::human_knee_cartilage();
1129        let pi = cart.donnan_swelling_pressure(0.15);
1130        assert!(pi > 0.0, "Donnan pressure should be positive, got {pi}");
1131    }
1132
1133    #[test]
1134    fn test_cartilage_strain_dependent_permeability() {
1135        let cart = CartilageModel::human_knee_cartilage();
1136        // Negative strain (compression) should reduce permeability
1137        let k_comp = cart.strain_dependent_permeability(-0.2);
1138        let k_tens = cart.strain_dependent_permeability(0.2);
1139        assert!(
1140            k_comp < cart.permeability,
1141            "permeability should decrease in compression"
1142        );
1143        assert!(
1144            k_tens > cart.permeability,
1145            "permeability should increase in tension"
1146        );
1147    }
1148
1149    // --- VascularWall ---
1150
1151    #[test]
1152    fn test_vascular_wall_thickness() {
1153        let aw = VascularWall::human_aorta();
1154        let h = aw.wall_thickness();
1155        assert!(
1156            (h - 2.5e-3).abs() < 1e-6,
1157            "aorta wall thickness should be ~2.5 mm, got {h}"
1158        );
1159    }
1160
1161    #[test]
1162    fn test_vascular_laplace_tension() {
1163        let aw = VascularWall::human_aorta();
1164        let p = 13_332.0; // ~100 mmHg in Pa
1165        let t = aw.laplace_tension(p);
1166        assert!(t > 0.0, "Laplace tension should be positive, got {t}");
1167    }
1168
1169    #[test]
1170    fn test_vascular_circumferential_stress() {
1171        let aw = VascularWall::human_aorta();
1172        let s = aw.circumferential_stress(13_332.0);
1173        assert!(
1174            s > 0.0,
1175            "circumferential stress should be positive, got {s}"
1176        );
1177    }
1178
1179    #[test]
1180    fn test_vascular_hgo_energy_at_identity() {
1181        let aw = VascularWall::human_aorta();
1182        let f_id = mat3_identity();
1183        let w = aw.hgo_strain_energy(&f_id);
1184        // At identity, both isochoric and fiber terms should be small
1185        assert!(
1186            w.abs() < 1.0,
1187            "HGO energy at identity should be near 0, got {w}"
1188        );
1189    }
1190
1191    #[test]
1192    fn test_vascular_hgo_energy_positive_under_stretch() {
1193        let aw = VascularWall::human_aorta();
1194        let lambda = 1.2_f64;
1195        let lp = 1.0 / lambda.sqrt();
1196        let f = [[lambda, 0.0, 0.0], [0.0, lp, 0.0], [0.0, 0.0, lp]];
1197        let w = aw.hgo_strain_energy(&f);
1198        assert!(
1199            w > 0.0,
1200            "HGO energy should be positive under stretch, got {w}"
1201        );
1202    }
1203
1204    #[test]
1205    fn test_vascular_residual_stretch() {
1206        let aw = VascularWall::human_aorta();
1207        let r_mid = 0.5 * (aw.inner_radius + aw.outer_radius);
1208        let lam = aw.residual_stretch_circumferential(r_mid);
1209        assert!(lam > 0.0, "residual stretch should be positive, got {lam}");
1210    }
1211
1212    // --- CellMechanics ---
1213
1214    #[test]
1215    fn test_cell_laplace_pressure() {
1216        let cell = CellMechanics::fibroblast();
1217        let p = cell.laplace_pressure();
1218        assert!(p > 0.0, "Laplace pressure should be positive, got {p}");
1219    }
1220
1221    #[test]
1222    fn test_cell_effective_modulus() {
1223        let cell = CellMechanics::fibroblast();
1224        let e = cell.effective_modulus();
1225        assert!(
1226            e > cell.cytoskeletal_modulus,
1227            "effective modulus should exceed cytoskeletal modulus"
1228        );
1229    }
1230
1231    #[test]
1232    fn test_cell_volume_positive() {
1233        let cell = CellMechanics::fibroblast();
1234        assert!(cell.volume() > 0.0, "cell volume should be positive");
1235    }
1236
1237    #[test]
1238    fn test_cell_osmotic_pressure_sign() {
1239        let cell = CellMechanics::fibroblast();
1240        // Compression (negative strain) → positive osmotic pressure
1241        let p = cell.osmotic_pressure(-0.1);
1242        assert!(
1243            p > 0.0,
1244            "osmotic pressure under compression should be positive"
1245        );
1246    }
1247
1248    #[test]
1249    fn test_cell_membrane_bending_stiffness() {
1250        let cell = CellMechanics::red_blood_cell();
1251        let kappa = cell.membrane_bending_stiffness();
1252        assert!(
1253            kappa > 0.0,
1254            "bending stiffness should be positive, got {kappa}"
1255        );
1256    }
1257
1258    // --- HydrogelModel ---
1259
1260    #[test]
1261    fn test_hydrogel_swelling_ratio() {
1262        let gel = HydrogelModel::peg_hydrogel();
1263        let q = gel.swelling_ratio();
1264        assert!(
1265            q > 1.0,
1266            "swelling ratio should be >1 for swollen gel, got {q}"
1267        );
1268    }
1269
1270    #[test]
1271    fn test_hydrogel_linear_swelling_strain() {
1272        let gel = HydrogelModel::peg_hydrogel();
1273        let eps = gel.linear_swelling_strain();
1274        assert!(
1275            eps > 0.0,
1276            "linear swelling strain should be positive, got {eps}"
1277        );
1278    }
1279
1280    #[test]
1281    fn test_hydrogel_network_modulus_positive() {
1282        let gel = HydrogelModel::peg_hydrogel();
1283        let g = gel.network_modulus();
1284        assert!(g > 0.0, "network modulus should be positive, got {g}");
1285    }
1286
1287    #[test]
1288    fn test_hydrogel_effective_diffusivity() {
1289        let gel = HydrogelModel::hyaluronic_acid();
1290        let d0 = 1.0e-9; // m²/s (typical protein in water)
1291        let deff = gel.effective_diffusivity(d0);
1292        assert!(
1293            deff > 0.0,
1294            "effective diffusivity should be positive, got {deff}"
1295        );
1296        assert!(
1297            deff < d0,
1298            "effective diffusivity should be less than free-solution value"
1299        );
1300    }
1301
1302    // --- BiomechanicsAnalysis ---
1303
1304    #[test]
1305    fn test_stress_shielding_factor_range() {
1306        let ba = BiomechanicsAnalysis::hip_replacement();
1307        let f = ba.stress_shielding_factor();
1308        assert!(
1309            f > 0.0 && f < 1.0,
1310            "stress shielding factor should be in (0,1), got {f}"
1311        );
1312    }
1313
1314    #[test]
1315    fn test_bone_stress_with_implant_less_than_without() {
1316        let ba = BiomechanicsAnalysis::hip_replacement();
1317        let with_imp = ba.bone_stress_with_implant();
1318        let without = ba.bone_stress_without_implant();
1319        assert!(
1320            with_imp < without,
1321            "bone stress with implant should be less (stress shielding): {with_imp} vs {without}"
1322        );
1323    }
1324
1325    #[test]
1326    fn test_fatigue_life_decreases_with_stress() {
1327        let ba = BiomechanicsAnalysis::hip_replacement();
1328        let n1 = ba.fatigue_life(50.0e6);
1329        let n2 = ba.fatigue_life(100.0e6);
1330        assert!(
1331            n1 > n2,
1332            "fatigue life should decrease with higher stress: N1={n1}, N2={n2}"
1333        );
1334    }
1335
1336    #[test]
1337    fn test_fatigue_life_infinite_at_zero_stress() {
1338        let ba = BiomechanicsAnalysis::hip_replacement();
1339        let n = ba.fatigue_life(0.0);
1340        assert!(
1341            n.is_infinite(),
1342            "fatigue life should be infinite at zero stress"
1343        );
1344    }
1345
1346    #[test]
1347    fn test_miners_damage_accumulates() {
1348        let ba = BiomechanicsAnalysis::hip_replacement();
1349        let loading = vec![(1.0e5, 60.0e6), (2.0e5, 80.0e6)];
1350        let d = ba.miners_damage(&loading);
1351        assert!(d > 0.0, "Miner's damage should be positive, got {d}");
1352    }
1353
1354    #[test]
1355    fn test_von_mises_stress_positive() {
1356        let vm =
1357            BiomechanicsAnalysis::von_mises_stress(100.0e6, 50.0e6, 20.0e6, 10.0e6, 5.0e6, 3.0e6);
1358        assert!(vm > 0.0, "Von Mises stress should be positive, got {vm}");
1359    }
1360
1361    #[test]
1362    fn test_von_mises_stress_hydrostatic_zero() {
1363        // Under hydrostatic (equal principal stresses, no shear), VM = 0
1364        let vm = BiomechanicsAnalysis::von_mises_stress(100.0e6, 100.0e6, 100.0e6, 0.0, 0.0, 0.0);
1365        assert!(
1366            vm.abs() < 1.0,
1367            "VM stress under hydrostatic loading should be 0, got {vm}"
1368        );
1369    }
1370
1371    #[test]
1372    fn test_tsai_wu_failure_below_one_safe() {
1373        let fi = BiomechanicsAnalysis::tsai_wu_failure_index(
1374            10.0e6, 5.0e6, 2.0e6, 120.0e6, 180.0e6, 60.0e6, 80.0e6, 50.0e6,
1375        );
1376        assert!(
1377            fi < 1.0,
1378            "Tsai-Wu index should be <1 for safe loading, got {fi}"
1379        );
1380    }
1381
1382    #[test]
1383    fn test_tsai_wu_failure_above_one_failed() {
1384        let fi = BiomechanicsAnalysis::tsai_wu_failure_index(
1385            200.0e6, 0.0, 0.0, 120.0e6, 180.0e6, 60.0e6, 80.0e6, 50.0e6,
1386        );
1387        assert!(
1388            fi > 1.0,
1389            "Tsai-Wu index should be >1 for overloaded case, got {fi}"
1390        );
1391    }
1392
1393    #[test]
1394    fn test_remodelling_stimulus_zero_in_dead_zone() {
1395        let stimulus = BiomechanicsAnalysis::remodelling_stimulus(1.0, 1.0, 0.1);
1396        assert!(
1397            stimulus.abs() < TOL,
1398            "remodelling stimulus should be 0 in dead zone"
1399        );
1400    }
1401
1402    #[test]
1403    fn test_remodelling_stimulus_positive_above_reference() {
1404        let stimulus = BiomechanicsAnalysis::remodelling_stimulus(2.0, 1.0, 0.05);
1405        assert!(
1406            stimulus > 0.0,
1407            "remodelling stimulus should be positive above reference"
1408        );
1409    }
1410
1411    #[test]
1412    fn test_max_principal_stress_failure_trigger() {
1413        let failed = BiomechanicsAnalysis {
1414            implant_modulus: 0.0,
1415            bone_modulus: 0.0,
1416            implant_area: 0.0,
1417            bone_area: 1.0,
1418            applied_load: 0.0,
1419            fatigue_exponent: 6.0,
1420            reference_fatigue_stress: 60.0e6,
1421            reference_cycles: 1.0e6,
1422        }
1423        .max_principal_stress_failure(200.0e6, 0.0, 0.0, 120.0e6, 180.0e6);
1424        assert!(
1425            failed,
1426            "should detect failure when stress exceeds tensile strength"
1427        );
1428    }
1429
1430    #[test]
1431    fn test_max_principal_stress_no_failure() {
1432        let ba = BiomechanicsAnalysis::hip_replacement();
1433        let safe = ba.max_principal_stress_failure(50.0e6, 30.0e6, 10.0e6, 120.0e6, 180.0e6);
1434        assert!(!safe, "should be safe when stresses are below strength");
1435    }
1436}