1#![allow(dead_code)]
10#![allow(clippy::too_many_arguments)]
11
12use std::f64::consts::PI;
13
14fn 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
54fn 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#[derive(Debug, Clone)]
77pub struct SoftTissue {
78 pub c: f64,
80 pub b: f64,
82 pub k1: f64,
84 pub k2: f64,
86 pub elastin_modulus: f64,
88 pub collagen_fraction: f64,
90 pub elastin_fraction: f64,
92 pub fiber_direction: [f64; 3],
94 pub density: f64,
96}
97
98impl SoftTissue {
99 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 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 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 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 let w_matrix = self.c / 2.0 * ((self.b * (i1 - 3.0)).exp() - 1.0);
147
148 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 let w_elastin = self.elastin_modulus / 2.0 * (i1 - 3.0);
158
159 w_matrix + w_fiber + w_elastin
160 }
161
162 pub fn small_strain_modulus(&self) -> f64 {
164 let dw_di1 = self.c * self.b + self.elastin_modulus;
166 4.0 * dw_di1
167 }
168
169 pub fn toe_region_stretch(&self) -> f64 {
173 1.0 + (1.0 / (2.0 * self.k2)).sqrt()
174 }
175
176 pub fn uniaxial_cauchy_stress(&self, lambda: f64) -> f64 {
179 if lambda <= 0.0 {
180 return 0.0;
181 }
182 let lambda_perp = 1.0 / lambda.sqrt();
184 let i1 = lambda * lambda + 2.0 * lambda_perp * lambda_perp;
185 let i4 = lambda * lambda; 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 2.0 * dw_di1 * (lambda * lambda - lambda_perp * lambda_perp)
199 + 2.0 * lambda * lambda * dw_di4
200 }
201}
202
203#[derive(Debug, Clone)]
212pub struct BoneMaterial {
213 pub e_longitudinal: f64,
215 pub e_transverse: f64,
217 pub g_lt: f64,
219 pub nu_lt: f64,
221 pub nu_tt: f64,
223 pub density: f64,
225 pub uts: f64,
227 pub ucs: f64,
229 pub volume_fraction: f64,
231 pub is_cortical: bool,
233}
234
235impl BoneMaterial {
236 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 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 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 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 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 pub fn modulus_from_ash_density(ash_density_g_cm3: f64) -> f64 {
293 6.95e9 * ash_density_g_cm3.powf(1.49)
295 }
296
297 pub fn anisotropy_ratio(&self) -> f64 {
299 self.e_longitudinal / self.e_transverse
300 }
301
302 pub fn longitudinal_wave_speed(&self) -> f64 {
304 (self.e_longitudinal / self.density).sqrt()
305 }
306
307 pub fn shear_wave_speed(&self) -> f64 {
309 (self.g_lt / self.density).sqrt()
310 }
311
312 pub fn compressive_yield_strain(&self) -> f64 {
314 self.ucs / self.e_longitudinal
315 }
316}
317
318#[derive(Debug, Clone)]
329pub struct CartilageModel {
330 pub aggregate_modulus: f64,
332 pub e_solid: f64,
334 pub nu_solid: f64,
336 pub permeability: f64,
338 pub thickness: f64,
340 pub porosity: f64,
342 pub fixed_charge_density: f64,
344 pub density: f64,
346}
347
348impl CartilageModel {
349 pub fn articular_cartilage() -> Self {
353 Self::human_knee_cartilage()
354 }
355
356 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 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 pub fn creep_time_constant(&self) -> f64 {
382 let h = self.thickness;
383 h * h / (self.aggregate_modulus * self.permeability)
384 }
385
386 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 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 pub fn donnan_swelling_pressure(&self, c_ext: f64) -> f64 {
401 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 pub fn darcy_fluid_flux(&self, dp_dz: f64) -> f64 {
410 self.permeability * dp_dz
411 }
412
413 pub fn strain_dependent_permeability(&self, e: f64) -> f64 {
417 let m = 4.0; self.permeability * (m * e).exp()
420 }
421}
422
423#[derive(Debug, Clone)]
432pub struct VascularWall {
433 pub inner_radius: f64,
435 pub outer_radius: f64,
437 pub opening_angle: f64,
439 pub intima_thickness: f64,
441 pub media_thickness: f64,
443 pub adventitia_thickness: f64,
445 pub c10: f64,
447 pub k1: f64,
449 pub k2: f64,
451 pub fiber_angle: f64,
453 pub collagen_fraction: f64,
455 pub smooth_muscle_fraction: f64,
457 pub density: f64,
459}
460
461impl VascularWall {
462 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 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 pub fn wall_thickness(&self) -> f64 {
502 self.outer_radius - self.inner_radius
503 }
504
505 pub fn residual_stretch_circumferential(&self, r: f64) -> f64 {
510 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 (r / r_mid) * (PI / (PI - alpha))
517 }
518
519 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 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 let d = 1.0 / (2.0 * self.c10); let w_vol = (j - 1.0).powi(2) / (2.0 * d);
535
536 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 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 pub fn laplace_tension(&self, internal_pressure: f64) -> f64 {
554 internal_pressure * self.inner_radius
555 }
556
557 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 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#[derive(Debug, Clone)]
582pub struct CellMechanics {
583 pub cortical_tension: f64,
585 pub radius: f64,
587 pub cytoskeletal_modulus: f64,
589 pub active_stress: f64,
591 pub adhesion_energy: f64,
593 pub density: f64,
595 pub num_stress_fibers: usize,
597 pub prestress: f64,
599}
600
601impl CellMechanics {
602 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 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 pub fn laplace_pressure(&self) -> f64 {
632 2.0 * self.cortical_tension / self.radius
633 }
634
635 pub fn effective_modulus(&self) -> f64 {
637 self.cytoskeletal_modulus + self.active_stress
638 }
639
640 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 pub fn membrane_bending_stiffness(&self) -> f64 {
656 let kbt = 4.28e-21_f64;
658 25.0 * kbt
659 }
660
661 pub fn volume(&self) -> f64 {
663 4.0 / 3.0 * PI * self.radius.powi(3)
664 }
665
666 pub fn osmotic_pressure(&self, volumetric_strain: f64) -> f64 {
670 -self.cytoskeletal_modulus * volumetric_strain
671 }
672}
673
674#[derive(Debug, Clone)]
683pub struct HydrogelModel {
684 pub phi0: f64,
686 pub phi_eq: f64,
688 pub chi: f64,
690 pub crosslink_density: f64,
692 pub dry_modulus: f64,
694 pub solvent_molar_volume: f64,
696 pub temperature: f64,
698}
699
700impl HydrogelModel {
701 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 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 const R: f64 = 8.314;
729
730 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 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 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 pub fn swelling_ratio(&self) -> f64 {
760 1.0 / self.phi_eq
761 }
762
763 pub fn linear_swelling_strain(&self) -> f64 {
765 self.swelling_ratio().powf(1.0 / 3.0) - 1.0
766 }
767
768 pub fn swollen_modulus(&self) -> f64 {
770 self.network_modulus()
771 }
772
773 pub fn effective_diffusivity(&self, d0: f64) -> f64 {
777 let phi = self.phi_eq;
779 d0 * (-0.84 * phi / (1.0 - phi)).exp()
780 }
781}
782
783#[derive(Debug, Clone)]
790pub struct BiomechanicsAnalysis {
791 pub implant_modulus: f64,
793 pub bone_modulus: f64,
795 pub implant_area: f64,
797 pub bone_area: f64,
799 pub applied_load: f64,
801 pub fatigue_exponent: f64,
803 pub reference_fatigue_stress: f64,
805 pub reference_cycles: f64,
807}
808
809impl BiomechanicsAnalysis {
810 pub fn hip_replacement() -> Self {
812 Self {
813 implant_modulus: 110.0e9, 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 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 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 pub fn bone_stress_without_implant(&self) -> f64 {
842 self.applied_load / self.bone_area
843 }
844
845 #[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(); 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 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 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 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 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 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#[cfg(test)]
950mod tests {
951 use super::*;
952
953 const TOL: f64 = 1e-6;
954
955 #[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 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 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 #[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; 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 #[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 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 #[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; 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 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 #[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 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 #[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; 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 #[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 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}