1pub type Vec3 = [f64; 3];
17
18pub type StrainTensor = [f64; 6];
21
22#[derive(Debug, Clone)]
37pub struct SoftTissue {
38 pub fung_c: f64,
40 pub fung_b: [f64; 6],
42 pub hgo_mu: f64,
44 pub hgo_k1: f64,
46 pub hgo_k2: f64,
48 pub hgo_kappa: f64,
50 pub fiber_angle: f64,
52 pub active_stress: f64,
54}
55
56impl SoftTissue {
57 pub fn new_fung(fung_c: f64, fung_b: [f64; 6]) -> Self {
63 Self {
64 fung_c,
65 fung_b,
66 hgo_mu: 0.0,
67 hgo_k1: 0.0,
68 hgo_k2: 0.0,
69 hgo_kappa: 0.0,
70 fiber_angle: 0.0,
71 active_stress: 0.0,
72 }
73 }
74
75 pub fn new_hgo(mu: f64, k1: f64, k2: f64, kappa: f64, fiber_angle: f64) -> Self {
84 Self {
85 fung_c: 0.0,
86 fung_b: [0.0; 6],
87 hgo_mu: mu,
88 hgo_k1: k1,
89 hgo_k2: k2,
90 hgo_kappa: kappa,
91 fiber_angle,
92 active_stress: 0.0,
93 }
94 }
95
96 pub fn fung_strain_energy(&self, strain: &StrainTensor) -> f64 {
101 let [e11, e22, e33, _e12, _e13, _e23] = *strain;
102 let [b11, b22, b33, b12, b13, b23] = self.fung_b;
103 let q = b11 * e11 * e11
104 + b22 * e22 * e22
105 + b33 * e33 * e33
106 + 2.0 * b12 * e11 * e22
107 + 2.0 * b13 * e11 * e33
108 + 2.0 * b23 * e22 * e33;
109 self.fung_c * (q.exp() - 1.0)
110 }
111
112 pub fn fung_stress_s11(&self, strain: &StrainTensor) -> f64 {
116 let [e11, e22, e33, _e12, _e13, _e23] = *strain;
117 let [b11, b22, b33, b12, b13, b23] = self.fung_b;
118 let q = b11 * e11 * e11
119 + b22 * e22 * e22
120 + b33 * e33 * e33
121 + 2.0 * b12 * e11 * e22
122 + 2.0 * b13 * e11 * e33
123 + 2.0 * b23 * e22 * e33;
124 2.0 * self.fung_c * (b11 * e11 + b12 * e22 + b13 * e33) * q.exp()
125 }
126
127 pub fn hgo_i4(&self, stretch_circ: f64, stretch_axial: f64) -> f64 {
132 let cos_a = self.fiber_angle.cos();
133 let sin_a = self.fiber_angle.sin();
134 let c11 = stretch_circ * stretch_circ;
136 let c22 = stretch_axial * stretch_axial;
137 cos_a * cos_a * c11 + sin_a * sin_a * c22
138 }
139
140 #[allow(clippy::too_many_arguments)]
145 pub fn hgo_fiber_energy(
146 &self,
147 stretch_circ: f64,
148 stretch_axial: f64,
149 stretch_radial: f64,
150 ) -> f64 {
151 let i1 = stretch_circ * stretch_circ
152 + stretch_axial * stretch_axial
153 + stretch_radial * stretch_radial;
154 let i4 = self.hgo_i4(stretch_circ, stretch_axial);
155 if i4 <= 1.0 {
157 return 0.0;
158 }
159 let e_val = self.hgo_kappa * i1 + (1.0 - 3.0 * self.hgo_kappa) * i4 - 1.0;
160 2.0 * self.hgo_k1 / (2.0 * self.hgo_k2) * ((self.hgo_k2 * e_val * e_val).exp() - 1.0)
161 }
162
163 pub fn hgo_total_energy(
165 &self,
166 stretch_circ: f64,
167 stretch_axial: f64,
168 stretch_radial: f64,
169 ) -> f64 {
170 let i1 = stretch_circ * stretch_circ
171 + stretch_axial * stretch_axial
172 + stretch_radial * stretch_radial;
173 let w_iso = self.hgo_mu / 2.0 * (i1 - 3.0);
174 let w_fiber = self.hgo_fiber_energy(stretch_circ, stretch_axial, stretch_radial);
175 w_iso + w_fiber
176 }
177
178 pub fn total_stress_with_active(&self, strain: &StrainTensor) -> f64 {
182 self.fung_stress_s11(strain) + self.active_stress
183 }
184
185 pub fn set_active_stress(&mut self, sigma_a: f64) {
187 self.active_stress = sigma_a;
188 }
189}
190
191#[derive(Debug, Clone)]
203pub struct BoneModel {
204 pub density: f64,
206 pub mineral_fraction: f64,
208 pub e_axial: f64,
210 pub e_transverse: f64,
212 pub shear_modulus: f64,
214 pub trabecular_thickness: f64,
216 pub trabecular_length: f64,
218}
219
220impl BoneModel {
221 pub fn new(
223 density: f64,
224 mineral_fraction: f64,
225 e_axial: f64,
226 e_transverse: f64,
227 shear_modulus: f64,
228 ) -> Self {
229 Self {
230 density,
231 mineral_fraction,
232 e_axial,
233 e_transverse,
234 shear_modulus,
235 trabecular_thickness: 0.15,
236 trabecular_length: 1.2,
237 }
238 }
239
240 pub fn density_to_modulus_kk(density: f64) -> f64 {
244 6850.0 * density.powf(1.49)
245 }
246
247 pub fn gibson_ashby_modulus(&self, solid_modulus_gpa: f64) -> f64 {
251 let rho_s = 1.9_f64; let relative_density = (self.density / rho_s).min(1.0);
253 solid_modulus_gpa * relative_density * relative_density
254 }
255
256 pub fn trabecular_modulus(&self) -> f64 {
260 self.gibson_ashby_modulus(18.0)
262 }
263
264 pub fn wolff_adapt(&mut self, sigma_actual: f64, sigma_ref: f64, k: f64, s: f64, dt: f64) {
271 let error = sigma_actual - sigma_ref;
272 let d_rho = if error > s {
273 k * (error - s)
274 } else if error < -s {
275 k * (error + s)
276 } else {
277 0.0
278 };
279 self.density = (self.density + d_rho * dt).clamp(0.05, 1.9);
280 }
281
282 pub fn yield_strength(&self) -> f64 {
286 94.0 * self.mineral_fraction.powf(3.84)
287 }
288}
289
290#[derive(Debug, Clone)]
301pub struct BloodRheology {
302 pub eta_0: f64,
304 pub eta_inf: f64,
306 pub lambda: f64,
308 pub carreau_a: f64,
310 pub power_n: f64,
312 pub yield_stress: f64,
314 pub casson_eta: f64,
316 pub hematocrit: f64,
318}
319
320impl BloodRheology {
321 pub fn new_physiological() -> Self {
325 Self {
326 eta_0: 0.056, eta_inf: 0.0035, lambda: 3.313,
329 carreau_a: 2.0,
330 power_n: 0.3568,
331 yield_stress: 0.004, casson_eta: 0.0035,
333 hematocrit: 0.45,
334 }
335 }
336
337 #[allow(clippy::too_many_arguments)]
339 pub fn new(
340 eta_0: f64,
341 eta_inf: f64,
342 lambda: f64,
343 carreau_a: f64,
344 power_n: f64,
345 yield_stress: f64,
346 casson_eta: f64,
347 hematocrit: f64,
348 ) -> Self {
349 Self {
350 eta_0,
351 eta_inf,
352 lambda,
353 carreau_a,
354 power_n,
355 yield_stress,
356 casson_eta,
357 hematocrit,
358 }
359 }
360
361 pub fn carreau_viscosity(&self, shear_rate: f64) -> f64 {
365 let base = 1.0 + (self.lambda * shear_rate.abs()).powf(self.carreau_a);
366 self.eta_inf
367 + (self.eta_0 - self.eta_inf) * base.powf((self.power_n - 1.0) / self.carreau_a)
368 }
369
370 pub fn casson_stress(&self, shear_rate: f64) -> f64 {
374 let gamma_abs = shear_rate.abs();
375 if gamma_abs < 1e-12 {
376 return 0.0;
377 }
378 let sqrt_tau = self.yield_stress.sqrt() + (self.casson_eta * gamma_abs).sqrt();
379 sqrt_tau * sqrt_tau * shear_rate.signum()
380 }
381
382 pub fn casson_viscosity(&self, shear_rate: f64) -> f64 {
384 let gamma_abs = shear_rate.abs();
385 if gamma_abs < 1e-12 {
386 return self.eta_0; }
388 self.casson_stress(shear_rate) / shear_rate
389 }
390
391 pub fn hematocrit_viscosity(&self) -> f64 {
395 let h = self.hematocrit.clamp(0.0, 0.99);
396 self.eta_inf / (1.0 - h / 2.0).powi(2)
397 }
398
399 pub fn is_newtonian_limit(&self, shear_rate: f64) -> bool {
403 let eta = self.carreau_viscosity(shear_rate);
404 (eta - self.eta_inf).abs() / (self.eta_0 - self.eta_inf + 1e-20) < 0.01
405 }
406}
407
408#[derive(Debug, Clone)]
417pub struct HydrogelModel {
418 pub chi: f64,
420 pub n_chains: f64,
422 pub nu_0: f64,
424 pub molar_volume_solvent: f64,
426 pub shear_modulus: f64,
428}
429
430impl HydrogelModel {
431 pub fn new(chi: f64, n_chains: f64, shear_modulus: f64) -> Self {
433 Self {
434 chi,
435 n_chains,
436 nu_0: 1.0,
437 molar_volume_solvent: 18e-6, shear_modulus,
439 }
440 }
441
442 pub fn mixing_chemical_potential(&self, phi: f64) -> f64 {
446 (1.0 - phi).ln() + phi + self.chi * phi * phi
447 }
448
449 pub fn elastic_chemical_potential(&self, phi: f64) -> f64 {
453 (1.0 / self.n_chains) * (phi / 2.0 - phi.powf(1.0 / 3.0))
455 }
456
457 pub fn total_chemical_potential(&self, phi: f64) -> f64 {
459 self.mixing_chemical_potential(phi) + self.elastic_chemical_potential(phi)
460 }
461
462 pub fn equilibrium_swelling_ratio(&self) -> f64 {
467 let mut lo = 1e-4_f64;
469 let mut hi = 1.0 - 1e-4;
470 let mu_lo = self.total_chemical_potential(lo);
471 let mu_hi = self.total_chemical_potential(hi);
472 if mu_lo * mu_hi > 0.0 {
474 if mu_lo.abs() < mu_hi.abs() {
475 return 1.0 / lo;
476 } else {
477 return 1.0 / hi;
478 }
479 }
480 for _ in 0..60 {
481 let mid = 0.5 * (lo + hi);
482 if self.total_chemical_potential(lo) * self.total_chemical_potential(mid) <= 0.0 {
483 hi = mid;
484 } else {
485 lo = mid;
486 }
487 }
488 let phi_eq = 0.5 * (lo + hi);
489 1.0 / phi_eq
490 }
491}
492
493#[derive(Debug, Clone)]
505pub struct CartilageModel {
506 pub aggregate_modulus: f64,
508 pub permeability: f64,
510 pub poisson_solid: f64,
512 pub thickness_mm: f64,
514}
515
516impl CartilageModel {
517 pub fn new(
519 aggregate_modulus: f64,
520 permeability: f64,
521 poisson_solid: f64,
522 thickness_mm: f64,
523 ) -> Self {
524 Self {
525 aggregate_modulus,
526 permeability,
527 poisson_solid,
528 thickness_mm,
529 }
530 }
531
532 pub fn characteristic_time(&self) -> f64 {
536 let h_m = self.thickness_mm * 1e-3;
537 h_m * h_m / (self.aggregate_modulus * 1e6 * self.permeability)
538 }
539
540 pub fn creep_response(&self, sigma0: f64, time: f64) -> f64 {
544 let t_star = self.characteristic_time();
545 let u_inf = sigma0 / self.aggregate_modulus;
546 u_inf * (1.0 - (-time / t_star).exp())
547 }
548
549 pub fn stress_relaxation(&self, epsilon0: f64, time: f64) -> f64 {
553 let t_star = self.characteristic_time();
554 let sigma0 = self.aggregate_modulus * epsilon0;
555 sigma0 * (-time / t_star).exp()
556 }
557
558 pub fn lame_lambda(&self) -> f64 {
560 let nu = self.poisson_solid;
561 let e = 2.0 * self.aggregate_modulus * (1.0 - nu) / (1.0 - 2.0 * nu);
562 e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
563 }
564}
565
566#[derive(Debug, Clone)]
575pub struct ArterialWall {
576 pub intima: SoftTissue,
578 pub media: SoftTissue,
580 pub adventitia: SoftTissue,
582 pub layer_fractions: [f64; 3],
584 pub opening_angle: f64,
586 pub inner_radius: f64,
588 pub outer_radius: f64,
590}
591
592impl ArterialWall {
593 pub fn new_aorta() -> Self {
595 let intima = SoftTissue::new_hgo(7.64e3, 996.6, 524.6, 0.226, 0.523);
596 let media = SoftTissue::new_hgo(3.0e3, 15.0e3, 20.0, 0.226, 0.464);
597 let adventitia = SoftTissue::new_hgo(0.3e3, 57.0e3, 11.2, 0.226, 0.785);
598 Self {
599 intima,
600 media,
601 adventitia,
602 layer_fractions: [0.1, 0.5, 0.4],
603 opening_angle: 0.541, inner_radius: 9.0,
605 outer_radius: 13.5,
606 }
607 }
608
609 pub fn residual_strain_inner(&self) -> f64 {
613 let alpha = std::f64::consts::PI - self.opening_angle;
615 let lambda = std::f64::consts::PI * self.inner_radius / (alpha * self.inner_radius);
616 lambda - 1.0
617 }
618
619 pub fn wall_stress_at_stretch(&self, lambda_circ: f64) -> f64 {
623 let lambda_axial = 1.1; let lambda_radial = 1.0 / (lambda_circ * lambda_axial); let s_i = self
626 .intima
627 .hgo_total_energy(lambda_circ, lambda_axial, lambda_radial);
628 let s_m = self
629 .media
630 .hgo_total_energy(lambda_circ, lambda_axial, lambda_radial);
631 let s_a = self
632 .adventitia
633 .hgo_total_energy(lambda_circ, lambda_axial, lambda_radial);
634 self.layer_fractions[0] * s_i
635 + self.layer_fractions[1] * s_m
636 + self.layer_fractions[2] * s_a
637 }
638}
639
640#[derive(Debug, Clone)]
649pub struct DegradablePolymer {
650 pub mw_initial: f64,
652 pub mw_current: f64,
654 pub hydrolysis_rate: f64,
656 pub autocatalysis: f64,
658 pub mw_critical: f64,
660}
661
662impl DegradablePolymer {
663 pub fn new_plga_50_50() -> Self {
665 Self {
666 mw_initial: 100_000.0, mw_current: 100_000.0,
668 hydrolysis_rate: 0.003, autocatalysis: 0.5,
670 mw_critical: 5_000.0, }
672 }
673
674 pub fn new(
676 mw_initial: f64,
677 hydrolysis_rate: f64,
678 autocatalysis: f64,
679 mw_critical: f64,
680 ) -> Self {
681 Self {
682 mw_initial,
683 mw_current: mw_initial,
684 hydrolysis_rate,
685 autocatalysis,
686 mw_critical,
687 }
688 }
689
690 pub fn step(&mut self, dt: f64) {
694 let degradation_factor =
695 1.0 + self.autocatalysis * (self.mw_initial - self.mw_current) / self.mw_initial;
696 let rate = self.hydrolysis_rate * degradation_factor * self.mw_current;
697 self.mw_current = (self.mw_current - rate * dt).max(self.mw_critical * 0.5);
698 }
699
700 pub fn run(&mut self, days: f64, dt: f64) {
702 let steps = (days / dt).ceil() as usize;
703 for _ in 0..steps {
704 self.step(dt);
705 }
706 }
707
708 pub fn mw_retention(&self) -> f64 {
710 self.mw_current / self.mw_initial
711 }
712
713 pub fn is_failed(&self) -> bool {
715 self.mw_current <= self.mw_critical
716 }
717
718 pub fn modulus_retention(&self) -> f64 {
722 self.mw_retention().sqrt()
723 }
724}
725
726#[cfg(test)]
731mod tests {
732 use super::*;
733
734 const EPS: f64 = 1e-10;
735
736 #[test]
740 fn test_fung_zero_strain_zero_energy() {
741 let tissue = SoftTissue::new_fung(0.5e3, [1.0, 1.0, 1.0, 0.5, 0.5, 0.5]);
742 let w = tissue.fung_strain_energy(&[0.0; 6]);
743 assert!(w.abs() < EPS, "W at zero strain should be 0, got {w}");
744 }
745
746 #[test]
748 fn test_fung_zero_strain_zero_stress() {
749 let tissue = SoftTissue::new_fung(0.5e3, [1.0, 1.0, 1.0, 0.5, 0.5, 0.5]);
750 let s = tissue.fung_stress_s11(&[0.0; 6]);
751 assert!(s.abs() < EPS, "S11 at zero strain should be 0, got {s}");
752 }
753
754 #[test]
756 fn test_fung_stress_increases_with_strain() {
757 let tissue = SoftTissue::new_fung(0.5e3, [2.0, 1.0, 1.0, 0.5, 0.5, 0.5]);
758 let s1 = tissue.fung_stress_s11(&[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]);
759 let s2 = tissue.fung_stress_s11(&[0.2, 0.0, 0.0, 0.0, 0.0, 0.0]);
760 assert!(
761 s2 > s1,
762 "Fung stress should increase with strain: s1={s1:.6}, s2={s2:.6}"
763 );
764 }
765
766 #[test]
768 fn test_fung_energy_positive() {
769 let tissue = SoftTissue::new_fung(1.0e3, [1.0, 1.0, 1.0, 0.0, 0.0, 0.0]);
770 let w = tissue.fung_strain_energy(&[0.1, 0.05, 0.02, 0.0, 0.0, 0.0]);
771 assert!(w > 0.0, "Fung energy should be positive, got {w}");
772 }
773
774 #[test]
776 fn test_fung_stress_exponential_growth() {
777 let tissue = SoftTissue::new_fung(1.0, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
778 let strains = [0.1, 0.2, 0.4, 0.8];
779 let mut prev = 0.0_f64;
780 for &e in &strains {
781 let s = tissue.fung_stress_s11(&[e, 0.0, 0.0, 0.0, 0.0, 0.0]);
782 assert!(
783 s > prev,
784 "stress at {e} ({s:.6}) should exceed prev ({prev:.6})"
785 );
786 prev = s;
787 }
788 }
789
790 #[test]
792 fn test_active_stress_adds() {
793 let mut tissue = SoftTissue::new_fung(1.0e3, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
794 let passive = tissue.fung_stress_s11(&[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]);
795 tissue.set_active_stress(500.0);
796 let total = tissue.total_stress_with_active(&[0.1, 0.0, 0.0, 0.0, 0.0, 0.0]);
797 assert!(
798 (total - passive - 500.0).abs() < 1e-6,
799 "Total stress should be passive + active, got {total:.6}"
800 );
801 }
802
803 #[test]
807 fn test_hgo_no_fiber_compression() {
808 let tissue = SoftTissue::new_hgo(1.0e3, 1.0e4, 10.0, 0.1, 0.0);
809 let w = tissue.hgo_fiber_energy(0.9, 1.0, 1.0 / 0.9);
811 assert_eq!(
812 w, 0.0,
813 "Fiber energy should be 0 under compression, got {w}"
814 );
815 }
816
817 #[test]
819 fn test_hgo_fiber_tension_positive() {
820 let tissue = SoftTissue::new_hgo(1.0e3, 1.0e4, 10.0, 0.1, 0.0);
821 let w = tissue.hgo_fiber_energy(1.2, 1.0, 1.0 / 1.2);
822 assert!(
823 w > 0.0,
824 "Fiber energy should be positive under tension, got {w}"
825 );
826 }
827
828 #[test]
830 fn test_hgo_total_energy_geq_isotropic() {
831 let tissue = SoftTissue::new_hgo(3.0e3, 1.5e4, 5.0, 0.1, 0.5);
832 let w_total = tissue.hgo_total_energy(1.3, 1.1, 1.0 / (1.3 * 1.1));
833 let radial = 1.0_f64 / (1.3 * 1.1);
834 let i1 = 1.3f64 * 1.3 + 1.1_f64 * 1.1 + radial * radial;
835 let w_iso = tissue.hgo_mu / 2.0 * (i1 - 3.0);
836 assert!(
837 w_total >= w_iso,
838 "Total energy should be ≥ isotropic: w_total={w_total:.6}, w_iso={w_iso:.6}"
839 );
840 }
841
842 #[test]
844 fn test_hgo_i4_identity() {
845 let tissue = SoftTissue::new_hgo(1.0e3, 1.0e4, 5.0, 0.1, 0.3);
846 let i4 = tissue.hgo_i4(1.0, 1.0);
847 assert!(
848 (i4 - 1.0).abs() < 1e-10,
849 "I4 at identity stretch should be 1, got {i4}"
850 );
851 }
852
853 #[test]
857 fn test_bone_density_modulus_increases() {
858 let e1 = BoneModel::density_to_modulus_kk(0.3);
859 let e2 = BoneModel::density_to_modulus_kk(0.6);
860 let e3 = BoneModel::density_to_modulus_kk(1.0);
861 assert!(e1 < e2 && e2 < e3, "Modulus should increase with density");
862 }
863
864 #[test]
866 fn test_bone_gibson_ashby_scaling() {
867 let bone1 = BoneModel::new(0.5, 0.6, 18.0, 12.0, 3.5);
868 let bone2 = BoneModel::new(1.0, 0.6, 18.0, 12.0, 3.5);
869 let e1 = bone1.gibson_ashby_modulus(18.0);
870 let e2 = bone2.gibson_ashby_modulus(18.0);
871 let expected_ratio = 4.0; assert!(
873 (e2 / e1 - expected_ratio).abs() < 0.01,
874 "Gibson-Ashby should give 4x modulus for 2x density: ratio={:.6}",
875 e2 / e1
876 );
877 }
878
879 #[test]
881 fn test_wolff_law_density_increases() {
882 let mut bone = BoneModel::new(0.5, 0.6, 12.0, 8.0, 3.0);
883 let initial_rho = bone.density;
884 bone.wolff_adapt(10.0, 5.0, 0.1, 0.5, 1.0);
885 assert!(
886 bone.density > initial_rho,
887 "Density should increase under high stress, got {:.4}",
888 bone.density
889 );
890 }
891
892 #[test]
894 fn test_wolff_law_density_decreases() {
895 let mut bone = BoneModel::new(1.0, 0.7, 18.0, 12.0, 3.5);
896 let initial_rho = bone.density;
897 bone.wolff_adapt(1.0, 8.0, 0.1, 0.5, 1.0);
898 assert!(
899 bone.density < initial_rho,
900 "Density should decrease under disuse, got {:.4}",
901 bone.density
902 );
903 }
904
905 #[test]
907 fn test_bone_yield_strength() {
908 let bone1 = BoneModel::new(1.5, 0.5, 18.0, 12.0, 3.5);
909 let bone2 = BoneModel::new(1.5, 0.7, 18.0, 12.0, 3.5);
910 assert!(
911 bone2.yield_strength() > bone1.yield_strength(),
912 "Higher mineral fraction → higher yield strength"
913 );
914 }
915
916 #[test]
920 fn test_blood_shear_thinning() {
921 let blood = BloodRheology::new_physiological();
922 let eta1 = blood.carreau_viscosity(1.0);
923 let eta2 = blood.carreau_viscosity(100.0);
924 assert!(
925 eta2 < eta1,
926 "Blood should shear-thin: η(1)={eta1:.6}, η(100)={eta2:.6}"
927 );
928 }
929
930 #[test]
932 fn test_blood_high_shear_newtonian() {
933 let blood = BloodRheology::new_physiological();
934 assert!(
935 blood.is_newtonian_limit(10000.0),
936 "Blood should be Newtonian at very high shear rate"
937 );
938 }
939
940 #[test]
942 fn test_casson_zero_shear() {
943 let blood = BloodRheology::new_physiological();
944 let tau = blood.casson_stress(0.0);
945 assert!(
946 tau.abs() < EPS,
947 "Casson stress at zero shear should be 0, got {tau}"
948 );
949 }
950
951 #[test]
953 fn test_casson_stress_increases() {
954 let blood = BloodRheology::new_physiological();
955 let tau1 = blood.casson_stress(10.0);
956 let tau2 = blood.casson_stress(100.0);
957 assert!(tau2 > tau1, "Casson stress should increase with shear rate");
958 }
959
960 #[test]
962 fn test_hematocrit_viscosity_greater_than_plasma() {
963 let blood = BloodRheology::new_physiological();
964 assert!(
965 blood.hematocrit_viscosity() > blood.eta_inf,
966 "Blood viscosity should exceed plasma viscosity"
967 );
968 }
969
970 #[test]
972 fn test_casson_positive_shear_positive_stress() {
973 let blood = BloodRheology::new_physiological();
974 assert!(
975 blood.casson_stress(5.0) > 0.0,
976 "Positive shear should give positive Casson stress"
977 );
978 }
979
980 #[test]
982 fn test_carreau_bounds() {
983 let blood = BloodRheology::new_physiological();
984 for &gamma in &[0.001, 0.1, 1.0, 10.0, 1000.0] {
985 let eta = blood.carreau_viscosity(gamma);
986 assert!(
987 eta >= blood.eta_inf && eta <= blood.eta_0,
988 "Carreau viscosity out of [η_∞, η_0] at γ̇={gamma}: η={eta:.6}"
989 );
990 }
991 }
992
993 #[test]
997 fn test_hydrogel_mixing_potential_negative_low_phi() {
998 let gel = HydrogelModel::new(0.4, 50.0, 1.0e4);
999 let mu = gel.mixing_chemical_potential(0.01);
1000 assert!(
1001 mu < 0.0,
1002 "Mixing potential should be negative at low phi, got {mu:.6}"
1003 );
1004 }
1005
1006 #[test]
1008 fn test_hydrogel_swelling_ratio_gt1() {
1009 let gel = HydrogelModel::new(0.3, 100.0, 1.0e4);
1010 let q = gel.equilibrium_swelling_ratio();
1011 assert!(
1012 q > 1.0,
1013 "Equilibrium swelling ratio should be > 1, got {q:.6}"
1014 );
1015 }
1016
1017 #[test]
1023 fn test_hydrogel_crosslink_reduces_swelling() {
1024 let gel_loose = HydrogelModel::new(0.3, 500.0, 1.0e3);
1027 let gel_tight = HydrogelModel::new(0.3, 10.0, 1.0e3);
1028 let phi = 0.1;
1029 let mu_loose = gel_loose.elastic_chemical_potential(phi);
1030 let mu_tight = gel_tight.elastic_chemical_potential(phi);
1031 assert!(
1033 mu_tight.abs() > mu_loose.abs(),
1034 "Tighter cross-linking should have larger |elastic potential|: |mu_tight|={:.6}, |mu_loose|={:.6}",
1035 mu_tight.abs(),
1036 mu_loose.abs()
1037 );
1038 }
1039
1040 #[test]
1044 fn test_cartilage_char_time_positive() {
1045 let cart = CartilageModel::new(0.79, 1.16e-15, 0.1, 2.0);
1046 let t_star = cart.characteristic_time();
1047 assert!(
1048 t_star > 0.0,
1049 "Characteristic time should be positive, got {t_star}"
1050 );
1051 }
1052
1053 #[test]
1055 fn test_cartilage_creep_increases() {
1056 let cart = CartilageModel::new(0.79, 1.16e-15, 0.1, 2.0);
1057 let u1 = cart.creep_response(0.5, 100.0);
1058 let u2 = cart.creep_response(0.5, 1000.0);
1059 assert!(u2 > u1, "Creep deformation should increase with time");
1060 }
1061
1062 #[test]
1064 fn test_cartilage_stress_relaxation_decreases() {
1065 let cart = CartilageModel::new(0.79, 1.16e-15, 0.1, 2.0);
1066 let s1 = cart.stress_relaxation(0.1, 100.0);
1067 let s2 = cart.stress_relaxation(0.1, 1000.0);
1068 assert!(s2 < s1, "Stress relaxation should decrease with time");
1069 }
1070
1071 #[test]
1075 fn test_arterial_wall_stress_increases() {
1076 let wall = ArterialWall::new_aorta();
1077 let s1 = wall.wall_stress_at_stretch(1.1);
1078 let s2 = wall.wall_stress_at_stretch(1.5);
1079 assert!(
1080 s2 > s1,
1081 "Wall stress should increase with stretch: s1={s1:.6}, s2={s2:.6}"
1082 );
1083 }
1084
1085 #[test]
1087 fn test_arterial_layer_fractions_sum_to_one() {
1088 let wall = ArterialWall::new_aorta();
1089 let sum: f64 = wall.layer_fractions.iter().sum();
1090 assert!(
1091 (sum - 1.0).abs() < 1e-10,
1092 "Layer fractions should sum to 1, got {sum:.6}"
1093 );
1094 }
1095
1096 #[test]
1100 fn test_polymer_mw_decreases() {
1101 let mut poly = DegradablePolymer::new_plga_50_50();
1102 poly.run(100.0, 1.0);
1103 assert!(
1104 poly.mw_current < poly.mw_initial,
1105 "MW should decrease after degradation, got {:.0}",
1106 poly.mw_current
1107 );
1108 }
1109
1110 #[test]
1112 fn test_polymer_mw_retention_bounded() {
1113 let mut poly = DegradablePolymer::new_plga_50_50();
1114 poly.run(30.0, 1.0);
1115 let ret = poly.mw_retention();
1116 assert!(
1117 (0.0..=1.0).contains(&ret),
1118 "MW retention should be in [0, 1], got {ret:.6}"
1119 );
1120 }
1121
1122 #[test]
1124 fn test_polymer_modulus_retention_leq1() {
1125 let mut poly = DegradablePolymer::new_plga_50_50();
1126 poly.run(50.0, 1.0);
1127 let e_ret = poly.modulus_retention();
1128 assert!(
1129 e_ret <= 1.0,
1130 "Modulus retention should be ≤ 1, got {e_ret:.6}"
1131 );
1132 }
1133
1134 #[test]
1136 fn test_polymer_higher_rate_more_degradation() {
1137 let mut slow = DegradablePolymer::new(100_000.0, 0.001, 0.5, 5_000.0);
1138 let mut fast = DegradablePolymer::new(100_000.0, 0.01, 0.5, 5_000.0);
1139 slow.run(100.0, 1.0);
1140 fast.run(100.0, 1.0);
1141 assert!(
1142 fast.mw_current < slow.mw_current,
1143 "Higher rate should degrade more: fast={:.0}, slow={:.0}",
1144 fast.mw_current,
1145 slow.mw_current
1146 );
1147 }
1148
1149 #[test]
1151 fn test_polymer_autocatalysis_accelerates() {
1152 let mut no_auto = DegradablePolymer::new(100_000.0, 0.003, 0.0, 1_000.0);
1153 let mut with_auto = DegradablePolymer::new(100_000.0, 0.003, 2.0, 1_000.0);
1154 no_auto.run(100.0, 1.0);
1155 with_auto.run(100.0, 1.0);
1156 assert!(
1157 with_auto.mw_current < no_auto.mw_current,
1158 "Autocatalysis should accelerate degradation"
1159 );
1160 }
1161}