1#![allow(dead_code)]
10#![allow(unused_imports)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15#[derive(Clone, Debug)]
21pub struct CorticalBone {
22 pub e_long: f64,
24 pub e_trans: f64,
26 pub g_lt: f64,
28 pub nu_lt: f64,
30 pub nu_tt: f64,
32 pub density: f64,
34 pub uts: f64,
36 pub ucs: f64,
38 pub k_ic: f64,
40}
41
42impl CorticalBone {
43 pub fn femur_cortical() -> Self {
45 Self::human_femur()
46 }
47
48 pub fn human_femur() -> Self {
50 Self {
51 e_long: 20.0e9,
52 e_trans: 12.0e9,
53 g_lt: 4.5e9,
54 nu_lt: 0.29,
55 nu_tt: 0.63,
56 density: 1900.0,
57 uts: 120.0e6,
58 ucs: 180.0e6,
59 k_ic: 2.5e6,
60 }
61 }
62
63 pub fn longitudinal_wave_speed(&self) -> f64 {
65 (self.e_long / self.density).sqrt()
66 }
67
68 pub fn transverse_wave_speed(&self) -> f64 {
70 (self.g_lt / self.density).sqrt()
71 }
72
73 pub fn apparent_density(vf: f64) -> f64 {
75 vf * 1900.0
77 }
78
79 pub fn modulus_from_density_power_law(density_g_cm3: f64) -> f64 {
81 let a = 2.065e10;
83 let b = 3.09;
84 a * density_g_cm3.powf(b)
85 }
86
87 pub fn bending_stiffness(&self, outer_r: f64, inner_r: f64, length: f64) -> f64 {
91 let i = PI / 4.0 * (outer_r.powi(4) - inner_r.powi(4));
92 48.0 * self.e_long * i / length.powi(3)
93 }
94
95 pub fn three_point_bending_stress(
97 &self,
98 force: f64,
99 length: f64,
100 outer_r: f64,
101 inner_r: f64,
102 ) -> f64 {
103 let i = PI / 4.0 * (outer_r.powi(4) - inner_r.powi(4));
104 force * length * outer_r / (4.0 * i)
105 }
106}
107
108#[derive(Clone, Debug)]
110pub struct CancellousBone {
111 pub relative_density: f64,
113 pub apparent_density: f64,
115 pub apparent_modulus: f64,
117 pub apparent_strength: f64,
119 pub anisotropy_ratio: f64,
121}
122
123impl CancellousBone {
124 pub fn from_relative_density(rel_density: f64) -> Self {
128 let e_solid = 20.0e9;
130 let sigma_solid = 170.0e6;
131 let c1 = 1.0;
132 let c2 = 0.2;
133 let apparent_modulus = c1 * e_solid * rel_density.powi(2);
134 let apparent_strength = c2 * sigma_solid * rel_density.powf(1.5);
135 Self {
136 relative_density: rel_density,
137 apparent_density: rel_density * 1900.0,
138 apparent_modulus,
139 apparent_strength,
140 anisotropy_ratio: 1.5,
141 }
142 }
143
144 pub fn energy_absorption(&self) -> f64 {
146 let densification_strain = 1.0 - 1.4 * self.relative_density;
148 0.5 * self.apparent_strength * densification_strain.max(0.0)
149 }
150
151 pub fn failure_strain(&self) -> f64 {
153 0.07 * (self.relative_density / 0.2).powf(-0.3)
155 }
156}
157
158pub fn cortical_bone_stiffness_tensor(
162 e_l: f64,
163 e_t: f64,
164 g_lt: f64,
165 nu_lt: f64,
166 nu_tt: f64,
167) -> [[f64; 6]; 6] {
168 let nu_tl = nu_lt * e_t / e_l;
170 let det = 1.0 - nu_tt * nu_tt - 2.0 * nu_lt * nu_tl;
171 let mut c = [[0.0f64; 6]; 6];
172 c[0][0] = e_l * (1.0 - nu_tt * nu_tt) / det;
173 c[1][1] = e_t * (1.0 - nu_lt * nu_tl) / det;
174 c[2][2] = c[1][1];
175 c[0][1] = e_l * (nu_tl + nu_tt * nu_tl) / det;
176 c[1][0] = c[0][1];
177 c[0][2] = c[0][1];
178 c[2][0] = c[0][2];
179 c[1][2] = e_t * (nu_tt + nu_lt * nu_tl) / det;
180 c[2][1] = c[1][2];
181 c[3][3] = g_lt;
182 c[4][4] = g_lt;
183 c[5][5] = e_t / (2.0 * (1.0 + nu_tt));
184 c
185}
186
187#[derive(Clone, Debug)]
193pub struct CartilageBiphasic {
194 pub ha: f64,
196 pub mu_s: f64,
198 pub permeability: f64,
200 pub thickness: f64,
202 pub density: f64,
204}
205
206impl CartilageBiphasic {
207 pub fn superficial_zone() -> Self {
209 Self {
210 ha: 0.42e6,
211 mu_s: 0.10e6,
212 permeability: 4.0e-15,
213 thickness: 2.0e-3,
214 density: 1100.0,
215 }
216 }
217
218 pub fn middle_zone() -> Self {
220 Self {
221 ha: 0.60e6,
222 mu_s: 0.15e6,
223 permeability: 2.0e-15,
224 thickness: 2.5e-3,
225 density: 1100.0,
226 }
227 }
228
229 pub fn deep_zone() -> Self {
231 Self {
232 ha: 0.85e6,
233 mu_s: 0.20e6,
234 permeability: 1.0e-15,
235 thickness: 3.0e-3,
236 density: 1100.0,
237 }
238 }
239
240 pub fn knee() -> Self {
242 Self {
243 ha: 0.7e6,
244 mu_s: 0.15e6,
245 permeability: 2.17e-15,
246 thickness: 2.5e-3,
247 density: 1100.0,
248 }
249 }
250
251 pub fn creep_time_constant(&self) -> f64 {
253 self.thickness * self.thickness / (self.ha * self.permeability)
254 }
255
256 pub fn steady_state_strain(&self, sigma: f64) -> f64 {
258 sigma / self.ha
259 }
260
261 pub fn creep_displacement(&self, sigma: f64, t: f64) -> f64 {
263 let tau = self.creep_time_constant();
264 let eps_inf = self.steady_state_strain(sigma);
265 let correction = (1.0 - (-t / tau * PI * PI / 4.0).exp()).max(0.0);
267 eps_inf * self.thickness * correction
268 }
269
270 pub fn fluid_pressure(&self, sigma: f64, t: f64) -> f64 {
272 let tau = self.creep_time_constant();
273 sigma * (-t / tau * PI * PI / 4.0).exp()
274 }
275
276 pub fn poisson_ratio(&self) -> f64 {
278 (self.ha - 2.0 * self.mu_s) / (2.0 * (self.ha - self.mu_s))
279 }
280
281 pub fn confined_compression_stiffness(&self, area: f64) -> f64 {
283 self.ha * area / self.thickness
284 }
285}
286
287#[derive(Clone, Debug)]
293pub struct TendonViscoelastic {
294 pub toe_strain: f64,
296 pub toe_modulus: f64,
298 pub linear_modulus: f64,
300 pub tau: f64,
302 pub relaxation_magnitude: f64,
304 pub area: f64,
306 pub resting_length: f64,
308}
309
310impl TendonViscoelastic {
311 pub fn achilles() -> Self {
313 Self {
314 toe_strain: 0.03,
315 toe_modulus: 50.0e6,
316 linear_modulus: 1.2e9,
317 tau: 20.0,
318 relaxation_magnitude: 0.15,
319 area: 80.0e-6,
320 resting_length: 0.25,
321 }
322 }
323
324 pub fn elastic_stress(&self, strain: f64) -> f64 {
326 if strain < 0.0 {
327 return 0.0;
328 }
329 if strain <= self.toe_strain {
330 let b = (self.linear_modulus / self.toe_modulus).ln() / self.toe_strain;
332 let a = self.toe_modulus / b;
333 a * ((b * strain).exp() - 1.0)
334 } else {
335 let toe_stress = self.elastic_stress(self.toe_strain);
336 toe_stress + self.linear_modulus * (strain - self.toe_strain)
337 }
338 }
339
340 pub fn reduced_relaxation(&self, t: f64) -> f64 {
342 1.0 - self.relaxation_magnitude * (1.0 - (-t / self.tau).exp())
343 }
344
345 pub fn stress_relaxation(&self, eps0: f64, t: f64) -> f64 {
347 self.elastic_stress(eps0) * self.reduced_relaxation(t)
348 }
349
350 pub fn force(&self, strain: f64) -> f64 {
352 self.elastic_stress(strain) * self.area
353 }
354
355 pub fn stiffness(&self, strain: f64) -> f64 {
357 let deps = 1e-6;
358 let df = self.force(strain + deps) - self.force(strain - deps);
359 df / (2.0 * deps * self.resting_length)
360 }
361
362 pub fn ultimate_force(&self) -> f64 {
364 self.elastic_stress(0.10) * self.area
366 }
367}
368
369#[derive(Clone, Debug)]
375pub struct HydrogelFloryRehner {
376 pub chi: f64,
378 pub nu_crosslink: f64,
380 pub phi_ref: f64,
382 pub temperature: f64,
384 pub v_solvent: f64,
386}
387
388const R_GAS: f64 = 8.314;
390
391impl HydrogelFloryRehner {
392 pub fn polyacrylamide() -> Self {
396 Self {
397 chi: 0.48,
398 nu_crosslink: 200.0,
399 phi_ref: 0.08,
400 temperature: 298.15,
401 v_solvent: 1.8e-5,
402 }
403 }
404
405 pub fn pegda() -> Self {
407 Self {
408 chi: 0.45,
409 nu_crosslink: 500.0,
410 phi_ref: 0.1,
411 temperature: 310.15,
412 v_solvent: 1.8e-5,
413 }
414 }
415
416 pub fn chemical_potential_mixing(&self, phi_p: f64) -> f64 {
418 let phi_s = 1.0 - phi_p;
419 R_GAS * self.temperature * ((phi_s).ln() + phi_p + self.chi * phi_p * phi_p)
420 }
421
422 pub fn chemical_potential_elastic(&self, phi_p: f64) -> f64 {
424 R_GAS
425 * self.temperature
426 * self.nu_crosslink
427 * self.v_solvent
428 * (phi_p / self.phi_ref).powf(1.0 / 3.0)
429 * (0.5 - phi_p / self.phi_ref)
430 }
431
432 pub fn total_chemical_potential(&self, phi_p: f64) -> f64 {
434 self.chemical_potential_mixing(phi_p) + self.chemical_potential_elastic(phi_p)
435 }
436
437 pub fn equilibrium_swelling_ratio(&self) -> f64 {
441 let mut lo = 0.001f64;
443 let mut hi = 0.999f64;
444 for _ in 0..100 {
445 let mid = (lo + hi) / 2.0;
446 let f_lo = self.total_chemical_potential(lo);
447 let f_mid = self.total_chemical_potential(mid);
448 if f_lo * f_mid < 0.0 {
449 hi = mid;
450 } else {
451 lo = mid;
452 }
453 }
454 let phi_eq = (lo + hi) / 2.0;
455 1.0 / phi_eq }
457
458 pub fn shear_modulus(&self, phi_p: f64) -> f64 {
460 R_GAS * self.temperature * self.nu_crosslink * phi_p.powf(1.0 / 3.0)
461 }
462
463 pub fn osmotic_pressure(&self, phi_p: f64) -> f64 {
465 -R_GAS * self.temperature / self.v_solvent
466 * (self.total_chemical_potential(phi_p) / (R_GAS * self.temperature))
467 }
468}
469
470#[derive(Clone, Debug)]
476pub struct CellMechanics {
477 pub substrate_modulus: f64,
479 pub cell_stiffness: f64,
481 pub adhesion_energy: f64,
483 pub cell_radius: f64,
485 pub cortical_tension: f64,
487}
488
489impl CellMechanics {
490 pub fn fibroblast_soft() -> Self {
492 Self {
493 substrate_modulus: 1.0e3,
494 cell_stiffness: 500.0,
495 adhesion_energy: 1e-5,
496 cell_radius: 10.0e-6,
497 cortical_tension: 4e-4,
498 }
499 }
500
501 pub fn jkr_contact_radius(&self, f: f64) -> f64 {
505 let r = self.cell_radius;
506 let e_star = self.effective_modulus();
507 let w = self.adhesion_energy;
508 let p0 = f + 3.0 * PI * w * r + (6.0 * PI * w * r * f + (3.0 * PI * w * r).powi(2)).sqrt();
509 (r * p0 / (4.0 * e_star / 3.0)).cbrt()
510 }
511
512 pub fn effective_modulus(&self) -> f64 {
514 let e_cell = self.cell_stiffness;
515 let e_sub = self.substrate_modulus;
516 let nu = 0.5; 1.0 / (2.0 * (1.0 - nu * nu) / e_cell + 2.0 * (1.0 - nu * nu) / e_sub)
518 }
519
520 pub fn hertz_force(&self, indent_depth: f64) -> f64 {
522 let e_star = self.effective_modulus();
523 let r = self.cell_radius;
524 4.0 / 3.0 * e_star * r.sqrt() * indent_depth.powf(1.5)
525 }
526
527 pub fn spreading_area(&self) -> f64 {
529 let a_max = PI * (50e-6f64).powi(2);
531 let a_min = PI * (5e-6f64).powi(2);
532 let half_max = 10.0e3; let e = self.substrate_modulus;
534 a_min + (a_max - a_min) * e / (e + half_max)
535 }
536
537 pub fn traction_force(&self, n_bonds: usize) -> f64 {
539 let k_bond = 1e-3; let d_slip = 5e-9; n_bonds as f64 * k_bond * d_slip
543 }
544}
545
546#[derive(Clone, Debug)]
552pub struct BiodegradationKinetics {
553 pub rate_constant: f64,
555 pub mw_initial: f64,
557 pub mw_critical: f64,
559 pub d_water: f64,
561 pub half_thickness: f64,
563}
564
565impl BiodegradationKinetics {
566 pub fn pla_scaffold() -> Self {
568 Self {
569 rate_constant: 1.0e-7,
570 mw_initial: 100_000.0,
571 mw_critical: 10_000.0,
572 d_water: 1.0e-14,
573 half_thickness: 1.0e-3,
574 }
575 }
576
577 pub fn molecular_weight(&self, t: f64) -> f64 {
579 self.mw_initial * (-self.rate_constant * t).exp()
581 }
582
583 pub fn time_to_failure(&self) -> f64 {
585 (self.mw_initial / self.mw_critical).ln() / self.rate_constant
586 }
587
588 pub fn mass_loss_fraction(&self, t: f64) -> f64 {
592 let t_half = 0.693 / self.rate_constant;
593 1.0 - (-0.693 * t / t_half).exp()
594 }
595
596 pub fn diffusion_time(&self) -> f64 {
598 self.half_thickness * self.half_thickness / self.d_water
599 }
600
601 pub fn is_bulk_degradation(&self) -> bool {
605 let t_reaction = 1.0 / self.rate_constant;
606 let t_diffusion = self.diffusion_time();
607 t_diffusion < t_reaction
608 }
609
610 pub fn plga_mass_loss(&self, t: f64, autocatalysis_factor: f64) -> f64 {
612 let k_eff = self.rate_constant * (1.0 + autocatalysis_factor * self.mass_loss_fraction(t));
613 1.0 - (-k_eff * t).exp()
614 }
615}
616
617#[derive(Clone, Debug)]
623pub struct ScaffoldPermeability {
624 pub porosity: f64,
626 pub specific_surface: f64,
628 pub kozeny_const: f64,
630 pub viscosity: f64,
632 pub thickness: f64,
634}
635
636impl ScaffoldPermeability {
637 pub fn ha_scaffold() -> Self {
639 Self {
640 porosity: 0.65,
641 specific_surface: 2.0e6,
642 kozeny_const: 5.0,
643 viscosity: 1e-3,
644 thickness: 5.0e-3,
645 }
646 }
647
648 pub fn permeability(&self) -> f64 {
652 let phi = self.porosity;
653 let sv = self.specific_surface;
654 phi.powi(3) / (self.kozeny_const * sv * sv * (1.0 - phi).powi(2))
655 }
656
657 pub fn darcy_velocity(&self, pressure_gradient: f64) -> f64 {
659 self.permeability() * pressure_gradient / self.viscosity
660 }
661
662 pub fn hydraulic_conductivity(&self) -> f64 {
664 self.permeability() / self.viscosity
665 }
666
667 pub fn tortuosity(&self) -> f64 {
669 self.porosity.powf(-0.5)
670 }
671
672 pub fn effective_diffusivity(&self, d_bulk: f64) -> f64 {
674 d_bulk * self.porosity / self.tortuosity()
675 }
676
677 pub fn mean_pore_diameter(&self) -> f64 {
679 4.0 * self.porosity / self.specific_surface
680 }
681}
682
683#[derive(Clone, Debug)]
689pub struct BiocompatibilityIndex {
690 pub cytotoxicity: f64,
692 pub inflammatory_response: f64,
694 pub haemocompatibility: f64,
696 pub genotoxicity: f64,
698 pub host_response: f64,
700}
701
702impl BiocompatibilityIndex {
703 pub fn composite_score(&self) -> f64 {
705 0.3 * self.cytotoxicity
706 + 0.25 * self.inflammatory_response
707 + 0.2 * self.haemocompatibility
708 + 0.15 * self.genotoxicity
709 + 0.1 * self.host_response
710 }
711
712 pub fn passes_iso_10993(&self) -> bool {
714 self.composite_score() < 0.3
715 }
716
717 pub fn ti6al4v() -> Self {
719 Self {
720 cytotoxicity: 0.05,
721 inflammatory_response: 0.1,
722 haemocompatibility: 0.08,
723 genotoxicity: 0.02,
724 host_response: 0.05,
725 }
726 }
727
728 pub fn uhmwpe() -> Self {
730 Self {
731 cytotoxicity: 0.04,
732 inflammatory_response: 0.08,
733 haemocompatibility: 0.05,
734 genotoxicity: 0.01,
735 host_response: 0.04,
736 }
737 }
738}
739
740#[derive(Clone, Debug)]
746pub struct SutureMaterial {
747 pub name: String,
749 pub modulus: f64,
751 pub uts: f64,
753 pub failure_strain: f64,
755 pub knot_efficiency: f64,
757 pub knot_breaking_force: f64,
759 pub diameter: f64,
761 pub biodegradable: bool,
763 pub half_life_days: f64,
765}
766
767impl SutureMaterial {
768 pub fn vicryl(diameter_mm: f64) -> Self {
770 let d = diameter_mm * 1e-3;
771 Self {
772 name: "Vicryl (PGA/PLA)".into(),
773 modulus: 8.0e9,
774 uts: 550.0e6,
775 failure_strain: 0.20,
776 knot_efficiency: 0.55,
777 knot_breaking_force: 40.0 * d / 0.3e-3,
778 diameter: d,
779 biodegradable: true,
780 half_life_days: 28.0,
781 }
782 }
783
784 pub fn prolene(diameter_mm: f64) -> Self {
786 let d = diameter_mm * 1e-3;
787 Self {
788 name: "Prolene (PP)".into(),
789 modulus: 3.5e9,
790 uts: 450.0e6,
791 failure_strain: 0.40,
792 knot_efficiency: 0.50,
793 knot_breaking_force: 35.0 * d / 0.3e-3,
794 diameter: d,
795 biodegradable: false,
796 half_life_days: f64::INFINITY,
797 }
798 }
799
800 pub fn area(&self) -> f64 {
802 PI * (self.diameter / 2.0).powi(2)
803 }
804
805 pub fn breaking_strength(&self) -> f64 {
807 self.uts * self.area()
808 }
809
810 pub fn knot_strength(&self) -> f64 {
812 self.breaking_strength() * self.knot_efficiency
813 }
814
815 pub fn elongation_at_break(&self, gauge_length: f64) -> f64 {
817 self.failure_strain * gauge_length
818 }
819
820 pub fn strength_retention(&self, t_days: f64) -> f64 {
822 if !self.biodegradable {
823 return 1.0;
824 }
825 (-0.693 * t_days / self.half_life_days).exp()
826 }
827}
828
829#[derive(Clone, Debug)]
831pub struct SurgicalMesh {
832 pub areal_density: f64,
834 pub pore_size: f64,
836 pub porosity: f64,
838 pub tensile_strength_per_width: f64,
840 pub stiffness: f64,
842 pub burst_strength: f64,
844}
845
846impl SurgicalMesh {
847 pub fn lightweight_pp() -> Self {
849 Self {
850 areal_density: 36.0,
851 pore_size: 2.0e-3,
852 porosity: 0.80,
853 tensile_strength_per_width: 60.0,
854 stiffness: 5.0,
855 burst_strength: 120.0,
856 }
857 }
858
859 pub fn safety_factor(&self, mesh_width: f64, max_pressure: f64) -> f64 {
863 let load = max_pressure * mesh_width;
864 self.tensile_strength_per_width / load
865 }
866}
867
868#[derive(Clone, Debug)]
874pub struct DentalEnamel {
875 pub modulus: f64,
877 pub hardness: f64,
879 pub k_ic: f64,
881 pub density: f64,
883 pub mineral_fraction: f64,
885}
886
887impl DentalEnamel {
888 pub fn human() -> Self {
890 Self {
891 modulus: 75.0e9,
892 hardness: 3.5e9,
893 k_ic: 0.7e6,
894 density: 2940.0,
895 mineral_fraction: 0.92,
896 }
897 }
898
899 pub fn critical_flaw_size(&self, tensile_strength: f64) -> f64 {
901 (self.k_ic / (tensile_strength * PI.sqrt())).powi(2) / PI
902 }
903
904 pub fn vickers_hardness(&self) -> f64 {
906 self.hardness / (9.81 * 1e6 / 1e4) }
908}
909
910#[derive(Clone, Debug)]
912pub struct Dentin {
913 pub modulus: f64,
915 pub hardness: f64,
917 pub k_ic: f64,
919 pub density: f64,
921 pub mineral_fraction: f64,
923 pub collagen_fraction: f64,
925}
926
927impl Dentin {
928 pub fn human() -> Self {
930 Self {
931 modulus: 20.0e9,
932 hardness: 0.5e9,
933 k_ic: 3.1e6,
934 density: 2100.0,
935 mineral_fraction: 0.50,
936 collagen_fraction: 0.30,
937 }
938 }
939
940 pub fn tubule_toughening_factor(&self, tubule_density: f64) -> f64 {
942 1.0 / (1.0 + 0.1 * tubule_density * 1e-9)
944 }
945}
946
947#[derive(Clone, Debug)]
949pub struct DentalComposite {
950 pub filler_fraction: f64,
952 pub filler_modulus: f64,
954 pub matrix_modulus: f64,
956 pub particle_size: f64,
958 pub flexural_strength: f64,
960 pub poly_shrinkage: f64,
962 pub water_absorption: f64,
964}
965
966impl DentalComposite {
967 pub fn nano_hybrid() -> Self {
969 Self {
970 filler_fraction: 0.70,
971 filler_modulus: 70.0e9,
972 matrix_modulus: 3.5e9,
973 particle_size: 0.5e-6,
974 flexural_strength: 120.0e6,
975 poly_shrinkage: 0.02,
976 water_absorption: 1.5,
977 }
978 }
979
980 pub fn nano_hybrid_v2() -> Self {
982 Self::nano_hybrid()
983 }
984
985 pub fn reuss_modulus(&self) -> f64 {
987 let vf = self.filler_fraction;
988 1.0 / (vf / self.filler_modulus + (1.0 - vf) / self.matrix_modulus)
989 }
990
991 pub fn voigt_modulus(&self) -> f64 {
993 let vf = self.filler_fraction;
994 vf * self.filler_modulus + (1.0 - vf) * self.matrix_modulus
995 }
996
997 pub fn halpin_tsai_modulus(&self) -> f64 {
999 let eta = (self.filler_modulus / self.matrix_modulus - 1.0)
1000 / (self.filler_modulus / self.matrix_modulus + 2.0);
1001 self.matrix_modulus * (1.0 + 2.0 * eta * self.filler_fraction)
1002 / (1.0 - eta * self.filler_fraction)
1003 }
1004
1005 pub fn shrinkage_stress(&self, compliance: f64) -> f64 {
1007 self.poly_shrinkage / compliance
1008 }
1009}
1010
1011#[derive(Clone, Debug, PartialEq)]
1017pub enum TissueType {
1018 Bone,
1020 Cartilage,
1022 Tendon,
1024 Skin,
1026 Muscle,
1028 Artery,
1030 Custom(String),
1032}
1033
1034#[derive(Clone, Debug)]
1040pub struct BoneMaterial {
1041 pub is_cortical: bool,
1043 pub density_g_cm3: f64,
1045 pub porosity: f64,
1047 pub bvf: f64,
1049 pub power_law_a: f64,
1051 pub power_law_b: f64,
1053}
1054
1055impl BoneMaterial {
1056 pub fn cortical() -> Self {
1058 Self {
1059 is_cortical: true,
1060 density_g_cm3: 1.85,
1061 porosity: 0.05,
1062 bvf: 0.95,
1063 power_law_a: 2.065e10,
1064 power_law_b: 3.09,
1065 }
1066 }
1067
1068 pub fn trabecular() -> Self {
1070 Self {
1071 is_cortical: false,
1072 density_g_cm3: 0.30,
1073 porosity: 0.75,
1074 bvf: 0.25,
1075 power_law_a: 1.310e10,
1076 power_law_b: 2.74,
1077 }
1078 }
1079
1080 pub fn young_modulus_from_density(&self) -> f64 {
1084 self.power_law_a * self.density_g_cm3.powf(self.power_law_b)
1085 }
1086
1087 pub fn modulus_at_density(&self, rho_g_cm3: f64) -> f64 {
1089 self.power_law_a * rho_g_cm3.powf(self.power_law_b)
1090 }
1091
1092 pub fn tissue_type(&self) -> TissueType {
1094 TissueType::Bone
1095 }
1096}
1097
1098#[derive(Clone, Debug)]
1104pub struct CartilageMaterial {
1105 pub aggregate_modulus: f64,
1107 pub shear_modulus: f64,
1109 pub permeability: f64,
1111 pub fluid_fraction: f64,
1113 pub thickness: f64,
1115}
1116
1117impl CartilageMaterial {
1118 pub fn knee() -> Self {
1120 Self {
1121 aggregate_modulus: 0.7e6,
1122 shear_modulus: 0.15e6,
1123 permeability: 2.17e-15,
1124 fluid_fraction: 0.75,
1125 thickness: 2.5e-3,
1126 }
1127 }
1128
1129 pub fn creep_time_constant(&self) -> f64 {
1131 self.thickness * self.thickness / (self.aggregate_modulus * self.permeability)
1132 }
1133
1134 pub fn steady_state_strain(&self, sigma: f64) -> f64 {
1136 sigma / self.aggregate_modulus
1137 }
1138
1139 pub fn fluid_pressure(&self, sigma: f64, t: f64) -> f64 {
1141 let tau = self.creep_time_constant();
1142 sigma * (-t / tau * PI * PI / 4.0).exp()
1143 }
1144
1145 pub fn tissue_type(&self) -> TissueType {
1147 TissueType::Cartilage
1148 }
1149}
1150
1151#[derive(Clone, Debug)]
1157pub struct TendonLigament {
1158 pub fiber_angle_rad: f64,
1160 pub toe_stiffness: f64,
1162 pub linear_stiffness: f64,
1164 pub toe_strain: f64,
1166 pub failure_strain: f64,
1168}
1169
1170impl TendonLigament {
1171 pub fn achilles() -> Self {
1173 Self {
1174 fiber_angle_rad: 0.05,
1175 toe_stiffness: 50.0e6,
1176 linear_stiffness: 1.2e9,
1177 toe_strain: 0.03,
1178 failure_strain: 0.10,
1179 }
1180 }
1181
1182 pub fn stress(&self, eps: f64) -> f64 {
1184 if eps <= 0.0 {
1185 return 0.0;
1186 }
1187 if eps <= self.toe_strain {
1188 let b = (self.linear_stiffness / self.toe_stiffness).ln() / self.toe_strain;
1190 let a = self.toe_stiffness / b;
1191 a * ((b * eps).exp() - 1.0)
1192 } else {
1193 let sigma_toe = self.stress(self.toe_strain);
1194 sigma_toe + self.linear_stiffness * (eps - self.toe_strain)
1195 }
1196 }
1197
1198 pub fn is_failed(&self, eps: f64) -> bool {
1200 eps >= self.failure_strain
1201 }
1202
1203 pub fn tissue_type(&self) -> TissueType {
1205 TissueType::Tendon
1206 }
1207}
1208
1209#[derive(Clone, Debug)]
1217pub struct ArterialWall {
1218 pub c10: f64,
1220 pub k1: f64,
1222 pub k2: f64,
1224 pub fiber_angle_rad: f64,
1226}
1227
1228impl ArterialWall {
1229 pub fn aorta_media() -> Self {
1231 Self {
1232 c10: 26.95e3,
1233 k1: 15.56e3,
1234 k2: 11.65,
1235 fiber_angle_rad: 0.4869, }
1237 }
1238
1239 pub fn neo_hookean_energy(&self, i1: f64) -> f64 {
1241 self.c10 * (i1 - 3.0)
1242 }
1243
1244 pub fn fiber_energy(&self, i4: f64) -> f64 {
1246 if i4 <= 1.0 {
1247 return 0.0;
1249 }
1250 self.k1 / (2.0 * self.k2) * ((self.k2 * (i4 - 1.0).powi(2)).exp() - 1.0)
1251 }
1252
1253 pub fn strain_energy(&self, i1: f64, i4: f64) -> f64 {
1255 self.neo_hookean_energy(i1) + self.fiber_energy(i4)
1256 }
1257
1258 pub fn fiber_stress(&self, i4: f64) -> f64 {
1260 if i4 <= 1.0 {
1261 return 0.0;
1262 }
1263 2.0 * self.k1 * (i4 - 1.0) * (self.k2 * (i4 - 1.0).powi(2)).exp()
1264 }
1265
1266 pub fn tissue_type(&self) -> TissueType {
1268 TissueType::Artery
1269 }
1270}
1271
1272#[derive(Clone, Debug)]
1281pub struct MuscleMaterial {
1282 pub max_isometric_force: f64,
1284 pub optimal_fiber_length: f64,
1286 pub pennation_angle: f64,
1288 pub vmax_factor: f64,
1290 pub passive_stiffness: f64,
1292}
1293
1294impl MuscleMaterial {
1295 pub fn biceps() -> Self {
1297 Self {
1298 max_isometric_force: 1200.0,
1299 optimal_fiber_length: 0.116,
1300 pennation_angle: 0.0,
1301 vmax_factor: 10.0,
1302 passive_stiffness: 1.0e4,
1303 }
1304 }
1305
1306 pub fn force_length_factor(&self, length: f64) -> f64 {
1310 let ratio = length / self.optimal_fiber_length;
1311 (-4.0 * (ratio - 1.0).powi(2)).exp()
1313 }
1314
1315 pub fn active_force(&self, activation: f64, length: f64) -> f64 {
1320 let a = activation.clamp(0.0, 1.0);
1321 let pennation_cos = self.pennation_angle.cos();
1322 a * self.max_isometric_force * self.force_length_factor(length) * pennation_cos
1323 }
1324
1325 pub fn passive_force(&self, length: f64) -> f64 {
1327 let stretch = (length - self.optimal_fiber_length).max(0.0);
1328 self.passive_stiffness * stretch
1329 }
1330
1331 pub fn total_force(&self, activation: f64, length: f64) -> f64 {
1333 self.active_force(activation, length) + self.passive_force(length)
1334 }
1335
1336 pub fn tissue_type(&self) -> TissueType {
1338 TissueType::Muscle
1339 }
1340}
1341
1342pub fn reuss_modulus(e1: f64, e2: f64, vf1: f64) -> f64 {
1352 let vf2 = 1.0 - vf1;
1353 1.0 / (vf1 / e1 + vf2 / e2)
1354}
1355
1356pub fn voigt_modulus(e1: f64, e2: f64, vf1: f64) -> f64 {
1362 vf1 * e1 + (1.0 - vf1) * e2
1363}
1364
1365pub fn strain_energy_density_neo_hookean(c1: f64, i1: f64) -> f64 {
1370 c1 * (i1 - 3.0)
1371}
1372
1373#[cfg(test)]
1378mod tests {
1379 use super::*;
1380
1381 const EPS: f64 = 1e-9;
1382
1383 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1384 (a - b).abs() < tol
1385 }
1386
1387 #[test]
1390 fn test_cortical_bone_wave_speed() {
1391 let bone = CorticalBone::human_femur();
1392 let v = bone.longitudinal_wave_speed();
1393 assert!(v > 2000.0 && v < 5000.0, "Wave speed should be ~3200 m/s");
1394 }
1395
1396 #[test]
1397 fn test_cortical_bone_bending_stiffness() {
1398 let bone = CorticalBone::human_femur();
1399 let k = bone.bending_stiffness(0.015, 0.010, 0.3);
1400 assert!(k > 0.0);
1401 }
1402
1403 #[test]
1404 fn test_cortical_bone_three_point_stress() {
1405 let bone = CorticalBone::human_femur();
1406 let sigma = bone.three_point_bending_stress(1000.0, 0.3, 0.015, 0.010);
1407 assert!(sigma > 0.0);
1408 }
1409
1410 #[test]
1411 fn test_cortical_stiffness_tensor_symmetry() {
1412 let bone = CorticalBone::human_femur();
1413 let c = cortical_bone_stiffness_tensor(
1414 bone.e_long,
1415 bone.e_trans,
1416 bone.g_lt,
1417 bone.nu_lt,
1418 bone.nu_tt,
1419 );
1420 assert!(approx_eq(c[0][1], c[1][0], 1e3));
1421 assert!(approx_eq(c[0][2], c[2][0], 1e3));
1422 }
1423
1424 #[test]
1427 fn test_cancellous_bone_modulus() {
1428 let bone = CancellousBone::from_relative_density(0.2);
1429 assert!(bone.apparent_modulus > 0.0);
1430 let expected = 1.0 * 20.0e9 * 0.04; assert!(approx_eq(bone.apparent_modulus, expected, 1.0));
1433 }
1434
1435 #[test]
1436 fn test_cancellous_energy_absorption() {
1437 let bone = CancellousBone::from_relative_density(0.2);
1438 let e = bone.energy_absorption();
1439 assert!(e > 0.0);
1440 }
1441
1442 #[test]
1445 fn test_cartilage_creep_time_constant() {
1446 let cart = CartilageBiphasic::knee();
1447 let tau = cart.creep_time_constant();
1448 assert!(tau > 0.0);
1449 }
1450
1451 #[test]
1452 fn test_cartilage_steady_state_strain() {
1453 let cart = CartilageBiphasic::knee();
1454 let eps = cart.steady_state_strain(1.0e4);
1455 assert!(approx_eq(eps, 1.0e4 / 0.7e6, 1e-6));
1456 }
1457
1458 #[test]
1459 fn test_cartilage_creep_displacement_increases_with_time() {
1460 let cart = CartilageBiphasic::knee();
1461 let d1 = cart.creep_displacement(1.0e4, 10.0);
1462 let d2 = cart.creep_displacement(1.0e4, 100.0);
1463 assert!(d2 >= d1);
1464 }
1465
1466 #[test]
1467 fn test_cartilage_fluid_pressure_decays() {
1468 let cart = CartilageBiphasic::knee();
1469 let p0 = cart.fluid_pressure(1.0e4, 0.0);
1470 let p1 = cart.fluid_pressure(1.0e4, 100.0);
1471 assert!(p0 >= p1);
1472 }
1473
1474 #[test]
1477 fn test_tendon_elastic_stress_toe_region() {
1478 let tendon = TendonViscoelastic::achilles();
1479 let s = tendon.elastic_stress(0.01);
1480 assert!(s > 0.0);
1481 let s2 = tendon.elastic_stress(0.02);
1482 assert!(s2 > s);
1483 }
1484
1485 #[test]
1486 fn test_tendon_elastic_stress_linear_region() {
1487 let tendon = TendonViscoelastic::achilles();
1488 let s1 = tendon.elastic_stress(0.05);
1489 let s2 = tendon.elastic_stress(0.06);
1490 let d = s2 - s1;
1491 assert!(approx_eq(d, 1.2e7, 1.0e6));
1493 }
1494
1495 #[test]
1496 fn test_tendon_stress_relaxation_decreases() {
1497 let tendon = TendonViscoelastic::achilles();
1498 let s1 = tendon.stress_relaxation(0.05, 1.0);
1499 let s2 = tendon.stress_relaxation(0.05, 100.0);
1500 assert!(s1 > s2);
1501 }
1502
1503 #[test]
1504 fn test_tendon_reduced_relaxation_limits() {
1505 let tendon = TendonViscoelastic::achilles();
1506 let g0 = tendon.reduced_relaxation(0.0);
1507 let g_inf = tendon.reduced_relaxation(1e9);
1508 assert!(approx_eq(g0, 1.0, 1e-6));
1509 assert!(approx_eq(g_inf, 1.0 - tendon.relaxation_magnitude, 1e-4));
1510 }
1511
1512 #[test]
1515 fn test_hydrogel_swelling_ratio_positive() {
1516 let gel = HydrogelFloryRehner::pegda();
1517 let q = gel.equilibrium_swelling_ratio();
1518 assert!(q > 1.0, "Swelling ratio must exceed 1 for a swelling gel");
1519 }
1520
1521 #[test]
1522 fn test_hydrogel_shear_modulus_positive() {
1523 let gel = HydrogelFloryRehner::pegda();
1524 let g = gel.shear_modulus(0.1);
1525 assert!(g > 0.0);
1526 }
1527
1528 #[test]
1531 fn test_scaffold_permeability_kozeny() {
1532 let scaffold = ScaffoldPermeability::ha_scaffold();
1533 let k = scaffold.permeability();
1534 assert!(k > 0.0 && k < 1e-10);
1535 }
1536
1537 #[test]
1538 fn test_scaffold_darcy_velocity() {
1539 let scaffold = ScaffoldPermeability::ha_scaffold();
1540 let v = scaffold.darcy_velocity(1.0e3);
1541 assert!(v > 0.0);
1542 }
1543
1544 #[test]
1545 fn test_scaffold_pore_diameter() {
1546 let scaffold = ScaffoldPermeability::ha_scaffold();
1547 let d = scaffold.mean_pore_diameter();
1548 assert!(approx_eq(d, 4.0 * 0.65 / 2.0e6, 1e-8));
1550 }
1551
1552 #[test]
1555 fn test_biocompatibility_ti6al4v_passes() {
1556 let mat = BiocompatibilityIndex::ti6al4v();
1557 assert!(mat.passes_iso_10993());
1558 }
1559
1560 #[test]
1561 fn test_biocompatibility_score_range() {
1562 let mat = BiocompatibilityIndex {
1563 cytotoxicity: 0.5,
1564 inflammatory_response: 0.5,
1565 haemocompatibility: 0.5,
1566 genotoxicity: 0.5,
1567 host_response: 0.5,
1568 };
1569 let score = mat.composite_score();
1570 assert!(approx_eq(score, 0.5, EPS));
1571 }
1572
1573 #[test]
1576 fn test_suture_breaking_strength() {
1577 let suture = SutureMaterial::vicryl(0.3);
1578 let f = suture.breaking_strength();
1579 assert!(f > 0.0);
1580 }
1581
1582 #[test]
1583 fn test_suture_strength_retention_biodegradable() {
1584 let suture = SutureMaterial::vicryl(0.3);
1585 let r0 = suture.strength_retention(0.0);
1586 let r28 = suture.strength_retention(28.0);
1587 assert!(approx_eq(r0, 1.0, 1e-10));
1588 assert!(approx_eq(r28, 0.5, 0.01));
1589 }
1590
1591 #[test]
1592 fn test_suture_prolene_non_degradable() {
1593 let suture = SutureMaterial::prolene(0.3);
1594 assert!(!suture.biodegradable);
1595 assert!(approx_eq(suture.strength_retention(3650.0), 1.0, 1e-10));
1596 }
1597
1598 #[test]
1601 fn test_enamel_critical_flaw() {
1602 let enamel = DentalEnamel::human();
1603 let flaw = enamel.critical_flaw_size(50.0e6);
1604 assert!(flaw > 0.0 && flaw < 1e-3);
1605 }
1606
1607 #[test]
1610 fn test_composite_voigt_reuss_bounds() {
1611 let comp = DentalComposite::nano_hybrid_v2();
1612 let voigt = comp.voigt_modulus();
1613 let reuss = comp.reuss_modulus();
1614 assert!(voigt >= reuss, "Voigt bound must be >= Reuss bound");
1615 }
1616
1617 #[test]
1618 fn test_composite_halpin_tsai_in_bounds() {
1619 let comp = DentalComposite::nano_hybrid_v2();
1620 let ht = comp.halpin_tsai_modulus();
1621 let reuss = comp.reuss_modulus();
1622 let voigt = comp.voigt_modulus();
1623 assert!(ht >= reuss && ht <= voigt);
1624 }
1625
1626 #[test]
1629 fn test_cell_hertz_force_positive() {
1630 let cell = CellMechanics::fibroblast_soft();
1631 let f = cell.hertz_force(100e-9);
1632 assert!(f > 0.0);
1633 }
1634
1635 #[test]
1636 fn test_cell_spreading_area_increases_with_stiffness() {
1637 let soft = CellMechanics::fibroblast_soft();
1638 let hard = CellMechanics {
1639 substrate_modulus: 100e3,
1640 ..CellMechanics::fibroblast_soft()
1641 };
1642 assert!(hard.spreading_area() > soft.spreading_area());
1643 }
1644
1645 #[test]
1648 fn test_degradation_mw_decreases() {
1649 let pla = BiodegradationKinetics::pla_scaffold();
1650 let mw0 = pla.molecular_weight(0.0);
1651 let mw1 = pla.molecular_weight(1e8);
1652 assert!(approx_eq(mw0, pla.mw_initial, 1e-6));
1653 assert!(mw1 < mw0);
1654 }
1655
1656 #[test]
1657 fn test_degradation_time_to_failure() {
1658 let pla = BiodegradationKinetics::pla_scaffold();
1659 let t = pla.time_to_failure();
1660 assert!(approx_eq(
1661 pla.molecular_weight(t),
1662 pla.mw_critical,
1663 pla.mw_critical * 0.01
1664 ));
1665 }
1666
1667 #[test]
1668 fn test_degradation_mass_loss_fraction() {
1669 let pla = BiodegradationKinetics::pla_scaffold();
1670 let ml = pla.mass_loss_fraction(0.0);
1671 assert!(approx_eq(ml, 0.0, 1e-10));
1672 let ml_inf = pla.mass_loss_fraction(1e12);
1673 assert!((ml_inf - 1.0).abs() < 0.01);
1674 }
1675
1676 #[test]
1679 fn test_dentin_modulus_range() {
1680 let d = Dentin::human();
1681 assert!(d.modulus >= 15.0e9 && d.modulus <= 25.0e9);
1682 }
1683
1684 #[test]
1692 fn test_tissue_type_equality() {
1693 assert_eq!(TissueType::Bone, TissueType::Bone);
1694 assert_ne!(TissueType::Bone, TissueType::Cartilage);
1695 }
1696
1697 #[test]
1698 fn test_tissue_type_custom() {
1699 let t = TissueType::Custom("Intervertebral Disc".into());
1700 assert_ne!(t, TissueType::Bone);
1701 }
1702
1703 #[test]
1706 fn test_bone_cortical_modulus_range() {
1707 let b = BoneMaterial::cortical();
1708 let e = b.young_modulus_from_density();
1709 assert!(
1712 e > 1.0e9 && e < 500.0e9,
1713 "Cortical E = {e:.3e} Pa out of range"
1714 );
1715 }
1716
1717 #[test]
1718 fn test_bone_trabecular_modulus_range() {
1719 let b = BoneMaterial::trabecular();
1720 let e = b.young_modulus_from_density();
1721 assert!(
1723 e > 1.0e8 && e < 5.0e9,
1724 "Trabecular E = {e:.3e} Pa out of range"
1725 );
1726 }
1727
1728 #[test]
1729 fn test_bone_modulus_increases_with_density() {
1730 let b = BoneMaterial::cortical();
1731 let e_low = b.modulus_at_density(0.5);
1732 let e_high = b.modulus_at_density(2.0);
1733 assert!(e_high > e_low, "Higher density should give higher modulus");
1734 }
1735
1736 #[test]
1737 fn test_bone_power_law_exponent() {
1738 let b = BoneMaterial::cortical();
1739 let ratio = b.modulus_at_density(2.0) / b.modulus_at_density(1.0);
1741 let expected = (2.0_f64).powf(b.power_law_b);
1742 assert!(
1743 approx_eq(ratio, expected, 1.0),
1744 "Power-law scaling incorrect"
1745 );
1746 }
1747
1748 #[test]
1749 fn test_bone_tissue_type() {
1750 let b = BoneMaterial::cortical();
1751 assert_eq!(b.tissue_type(), TissueType::Bone);
1752 }
1753
1754 #[test]
1755 fn test_bone_bvf_range() {
1756 let c = BoneMaterial::cortical();
1757 let t = BoneMaterial::trabecular();
1758 assert!(c.bvf > 0.9 && c.bvf <= 1.0);
1759 assert!(t.bvf < 0.5 && t.bvf > 0.0);
1760 }
1761
1762 #[test]
1765 fn test_cartilage_creep_constant_positive() {
1766 let c = CartilageMaterial::knee();
1767 let tau = c.creep_time_constant();
1768 assert!(tau > 0.0, "Creep time constant must be positive");
1769 }
1770
1771 #[test]
1772 fn test_cartilage_material_steady_state_strain() {
1773 let c = CartilageMaterial::knee();
1774 let sigma = 1.0e4;
1775 let eps = c.steady_state_strain(sigma);
1776 let expected = sigma / c.aggregate_modulus;
1777 assert!(approx_eq(eps, expected, 1e-12));
1778 }
1779
1780 #[test]
1781 fn test_cartilage_material_fluid_pressure_decays() {
1782 let c = CartilageMaterial::knee();
1783 let p0 = c.fluid_pressure(1.0e4, 0.0);
1784 let p1 = c.fluid_pressure(1.0e4, 100.0);
1785 assert!(p0 > p1, "Fluid pressure should decay over time");
1786 }
1787
1788 #[test]
1789 fn test_cartilage_fluid_fraction_range() {
1790 let c = CartilageMaterial::knee();
1791 assert!(c.fluid_fraction > 0.5 && c.fluid_fraction < 1.0);
1792 }
1793
1794 #[test]
1795 fn test_cartilage_tissue_type() {
1796 assert_eq!(
1797 CartilageMaterial::knee().tissue_type(),
1798 TissueType::Cartilage
1799 );
1800 }
1801
1802 #[test]
1805 fn test_tendon_zero_stress_at_zero_strain() {
1806 let t = TendonLigament::achilles();
1807 assert!(approx_eq(t.stress(0.0), 0.0, EPS));
1808 }
1809
1810 #[test]
1811 fn test_tendon_stress_increases_with_strain() {
1812 let t = TendonLigament::achilles();
1813 let s1 = t.stress(0.01);
1814 let s2 = t.stress(0.02);
1815 let s3 = t.stress(0.05);
1816 assert!(
1817 s1 < s2 && s2 < s3,
1818 "Stress must be monotonically increasing"
1819 );
1820 }
1821
1822 #[test]
1823 fn test_tendon_linear_region() {
1824 let t = TendonLigament::achilles();
1825 let ds = t.stress(0.06) - t.stress(0.05);
1827 let de = 0.01;
1828 let tangent = ds / de;
1829 let tol = t.linear_stiffness * 0.20;
1831 assert!(
1832 (tangent - t.linear_stiffness).abs() < tol,
1833 "Linear tangent {tangent:.3e} should be near {:.3e}",
1834 t.linear_stiffness
1835 );
1836 }
1837
1838 #[test]
1839 fn test_tendon_failure_strain() {
1840 let t = TendonLigament::achilles();
1841 assert!(!t.is_failed(0.05));
1842 assert!(t.is_failed(0.10));
1843 }
1844
1845 #[test]
1846 fn test_tendon_tissue_type() {
1847 assert_eq!(TendonLigament::achilles().tissue_type(), TissueType::Tendon);
1848 }
1849
1850 #[test]
1853 fn test_artery_neo_hookean_energy_zero_at_rest() {
1854 let a = ArterialWall::aorta_media();
1855 assert!(approx_eq(a.neo_hookean_energy(3.0), 0.0, EPS));
1857 }
1858
1859 #[test]
1860 fn test_artery_fiber_energy_zero_in_compression() {
1861 let a = ArterialWall::aorta_media();
1862 assert!(approx_eq(a.fiber_energy(0.8), 0.0, EPS));
1864 assert!(approx_eq(a.fiber_energy(1.0), 0.0, EPS));
1865 }
1866
1867 #[test]
1868 fn test_artery_fiber_energy_positive_in_tension() {
1869 let a = ArterialWall::aorta_media();
1870 assert!(a.fiber_energy(1.2) > 0.0);
1871 }
1872
1873 #[test]
1874 fn test_artery_total_strain_energy() {
1875 let a = ArterialWall::aorta_media();
1876 let w = a.strain_energy(3.5, 1.1);
1877 assert!(w > 0.0);
1878 }
1879
1880 #[test]
1881 fn test_artery_tissue_type() {
1882 assert_eq!(
1883 ArterialWall::aorta_media().tissue_type(),
1884 TissueType::Artery
1885 );
1886 }
1887
1888 #[test]
1891 fn test_muscle_max_force_at_optimal_length() {
1892 let m = MuscleMaterial::biceps();
1893 let f = m.active_force(1.0, m.optimal_fiber_length);
1894 assert!(
1896 approx_eq(f, m.max_isometric_force, 1.0),
1897 "Active force at optimal length should equal F0"
1898 );
1899 }
1900
1901 #[test]
1902 fn test_muscle_zero_force_at_zero_activation() {
1903 let m = MuscleMaterial::biceps();
1904 let f = m.active_force(0.0, m.optimal_fiber_length);
1905 assert!(approx_eq(f, 0.0, EPS));
1906 }
1907
1908 #[test]
1909 fn test_muscle_force_length_bell() {
1910 let m = MuscleMaterial::biceps();
1911 let fl_opt = m.force_length_factor(m.optimal_fiber_length);
1912 let fl_far = m.force_length_factor(m.optimal_fiber_length * 2.0);
1913 assert!(approx_eq(fl_opt, 1.0, 1e-10));
1914 assert!(fl_far < fl_opt);
1915 }
1916
1917 #[test]
1918 fn test_muscle_passive_force_zero_at_optimal() {
1919 let m = MuscleMaterial::biceps();
1920 assert!(approx_eq(m.passive_force(m.optimal_fiber_length), 0.0, EPS));
1921 }
1922
1923 #[test]
1924 fn test_muscle_passive_force_increases_with_stretch() {
1925 let m = MuscleMaterial::biceps();
1926 let f1 = m.passive_force(m.optimal_fiber_length + 0.01);
1927 let f2 = m.passive_force(m.optimal_fiber_length + 0.02);
1928 assert!(f2 > f1);
1929 }
1930
1931 #[test]
1932 fn test_muscle_total_force_sum() {
1933 let m = MuscleMaterial::biceps();
1934 let a = 0.7;
1935 let l = m.optimal_fiber_length;
1936 let total = m.total_force(a, l);
1937 let expected = m.active_force(a, l) + m.passive_force(l);
1938 assert!(approx_eq(total, expected, EPS));
1939 }
1940
1941 #[test]
1942 fn test_muscle_tissue_type() {
1943 assert_eq!(MuscleMaterial::biceps().tissue_type(), TissueType::Muscle);
1944 }
1945
1946 #[test]
1949 fn test_reuss_modulus_two_phase() {
1950 let e1 = 200.0e9; let e2 = 70.0e9; let vf1 = 0.5;
1953 let er = reuss_modulus(e1, e2, vf1);
1954 let ev = voigt_modulus(e1, e2, vf1);
1956 assert!(er <= ev, "Reuss must be ≤ Voigt");
1957 }
1958
1959 #[test]
1960 fn test_voigt_modulus_endpoints() {
1961 let e1 = 100.0e9;
1963 let e2 = 50.0e9;
1964 assert!(approx_eq(voigt_modulus(e1, e2, 0.0), e2, 1.0));
1965 assert!(approx_eq(voigt_modulus(e1, e2, 1.0), e1, 1.0));
1966 }
1967
1968 #[test]
1969 fn test_reuss_modulus_equal_phases() {
1970 let e = 100.0e9;
1971 let er = reuss_modulus(e, e, 0.5);
1972 assert!(approx_eq(er, e, 1.0));
1973 }
1974
1975 #[test]
1976 fn test_strain_energy_neo_hookean_zero_at_rest() {
1977 assert!(approx_eq(
1979 strain_energy_density_neo_hookean(1.0e4, 3.0),
1980 0.0,
1981 EPS
1982 ));
1983 }
1984
1985 #[test]
1986 fn test_strain_energy_neo_hookean_positive() {
1987 let w = strain_energy_density_neo_hookean(1.0e4, 4.0);
1988 assert!(w > 0.0);
1989 }
1990
1991 #[test]
1992 fn test_strain_energy_scales_linearly_with_c1() {
1993 let i1 = 4.0;
1994 let w1 = strain_energy_density_neo_hookean(1.0e4, i1);
1995 let w2 = strain_energy_density_neo_hookean(2.0e4, i1);
1996 assert!(approx_eq(w2, 2.0 * w1, EPS));
1997 }
1998}