Skip to main content

oxiphysics_materials/biomechanics/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use std::f64::consts::PI;
8
9/// Transversely isotropic meniscus model (fibrocartilage).
10#[derive(Debug, Clone)]
11pub struct MeniscusModel {
12    /// Circumferential (hoop) modulus \[Pa\].
13    pub e_circumferential: f64,
14    /// Radial modulus \[Pa\].
15    pub e_radial: f64,
16    /// Shear modulus \[Pa\].
17    pub shear_modulus: f64,
18    /// Permeability \[m^4/Ns\].
19    pub permeability: f64,
20    /// Fluid fraction.
21    pub fluid_fraction: f64,
22}
23impl MeniscusModel {
24    /// Create a new meniscus model.
25    pub fn new(
26        e_circumferential: f64,
27        e_radial: f64,
28        shear_modulus: f64,
29        permeability: f64,
30        fluid_fraction: f64,
31    ) -> Self {
32        Self {
33            e_circumferential,
34            e_radial,
35            shear_modulus,
36            permeability,
37            fluid_fraction,
38        }
39    }
40    /// Typical medial meniscus (knee).
41    pub fn medial_meniscus() -> Self {
42        Self::new(150e6, 1.4e6, 0.12e6, 1.2e-15, 0.72)
43    }
44    /// Hoop stress from joint reaction force F \[N\] acting radially.
45    pub fn hoop_stress(&self, joint_force: f64, area: f64) -> f64 {
46        joint_force / area
47    }
48}
49/// Cortical bone model using Carter-Hayes density-elasticity relationships.
50#[derive(Debug, Clone)]
51pub struct CorticalBone {
52    /// Apparent density ρ_app \[g/cm^3\].
53    pub density: f64,
54    /// Ash fraction (mineral content) \[dimensionless\].
55    pub ash_fraction: f64,
56}
57impl CorticalBone {
58    /// Create a new cortical bone model.
59    pub fn new(density: f64, ash_fraction: f64) -> Self {
60        Self {
61            density,
62            ash_fraction,
63        }
64    }
65    /// Typical human femur cortical bone.
66    pub fn femur_cortical() -> Self {
67        Self::new(1.85, 0.62)
68    }
69    /// Elastic modulus E \[MPa\] via Carter-Hayes relationship.
70    ///
71    /// E = 2017 * ρ_app^2.5 \[MPa\] (Carter & Hayes 1977, density in g/cm^3)
72    pub fn elastic_modulus(&self) -> f64 {
73        2017.0 * self.density.powf(2.5)
74    }
75    /// Compressive strength \[MPa\] via density power law.
76    ///
77    /// σ_c = 68 * ρ_app^1.6 \[MPa\]
78    pub fn strength_compressive(&self) -> f64 {
79        68.0 * self.density.powf(1.6)
80    }
81    /// Tensile strength \[MPa\] via density power law.
82    ///
83    /// σ_t = 131 * ρ_app \[MPa\] (approximate linear relationship)
84    pub fn strength_tensile(&self) -> f64 {
85        131.0 * self.density
86    }
87    /// Mineral content contribution to stiffness (Katz & Ukraincik).
88    pub fn mineral_stiffness_contribution(&self) -> f64 {
89        self.elastic_modulus() * (0.6 + 0.4 * self.ash_fraction)
90    }
91}
92/// Muscle model using Hill's three-element (active + passive + series) model.
93#[derive(Debug, Clone)]
94pub struct MuscleModel {
95    /// Maximum isometric force \[N\].
96    pub f_max: f64,
97    /// Optimal fiber length \[m\].
98    pub l_opt: f64,
99    /// Maximum shortening velocity \[m/s\].
100    pub v_max: f64,
101    /// Pennation angle at optimal length \[radians\].
102    pub pennation_angle: f64,
103}
104impl MuscleModel {
105    /// Create a new muscle model.
106    pub fn new(f_max: f64, l_opt: f64, v_max: f64, pennation_angle: f64) -> Self {
107        Self {
108            f_max,
109            l_opt,
110            v_max,
111            pennation_angle,
112        }
113    }
114    /// Typical human tibialis anterior muscle.
115    pub fn tibialis_anterior() -> Self {
116        Self::new(600.0, 0.068, 0.51, 0.17)
117    }
118    /// Typical human quadriceps (rectus femoris).
119    pub fn quadriceps() -> Self {
120        Self::new(1169.0, 0.084, 0.63, 0.17)
121    }
122    /// Active force-length relationship f_l(l̃) where l̃ = l/l_opt.
123    ///
124    /// Gaussian: f_l = exp(-((l̃ - 1)/ω)^2), ω ≈ 0.45
125    pub fn active_force_length(&self, l_normalized: f64) -> f64 {
126        let omega = 0.45;
127        let x = (l_normalized - 1.0) / omega;
128        (-x * x).exp()
129    }
130    /// Passive force-length relationship f_p(l̃).
131    ///
132    /// Exponential rise for l̃ > 1: f_p = (exp(k_p*(l̃-1)/ε_0) - 1) / (exp(k_p) - 1)
133    pub fn passive_force_length(&self, l_normalized: f64) -> f64 {
134        if l_normalized <= 1.0 {
135            return 0.0;
136        }
137        let k_p = 5.0;
138        let eps_0 = 0.6;
139        let numerator = (k_p * (l_normalized - 1.0) / eps_0).exp() - 1.0;
140        let denominator = k_p.exp() - 1.0;
141        (numerator / denominator).min(1.0)
142    }
143    /// Force-velocity relationship f_v(ṽ) where ṽ = v/v_max (Hill's equation).
144    ///
145    /// Concentric: f_v = (1 - ṽ)/(1 + ṽ/a_v), a_v ≈ 0.25
146    /// Eccentric:  f_v = (1.8 - 0.8*(1+ṽ)/(1-7.56*ṽ))
147    pub fn force_velocity(&self, v_normalized: f64) -> f64 {
148        if v_normalized <= 0.0 {
149            let a_v = 0.25;
150            let v = v_normalized.max(-1.0 + 1e-9);
151            ((1.0 + v) / (1.0 - v / a_v)).max(0.0)
152        } else {
153            let v = v_normalized.min(1.0);
154            (1.8 - 0.8 * (1.0 + v) / (1.0 - 7.56 * v / 6.0)).max(0.0)
155        }
156    }
157    /// Compute muscle force \[N\] given fiber length \[m\], velocity \[m/s\], and activation a ∈ \[0,1\].
158    pub fn muscle_force(&self, l: f64, v: f64, activation: f64) -> f64 {
159        let l_norm = l / self.l_opt;
160        let v_norm = v / self.v_max;
161        let fl = self.active_force_length(l_norm);
162        let fp = self.passive_force_length(l_norm);
163        let fv = self.force_velocity(v_norm);
164        let a = activation.clamp(0.0, 1.0);
165        let cos_psi = self.pennation_angle.cos();
166        (a * fl * fv + fp) * self.f_max * cos_psi
167    }
168    /// Maximum power output \[W\].
169    pub fn max_power(&self) -> f64 {
170        self.f_max * self.v_max / 4.0
171    }
172}
173/// Blood vessel model using thin-walled vessel theory (Laplace's law).
174#[derive(Debug, Clone)]
175pub struct BloodVesselModel {
176    /// Reference (unloaded) inner radius R0 \[m\].
177    pub r0: f64,
178    /// Wall thickness h0 \[m\].
179    pub h0: f64,
180    /// Vessel wall elastic modulus E \[Pa\].
181    pub e_wall: f64,
182    /// Poisson's ratio of vessel wall.
183    pub nu_wall: f64,
184}
185impl BloodVesselModel {
186    /// Create a new blood vessel model.
187    pub fn new(r0: f64, h0: f64, e_wall: f64, nu_wall: f64) -> Self {
188        Self {
189            r0,
190            h0,
191            e_wall,
192            nu_wall,
193        }
194    }
195    /// Typical human aorta.
196    pub fn aorta() -> Self {
197        Self::new(12.5e-3, 2.0e-3, 500e3, 0.45)
198    }
199    /// Typical femoral artery.
200    pub fn femoral_artery() -> Self {
201        Self::new(3.5e-3, 0.6e-3, 800e3, 0.45)
202    }
203    /// Circumferential (hoop) wall stress via Laplace's law \[Pa\].
204    ///
205    /// σ_θ = P * R / h
206    pub fn laplace_wall_stress(&self, p_transmural: f64) -> f64 {
207        p_transmural * self.r0 / self.h0
208    }
209    /// Pressure-radius compliance C \[m/Pa\] = dR/dP.
210    ///
211    /// Approximate: C = R0 * (1 - ν^2) / (E * h/R0)
212    pub fn compliance(&self, _p: f64) -> f64 {
213        self.r0 * (1.0 - self.nu_wall * self.nu_wall) / (self.e_wall * self.h0 / self.r0)
214    }
215    /// Pulse wave velocity \[m/s\] (Moens-Korteweg equation).
216    ///
217    /// c = sqrt(E * h / (2 * ρ * R))
218    pub fn pulse_wave_velocity(&self, rho: f64) -> f64 {
219        (self.e_wall * self.h0 / (2.0 * rho * self.r0)).sqrt()
220    }
221    /// Pressure-dependent radius (linearized expansion) \[m\].
222    pub fn radius_at_pressure(&self, p: f64) -> f64 {
223        let c = self.compliance(p);
224        self.r0 + c * p
225    }
226}
227/// Corneal biomechanical model (Ogden-type hyperelastic).
228#[derive(Debug, Clone)]
229pub struct CorneaModel {
230    /// Tangent modulus at small strain \[Pa\].
231    pub e_small: f64,
232    /// Tangent modulus at large strain (linearized) \[Pa\].
233    pub e_large: f64,
234    /// Transition strain.
235    pub transition_strain: f64,
236    /// Corneal thickness (central) \[m\].
237    pub cct: f64,
238    /// Intraocular pressure \[Pa\].
239    pub iop: f64,
240}
241impl CorneaModel {
242    /// Create a new cornea model.
243    pub fn new(e_small: f64, e_large: f64, transition_strain: f64, cct: f64, iop: f64) -> Self {
244        Self {
245            e_small,
246            e_large,
247            transition_strain,
248            cct,
249            iop,
250        }
251    }
252    /// Typical healthy human cornea.
253    pub fn healthy_cornea() -> Self {
254        Self::new(0.01e6, 0.5e6, 0.05, 550e-6, 2000.0)
255    }
256    /// Hoop stress from IOP (thin shell approximation) \[Pa\].
257    ///
258    /// σ = IOP * R / (2 * t)  (sphere approximation R ≈ 7.8 mm)
259    pub fn hoop_stress_iop(&self) -> f64 {
260        let r = 7.8e-3;
261        self.iop * r / (2.0 * self.cct)
262    }
263    /// Effective stiffness modulus at a given strain level \[Pa\].
264    pub fn tangent_modulus(&self, strain: f64) -> f64 {
265        if strain < self.transition_strain {
266            self.e_small
267        } else {
268            self.e_large
269        }
270    }
271}
272/// Failure mode of a biological tissue.
273#[allow(dead_code)]
274#[derive(Debug, Clone, PartialEq)]
275pub enum FailureMode {
276    /// No failure predicted.
277    NoFailure,
278    /// Tensile (ultimate tensile strength exceeded).
279    TensileFailure,
280    /// Compressive (ultimate compressive strength exceeded).
281    CompressiveFailure,
282    /// Shear (ultimate shear strength exceeded).
283    ShearFailure,
284    /// Fatigue (cyclic loading below monotonic UTS).
285    FatigueFailure,
286    /// Fracture (crack propagation, KIc exceeded).
287    FractureFailure,
288}
289/// Anisotropic heart valve leaflet material model.
290#[derive(Debug, Clone)]
291pub struct HeartValveTissue {
292    /// Circumferential (preferred fiber) modulus \[Pa\].
293    pub e_circ: f64,
294    /// Radial modulus \[Pa\].
295    pub e_radial: f64,
296    /// Flexural rigidity \[Pa·m^3\].
297    pub flexural_rigidity: f64,
298    /// Tissue thickness \[m\].
299    pub thickness: f64,
300}
301impl HeartValveTissue {
302    /// Create a new heart valve tissue model.
303    pub fn new(e_circ: f64, e_radial: f64, flexural_rigidity: f64, thickness: f64) -> Self {
304        Self {
305            e_circ,
306            e_radial,
307            flexural_rigidity,
308            thickness,
309        }
310    }
311    /// Typical aortic valve leaflet.
312    pub fn aortic_valve() -> Self {
313        Self::new(15e6, 2e6, 0.001, 0.5e-3)
314    }
315    /// Bending stiffness D = E*t^3/(12*(1-ν^2)) \[Pa·m^3\].
316    pub fn bending_stiffness(&self, nu: f64) -> f64 {
317        self.e_circ * self.thickness.powi(3) / (12.0 * (1.0 - nu * nu))
318    }
319    /// Anisotropy ratio E_circ / E_radial.
320    pub fn anisotropy_ratio(&self) -> f64 {
321        self.e_circ / self.e_radial
322    }
323}
324/// Soft tissue material using the Fung-type (exponential) hyperelastic model.
325///
326/// Strain energy: W = c1 * (exp(Q) - 1) where Q = c2*(I1-3) + alpha*(I2-3) + beta*(J-1)^2
327#[derive(Debug, Clone)]
328pub struct SoftTissueMaterial {
329    /// Material parameter c1 \[Pa\] — scaling of exponential term.
330    pub c1: f64,
331    /// Material parameter c2 \[dimensionless\] — controls I1 contribution.
332    pub c2: f64,
333    /// Material parameter alpha \[dimensionless\] — controls I2 contribution.
334    pub alpha: f64,
335    /// Material parameter beta \[Pa\] — volumetric penalty.
336    pub beta: f64,
337}
338impl SoftTissueMaterial {
339    /// Create a new Fung-type soft tissue model.
340    pub fn new(c1: f64, c2: f64, alpha: f64, beta: f64) -> Self {
341        Self {
342            c1,
343            c2,
344            alpha,
345            beta,
346        }
347    }
348    /// Typical liver soft tissue parameters.
349    pub fn liver() -> Self {
350        Self::new(350.0, 0.57, 0.1, 1000.0)
351    }
352    /// Typical brain tissue parameters (gray matter).
353    pub fn brain() -> Self {
354        Self::new(264.0, 0.4, 0.05, 500.0)
355    }
356    /// Compute the Q exponent argument from invariants I1, I2, J.
357    pub fn q_exponent(&self, i1: f64, i2: f64, j: f64) -> f64 {
358        self.c2 * (i1 - 3.0) + self.alpha * (i2 - 3.0) + self.beta * (j - 1.0).powi(2)
359    }
360    /// Compute strain energy density W \[Pa\] from invariants I1, I2, J.
361    ///
362    /// W = c1 * (exp(Q) - 1)
363    pub fn strain_energy_density(&self, i1: f64, i2: f64, j: f64) -> f64 {
364        let q = self.q_exponent(i1, i2, j);
365        self.c1 * (q.exp() - 1.0)
366    }
367    /// Compute the Cauchy stress tensor \[Pa\] from a Green-Lagrange strain tensor E.
368    ///
369    /// The deformation gradient is approximated as F = I + 2E (small-to-moderate strain).
370    /// Returns the Cauchy stress as \[\[f64;3\];3\].
371    pub fn cauchy_stress_green_lagrange(&self, strain: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
372        let id = identity3();
373        let mut f = [[0.0f64; 3]; 3];
374        for i in 0..3 {
375            for j in 0..3 {
376                f[i][j] = id[i][j] + strain[i][j];
377            }
378        }
379        let c = right_cauchy_green(&f);
380        let i1 = trace3(&c);
381        let i2 = second_invariant_c(&c);
382        let j = det3(&f).abs().max(1e-14);
383        let q = self.q_exponent(i1, i2, j);
384        let exp_q = q.exp();
385        let dw_di1 = self.c1 * self.c2 * exp_q;
386        let dw_di2 = self.c1 * self.alpha * exp_q;
387        let dw_dj = self.c1 * self.beta * 2.0 * (j - 1.0) * exp_q;
388        let ft = transpose3(&f);
389        let b = matmul3(&f, &ft);
390        let b2 = matmul3(&b, &b);
391        let i1_b = trace3(&b);
392        let term1 = scale3(2.0 / j * dw_di1, &b);
393        let term2_inner = add3(&scale3(i1_b, &b), &scale3(-1.0, &b2));
394        let term2 = scale3(2.0 / j * dw_di2, &term2_inner);
395        let term3 = scale3(dw_dj, &id);
396        let sigma1 = add3(&term1, &term2);
397        add3(&sigma1, &term3)
398    }
399}
400/// Tendon and ligament model using quasi-linear viscoelastic (QLV) theory (Fung 1972).
401///
402/// Models the nonlinear elastic toe-to-linear response and stress relaxation.
403#[derive(Debug, Clone)]
404pub struct TendonLigamentModel {
405    /// Toe-region tangent modulus \[Pa\].
406    pub toe_region_modulus: f64,
407    /// Linear-region (post-transition) modulus \[Pa\].
408    pub linear_modulus: f64,
409    /// Transition strain (toe-to-linear) \[dimensionless\].
410    pub transition_strain: f64,
411    /// QLV reduced relaxation coefficient C \[dimensionless\].
412    pub c_relax: f64,
413    /// QLV fast time constant τ1 \[s\].
414    pub tau1: f64,
415    /// QLV slow time constant τ2 \[s\].
416    pub tau2: f64,
417}
418impl TendonLigamentModel {
419    /// Create a new tendon/ligament model.
420    pub fn new(
421        toe_region_modulus: f64,
422        linear_modulus: f64,
423        transition_strain: f64,
424        c_relax: f64,
425        tau1: f64,
426        tau2: f64,
427    ) -> Self {
428        Self {
429            toe_region_modulus,
430            linear_modulus,
431            transition_strain,
432            c_relax,
433            tau1,
434            tau2,
435        }
436    }
437    /// Typical human patellar tendon.
438    pub fn patellar_tendon() -> Self {
439        Self::new(200e6, 1500e6, 0.02, 0.4, 0.01, 10.0)
440    }
441    /// Typical human ACL (anterior cruciate ligament).
442    pub fn acl() -> Self {
443        Self::new(50e6, 300e6, 0.03, 0.35, 0.02, 15.0)
444    }
445    /// Nonlinear stress-strain response σ(ε) \[Pa\].
446    ///
447    /// Toe region (ε < ε_t): σ = E_toe * ε * (ε / ε_t)
448    /// Linear region (ε ≥ ε_t): σ = E_toe*ε_t + E_linear*(ε - ε_t)
449    pub fn stress_strain_nonlinear(&self, strain: f64) -> f64 {
450        if strain < 0.0 {
451            return 0.0;
452        }
453        if strain < self.transition_strain {
454            self.toe_region_modulus * strain * strain / self.transition_strain
455        } else {
456            let sigma_transition = self.toe_region_modulus * self.transition_strain;
457            sigma_transition + self.linear_modulus * (strain - self.transition_strain)
458        }
459    }
460    /// Reduced relaxation function G(t) \[dimensionless\] from QLV theory.
461    ///
462    /// G(t) = (1 + C * (E1(t/τ1) - E1(t/τ2))) / (1 + C*ln(τ2/τ1))
463    /// Approximated using a two-exponential Prony series.
464    pub fn relaxation_function(&self, t: f64) -> f64 {
465        if t <= 0.0 {
466            return 1.0;
467        }
468        let g1 = (-t / self.tau1).exp();
469        let g2 = (-t / self.tau2).exp();
470        let denom = 1.0 + self.c_relax * (self.tau2 / self.tau1).ln();
471        let amplitude = self.c_relax / denom;
472        let g_inf = 1.0 - amplitude;
473        g_inf + 0.5 * amplitude * g1 + 0.5 * amplitude * g2
474    }
475    /// Viscoelastic stress via convolution integral (discrete time step approximation).
476    ///
477    /// σ(t) = G(t) * σ_e(ε) where σ_e is the instantaneous (elastic) stress.
478    pub fn viscoelastic_stress(&self, strain: f64, t: f64) -> f64 {
479        let sigma_elastic = self.stress_strain_nonlinear(strain);
480        let g_t = self.relaxation_function(t);
481        sigma_elastic * g_t
482    }
483    /// Ultimate tensile strength estimate from linear modulus and failure strain.
484    pub fn ultimate_stress(&self, failure_strain: f64) -> f64 {
485        self.stress_strain_nonlinear(failure_strain)
486    }
487}
488/// Trabecular (cancellous) bone model.
489#[derive(Debug, Clone)]
490pub struct TrabecularBone {
491    /// Bone volume fraction BV/TV \[dimensionless\] ∈ (0, 1).
492    pub bv_tv: f64,
493    /// Ash fraction \[dimensionless\].
494    pub ash_fraction: f64,
495}
496impl TrabecularBone {
497    /// Create a new trabecular bone model.
498    pub fn new(bv_tv: f64, ash_fraction: f64) -> Self {
499        Self {
500            bv_tv,
501            ash_fraction,
502        }
503    }
504    /// Typical vertebral trabecular bone.
505    pub fn vertebral() -> Self {
506        Self::new(0.15, 0.55)
507    }
508    /// Elastic modulus \[MPa\] of trabecular bone via BV/TV.
509    ///
510    /// E = 1904 * BV/TV^1.64 \[MPa\] (Kopperdahl & Keaveny 1998)
511    pub fn elastic_modulus_trabecular(&self) -> f64 {
512        1904.0 * self.bv_tv.powf(1.64)
513    }
514    /// Yield stress \[MPa\] in compression.
515    ///
516    /// σ_y = 24.8 * BV/TV^1.6 \[MPa\]
517    pub fn yield_stress_trabecular(&self) -> f64 {
518        24.8 * self.bv_tv.powf(1.6)
519    }
520    /// Fabric tensor isotropy ratio (0 = fully anisotropic, 1 = isotropic).
521    pub fn isotropy_ratio(&self) -> f64 {
522        (self.bv_tv / 0.35).min(1.0)
523    }
524}
525/// A simpler ligament wrapping model for joint constraint forces.
526#[derive(Debug, Clone)]
527pub struct LigamentWrapModel {
528    /// Rest (zero-force) length \[m\].
529    pub rest_length: f64,
530    /// Stiffness k \[N/m\].
531    pub stiffness: f64,
532    /// Slack length (below this: no force) \[m\].
533    pub slack_length: f64,
534}
535impl LigamentWrapModel {
536    /// Create a new ligament wrap model.
537    pub fn new(rest_length: f64, stiffness: f64, slack_length: f64) -> Self {
538        Self {
539            rest_length,
540            stiffness,
541            slack_length,
542        }
543    }
544    /// Restraint force \[N\] for a given current length \[m\].
545    pub fn restraint_force(&self, current_length: f64) -> f64 {
546        if current_length <= self.slack_length {
547            0.0
548        } else {
549            self.stiffness * (current_length - self.slack_length)
550        }
551    }
552    /// Strain = (l - l0) / l0.
553    pub fn strain(&self, current_length: f64) -> f64 {
554        (current_length - self.rest_length) / self.rest_length
555    }
556}
557/// Failure criteria for biological soft and hard tissues.
558///
559/// Implements:
560/// - Maximum principal stress criterion (brittle fracture analog)
561/// - Tsai-Wu criterion (anisotropic composite analog for fibrous tissues)
562/// - Bone fracture criterion based on stress intensity factor KI
563/// - Soft tissue tear criterion based on critical energy release rate Gc
564#[allow(dead_code)]
565#[derive(Debug, Clone)]
566pub struct TissueFailureCriteria {
567    /// Ultimate tensile strength \[Pa\].
568    pub uts: f64,
569    /// Ultimate compressive strength \[Pa\] (positive value).
570    pub ucs: f64,
571    /// Ultimate shear strength \[Pa\].
572    pub uss: f64,
573    /// Fatigue strength coefficient (Basquin σ'f) \[Pa\].
574    pub fatigue_coeff: f64,
575    /// Fatigue exponent (Basquin b, negative).
576    pub fatigue_exp: f64,
577    /// Critical stress intensity factor KIc \[Pa√m\].
578    pub k_ic: f64,
579    /// Critical energy release rate Gc \[J/m²\].
580    pub g_c: f64,
581    /// Tsai-Wu interaction coefficient F12* (normalized).
582    pub tsai_wu_f12_star: f64,
583}
584impl TissueFailureCriteria {
585    /// Cortical bone failure criteria (Reilly & Burstein 1975).
586    pub fn cortical_bone() -> Self {
587        Self {
588            uts: 133e6,
589            ucs: 193e6,
590            uss: 68e6,
591            fatigue_coeff: 140e6,
592            fatigue_exp: -0.08,
593            k_ic: 2.2e6,
594            g_c: 600.0,
595            tsai_wu_f12_star: -0.5,
596        }
597    }
598    /// Articular cartilage failure criteria.
599    pub fn articular_cartilage() -> Self {
600        Self {
601            uts: 15e6,
602            ucs: 5e6,
603            uss: 4e6,
604            fatigue_coeff: 10e6,
605            fatigue_exp: -0.10,
606            k_ic: 0.2e6,
607            g_c: 100.0,
608            tsai_wu_f12_star: -0.5,
609        }
610    }
611    /// Ligament/tendon failure criteria.
612    pub fn ligament() -> Self {
613        Self {
614            uts: 38e6,
615            ucs: 1e6,
616            uss: 10e6,
617            fatigue_coeff: 30e6,
618            fatigue_exp: -0.09,
619            k_ic: 0.5e6,
620            g_c: 200.0,
621            tsai_wu_f12_star: -0.5,
622        }
623    }
624    /// Maximum principal stress criterion.
625    ///
626    /// Returns the [`FailureMode`] given the three principal stresses \[Pa\].
627    pub fn max_principal_stress(&self, s1: f64, s2: f64, s3: f64) -> FailureMode {
628        let max_tensile = s1.max(s2).max(s3);
629        let max_compressive = s1.min(s2).min(s3);
630        if max_tensile >= self.uts {
631            FailureMode::TensileFailure
632        } else if max_compressive <= -self.ucs {
633            FailureMode::CompressiveFailure
634        } else {
635            FailureMode::NoFailure
636        }
637    }
638    /// Safety factor against tensile failure: UTS / σ_max.
639    pub fn tensile_safety_factor(&self, sigma_max: f64) -> f64 {
640        if sigma_max <= 0.0 {
641            f64::INFINITY
642        } else {
643            self.uts / sigma_max
644        }
645    }
646    /// Tsai-Wu criterion for fibrous tissue (fiber direction = 1, transverse = 2).
647    ///
648    /// F1·σ1 + F2·σ2 + F11·σ1² + F22·σ2² + 2·F12·σ1·σ2 + F66·τ12² = 1 at failure.
649    /// Returns the failure index (< 1 = safe, ≥ 1 = failure).
650    pub fn tsai_wu_index(&self, sigma1: f64, sigma2: f64, tau12: f64) -> f64 {
651        let f1 = 1.0 / self.uts - 1.0 / self.ucs;
652        let f2 = f1;
653        let f11 = 1.0 / (self.uts * self.ucs);
654        let f22 = f11;
655        let f66 = 1.0 / (self.uss * self.uss);
656        let f12 = self.tsai_wu_f12_star * (f11 * f22).sqrt();
657        f1 * sigma1
658            + f2 * sigma2
659            + f11 * sigma1 * sigma1
660            + f22 * sigma2 * sigma2
661            + 2.0 * f12 * sigma1 * sigma2
662            + f66 * tau12 * tau12
663    }
664    /// Fatigue life in cycles (Basquin law): σ_a = σ'f · (2N)^b.
665    ///
666    /// Returns the number of half-cycles to failure (reversals 2N_f).
667    pub fn fatigue_life_cycles(&self, stress_amplitude: f64) -> f64 {
668        if stress_amplitude <= 0.0 {
669            return f64::INFINITY;
670        }
671        let ratio = stress_amplitude / self.fatigue_coeff;
672        if ratio <= 0.0 {
673            return f64::INFINITY;
674        }
675        let two_n = ratio.powf(1.0 / self.fatigue_exp);
676        two_n / 2.0
677    }
678    /// Fracture criterion: compare mode-I stress intensity factor KI \[Pa√m\]
679    /// with the critical value KIc.  Returns [`FailureMode::FractureFailure`]
680    /// if KI ≥ KIc.
681    pub fn fracture_criterion(&self, k_i: f64) -> FailureMode {
682        if k_i >= self.k_ic {
683            FailureMode::FractureFailure
684        } else {
685            FailureMode::NoFailure
686        }
687    }
688    /// Energy release rate G \[J/m²\] from stress intensity factor (plane strain).
689    ///
690    /// G = KI² * (1 - ν²) / E.
691    pub fn energy_release_rate(&self, k_i: f64, e_modulus: f64, poisson: f64) -> f64 {
692        k_i * k_i * (1.0 - poisson * poisson) / e_modulus
693    }
694    /// Check whether critical energy release rate Gc is exceeded.
695    pub fn toughness_criterion(&self, k_i: f64, e_modulus: f64, poisson: f64) -> FailureMode {
696        let g = self.energy_release_rate(k_i, e_modulus, poisson);
697        if g >= self.g_c {
698            FailureMode::FractureFailure
699        } else {
700            FailureMode::NoFailure
701        }
702    }
703}
704/// Biphasic cartilage model (Mow et al.).
705///
706/// Treats cartilage as a mixture of solid and fluid phases.
707#[derive(Debug, Clone)]
708pub struct CartilageModel {
709    /// Aggregate modulus H_A \[Pa\] — equilibrium compressive stiffness.
710    pub solid_modulus_h_a: f64,
711    /// Hydraulic permeability k \[m^4/Ns\].
712    pub permeability: f64,
713    /// Fluid volume fraction (porosity) φ \[dimensionless\].
714    pub fluid_fraction: f64,
715    /// Specimen thickness H \[m\].
716    pub thickness: f64,
717}
718impl CartilageModel {
719    /// Create a new biphasic cartilage model.
720    pub fn new(
721        solid_modulus_h_a: f64,
722        permeability: f64,
723        fluid_fraction: f64,
724        thickness: f64,
725    ) -> Self {
726        Self {
727            solid_modulus_h_a,
728            permeability,
729            fluid_fraction,
730            thickness,
731        }
732    }
733    /// Typical articular cartilage (knee).
734    pub fn articular_cartilage() -> Self {
735        Self::new(0.5e6, 1e-15, 0.75, 2e-3)
736    }
737    /// Consolidation coefficient c_v = H_A * k \[m^2/s\].
738    pub fn consolidation_coefficient(&self) -> f64 {
739        self.solid_modulus_h_a * self.permeability
740    }
741    /// Creep compliance J(t) under constant stress σ_0 \[1/Pa\].
742    ///
743    /// J(t) = (1/H_A) * \[1 - sum_n (8/(π^2(2n-1)^2)) * exp(-cv*(2n-1)^2*π^2*t/(4H^2))\]
744    pub fn creep_compliance(&self, t: f64) -> f64 {
745        let cv = self.consolidation_coefficient();
746        let h = self.thickness;
747        let mut sum = 0.0;
748        for n in 1..=20usize {
749            let m = (2 * n - 1) as f64;
750            let coeff = 8.0 / (PI * PI * m * m);
751            let exponent = -cv * m * m * PI * PI * t / (4.0 * h * h);
752            sum += coeff * exponent.exp();
753        }
754        (1.0 / self.solid_modulus_h_a) * (1.0 - sum)
755    }
756    /// Stress relaxation G(t) normalized by initial stress \[dimensionless\].
757    ///
758    /// G(t) = sum_n (8/(π^2(2n-1)^2)) * exp(-cv*(2n-1)^2*π^2*t/(4H^2))
759    pub fn stress_relaxation(&self, t: f64) -> f64 {
760        let cv = self.consolidation_coefficient();
761        let h = self.thickness;
762        let mut sum = 0.0;
763        for n in 1..=20usize {
764            let m = (2 * n - 1) as f64;
765            let coeff = 8.0 / (PI * PI * m * m);
766            let exponent = -cv * m * m * PI * PI * t / (4.0 * h * h);
767            sum += coeff * exponent.exp();
768        }
769        sum
770    }
771}
772/// Two-layer skin model with epidermis and dermis.
773#[derive(Debug, Clone)]
774pub struct SkinModel {
775    /// Young's modulus of epidermis \[Pa\].
776    pub e_epidermis: f64,
777    /// Young's modulus of dermis \[Pa\].
778    pub e_dermis: f64,
779    /// Poisson's ratio of epidermis.
780    pub nu_epidermis: f64,
781    /// Poisson's ratio of dermis.
782    pub nu_dermis: f64,
783    /// Thickness of epidermis \[m\].
784    pub thickness_e: f64,
785    /// Thickness of dermis \[m\].
786    pub thickness_d: f64,
787}
788impl SkinModel {
789    /// Create a new two-layer skin model.
790    pub fn new(
791        e_epidermis: f64,
792        e_dermis: f64,
793        nu_epidermis: f64,
794        nu_dermis: f64,
795        thickness_e: f64,
796        thickness_d: f64,
797    ) -> Self {
798        Self {
799            e_epidermis,
800            e_dermis,
801            nu_epidermis,
802            nu_dermis,
803            thickness_e,
804            thickness_d,
805        }
806    }
807    /// Typical human forearm skin.
808    pub fn human_forearm() -> Self {
809        Self::new(1e6, 0.08e6, 0.48, 0.49, 0.1e-3, 1.2e-3)
810    }
811    /// Effective modulus for a given indentation depth \[Pa\].
812    ///
813    /// Uses depth-dependent weighting: shallow → epidermis dominated, deep → dermis dominated.
814    pub fn effective_modulus(&self, indentation: f64) -> f64 {
815        let h_e = self.thickness_e;
816        let w = (indentation / h_e).min(1.0);
817
818        (1.0 - w) * self.e_epidermis + w * self.e_dermis
819    }
820    /// Effective Poisson's ratio (depth-dependent).
821    pub fn effective_poisson(&self, indentation: f64) -> f64 {
822        let w = (indentation / self.thickness_e).min(1.0);
823        (1.0 - w) * self.nu_epidermis + w * self.nu_dermis
824    }
825    /// Hertz contact force \[N\] for spherical indenter of radius r \[m\],
826    /// effective modulus E_eff \[Pa\], indentation δ \[m\].
827    ///
828    /// F = (4/3) * E_eff / (1-ν^2) * sqrt(r) * δ^(3/2)
829    pub fn indentation_force_hertz(&self, r_indenter: f64, e_eff: f64, delta: f64) -> f64 {
830        let nu = self.effective_poisson(delta);
831        let e_reduced = e_eff / (1.0 - nu * nu);
832        (4.0 / 3.0) * e_reduced * r_indenter.sqrt() * delta.abs().powf(1.5)
833    }
834    /// Total skin thickness \[m\].
835    pub fn total_thickness(&self) -> f64 {
836        self.thickness_e + self.thickness_d
837    }
838}
839/// Simplified intervertebral disc model (annulus fibrosus + nucleus pulposus).
840#[derive(Debug, Clone)]
841pub struct IntervertebralDiscModel {
842    /// Nucleus pulposus bulk modulus \[Pa\].
843    pub nucleus_bulk_modulus: f64,
844    /// Annulus fibrosus shear modulus \[Pa\].
845    pub annulus_shear_modulus: f64,
846    /// Disc height \[m\].
847    pub height: f64,
848    /// Outer disc radius \[m\].
849    pub outer_radius: f64,
850    /// Inner (nucleus) radius \[m\].
851    pub inner_radius: f64,
852}
853impl IntervertebralDiscModel {
854    /// Create a new intervertebral disc model.
855    pub fn new(
856        nucleus_bulk_modulus: f64,
857        annulus_shear_modulus: f64,
858        height: f64,
859        outer_radius: f64,
860        inner_radius: f64,
861    ) -> Self {
862        Self {
863            nucleus_bulk_modulus,
864            annulus_shear_modulus,
865            height,
866            outer_radius,
867            inner_radius,
868        }
869    }
870    /// Typical lumbar L4-L5 disc.
871    pub fn lumbar_l4_l5() -> Self {
872        Self::new(1.72e6, 0.17e6, 11e-3, 23e-3, 12e-3)
873    }
874    /// Effective compressive stiffness of the disc \[N/m\].
875    pub fn compressive_stiffness(&self) -> f64 {
876        let area_n = PI * self.inner_radius * self.inner_radius;
877        let k_nucleus = self.nucleus_bulk_modulus * area_n / self.height;
878        let area_a =
879            PI * (self.outer_radius * self.outer_radius - self.inner_radius * self.inner_radius);
880        let k_annulus = self.annulus_shear_modulus * area_a / self.height;
881        k_nucleus + k_annulus
882    }
883    /// Intradiscal pressure under axial load F \[N\] → Pa.
884    pub fn intradiscal_pressure(&self, axial_force: f64) -> f64 {
885        let area_n = PI * self.inner_radius * self.inner_radius;
886        axial_force / area_n
887    }
888}
889/// Fiber-reinforced soft tissue with Holzapfel-Gasser-Ogden style fiber model.
890///
891/// Models a fiber family with mean orientation `a0` and angular dispersion `kappa`.
892#[derive(Debug, Clone)]
893pub struct FiberReinforcedTissue {
894    /// Ground matrix c \[Pa\].
895    pub c_matrix: f64,
896    /// Mean fiber direction (unit vector) in reference configuration.
897    pub a0: [f64; 3],
898    /// Fiber dispersion parameter κ ∈ \[0, 1/3\] (0 = aligned, 1/3 = isotropic).
899    pub kappa: f64,
900    /// Fiber stiffness k1 \[Pa\].
901    pub k1: f64,
902    /// Fiber exponential k2 \[dimensionless\].
903    pub k2: f64,
904}
905impl FiberReinforcedTissue {
906    /// Create a new fiber-reinforced tissue model.
907    pub fn new(c_matrix: f64, a0: [f64; 3], kappa: f64, k1: f64, k2: f64) -> Self {
908        Self {
909            c_matrix,
910            a0,
911            kappa,
912            k1,
913            k2,
914        }
915    }
916    /// Typical aortic tissue model (circumferential fiber family).
917    pub fn aortic_tissue() -> Self {
918        let a0 = [0.0, 1.0, 0.0];
919        Self::new(18.4e3, a0, 0.226, 996.6e3, 524.6)
920    }
921    /// Compute the fiber strain energy W_f \[Pa\] for a given I4 invariant.
922    ///
923    /// W_f = (k1/(2k2)) * (exp(k2*(I4-1)^2) - 1)  when I4 >= 1 (tension only).
924    pub fn fiber_strain_energy(&self, i4: f64) -> f64 {
925        if i4 < 1.0 {
926            return 0.0;
927        }
928        let e = i4 - 1.0;
929        (self.k1 / (2.0 * self.k2)) * ((self.k2 * e * e).exp() - 1.0)
930    }
931    /// Compute total strain energy W \[Pa\] from I1 and I4.
932    ///
933    /// W = c * (I1 - 3) + W_fiber(I4)
934    pub fn total_strain_energy(&self, i1: f64, i4: f64) -> f64 {
935        let w_matrix = self.c_matrix * (i1 - 3.0);
936        let w_fiber = self.fiber_strain_energy(i4);
937        w_matrix + w_fiber
938    }
939    /// Compute the effective I4 = a0 . C . a0 given deformation gradient F.
940    #[allow(clippy::needless_range_loop)]
941    pub fn compute_i4(&self, f: &[[f64; 3]; 3]) -> f64 {
942        let c = right_cauchy_green(f);
943        let mut ca = [0.0f64; 3];
944        for i in 0..3 {
945            for j in 0..3 {
946                ca[i] += c[i][j] * self.a0[j];
947            }
948        }
949        let mut i4 = 0.0;
950        for i in 0..3 {
951            i4 += self.a0[i] * ca[i];
952        }
953        i4
954    }
955}
956/// Cardiac muscle constitutive model (passive + active).
957///
958/// Combines:
959/// - **Passive**: transversely isotropic hyperelastic material (Guccione et al. 1991)
960///   with exponential fiber-sheet description.
961/// - **Active**: time-varying elastance model (Sagawa) for systolic force.
962///
963/// Fiber direction is assumed to be aligned with the local e₁ axis.
964#[allow(dead_code)]
965#[derive(Debug, Clone)]
966pub struct CardiacMuscleModel {
967    /// Passive stiffness parameter C \[Pa\].
968    pub c_passive: f64,
969    /// Fiber exponential coefficient bf.
970    pub bf: f64,
971    /// Cross-fiber coefficient bt.
972    pub bt: f64,
973    /// Fiber-sheet coupling coefficient bfs.
974    pub bfs: f64,
975    /// Maximum active stress Tmax \[Pa\].
976    pub t_max: f64,
977    /// Calcium sensitivity parameter Ca50 \[μM\].
978    pub ca50: f64,
979    /// Hill coefficient n for calcium activation.
980    pub n_hill: f64,
981    /// Sarcomere length at zero active stress l0 \[μm\].
982    pub l0: f64,
983}
984impl CardiacMuscleModel {
985    /// Default left ventricular myocardium parameters (Guccione et al. 1995).
986    pub fn left_ventricle() -> Self {
987        Self {
988            c_passive: 880.0,
989            bf: 18.48,
990            bt: 3.58,
991            bfs: 1.627,
992            t_max: 135_480.0,
993            ca50: 0.805,
994            n_hill: 2.0,
995            l0: 1.58,
996        }
997    }
998    /// Passive strain energy density \[Pa\] from the Guccione exponential model.
999    ///
1000    /// `e_ff`, `e_ss`, `e_nn` are normal Green strains in fiber, sheet, normal directions.
1001    /// `e_fs` is the fiber-sheet shear strain.
1002    pub fn passive_strain_energy(&self, e_ff: f64, e_ss: f64, e_nn: f64, e_fs: f64) -> f64 {
1003        let q = self.bf * e_ff * e_ff
1004            + self.bt * (e_ss * e_ss + e_nn * e_nn)
1005            + self.bfs * (2.0 * e_fs * e_fs);
1006        self.c_passive / 2.0 * (q.exp() - 1.0)
1007    }
1008    /// Active fiber stress \[Pa\] given sarcomere length `l` \[μm\] and
1009    /// intracellular calcium concentration `ca` \[μM\].
1010    ///
1011    /// Uses the steady-state Hill equation for calcium activation.
1012    pub fn active_fiber_stress(&self, l: f64, ca: f64) -> f64 {
1013        if l <= self.l0 {
1014            return 0.0;
1015        }
1016        let ca50_n = self.ca50.powf(self.n_hill);
1017        let ca_n = ca.powf(self.n_hill);
1018        let activation = ca_n / (ca_n + ca50_n);
1019        let l_factor = ((l - self.l0) / (2.3 - self.l0)).clamp(0.0, 1.0);
1020        self.t_max * activation * l_factor
1021    }
1022    /// Total (passive + active) fiber stress \[Pa\].
1023    pub fn total_fiber_stress(
1024        &self,
1025        e_ff: f64,
1026        e_ss: f64,
1027        e_nn: f64,
1028        e_fs: f64,
1029        l: f64,
1030        ca: f64,
1031    ) -> f64 {
1032        let w = self.passive_strain_energy(e_ff, e_ss, e_nn, e_fs);
1033        let dw = {
1034            let delta = 1e-6;
1035            let w2 = self.passive_strain_energy(e_ff + delta, e_ss, e_nn, e_fs);
1036            (w2 - w) / delta
1037        };
1038        dw + self.active_fiber_stress(l, ca)
1039    }
1040    /// End-systolic pressure–volume relationship slope Ees \[Pa/m³\] (simplified).
1041    ///
1042    /// Uses the time-varying elastance: E(t) = (Emax - Emin)*f(t) + Emin.
1043    pub fn elastance_at_peak(&self, volume: f64, v0: f64) -> f64 {
1044        self.t_max / (volume - v0).abs().max(1e-6)
1045    }
1046    /// Stroke work \[J\] given end-diastolic volume `edv` \[m³\], end-systolic volume `esv` \[m³\],
1047    /// and mean arterial pressure `map` \[Pa\].
1048    pub fn stroke_work(&self, edv: f64, esv: f64, _map: f64) -> f64 {
1049        let sv = edv - esv;
1050        let ees = self.elastance_at_peak(edv, esv);
1051        ees * sv * sv / 2.0
1052    }
1053}
1054/// Composite bone model combining cortical and trabecular bone.
1055#[derive(Debug, Clone)]
1056pub struct BoneModel {
1057    /// Cortical bone layer.
1058    pub cortical: CorticalBone,
1059    /// Trabecular bone core.
1060    pub trabecular: TrabecularBone,
1061    /// Cortical shell thickness \[m\].
1062    pub cortical_thickness: f64,
1063    /// Total bone diameter/width \[m\].
1064    pub total_width: f64,
1065}
1066impl BoneModel {
1067    /// Create a new composite bone model.
1068    pub fn new(
1069        cortical: CorticalBone,
1070        trabecular: TrabecularBone,
1071        cortical_thickness: f64,
1072        total_width: f64,
1073    ) -> Self {
1074        Self {
1075            cortical,
1076            trabecular,
1077            cortical_thickness,
1078            total_width,
1079        }
1080    }
1081    /// Typical femoral neck cross-section.
1082    pub fn femoral_neck() -> Self {
1083        Self::new(
1084            CorticalBone::femur_cortical(),
1085            TrabecularBone::vertebral(),
1086            2e-3,
1087            30e-3,
1088        )
1089    }
1090    /// Estimate effective axial modulus via rule of mixtures \[MPa\].
1091    pub fn effective_axial_modulus(&self) -> f64 {
1092        let r_total = self.total_width / 2.0;
1093        let r_inner = r_total - self.cortical_thickness;
1094        let a_cortical = PI * (r_total * r_total - r_inner * r_inner);
1095        let a_trabecular = PI * r_inner * r_inner;
1096        let a_total = PI * r_total * r_total;
1097        (self.cortical.elastic_modulus() * a_cortical
1098            + self.trabecular.elastic_modulus_trabecular() * a_trabecular)
1099            / a_total
1100    }
1101}
1102/// Huxley (1957) crossbridge model for skeletal muscle contraction.
1103///
1104/// Describes active force generation through attachment/detachment kinetics of
1105/// myosin crossbridges along the actin filament.  The model tracks the fraction
1106/// `n(x)` of attached crossbridges at extension `x`.
1107///
1108/// Rate constants:
1109/// - Attachment: `f(x)` = `f1` if 0 < x < h, 0 otherwise.
1110/// - Detachment: `g(x)` = `g1` if 0 < x < h, `g2` if x < 0.
1111#[allow(dead_code)]
1112#[derive(Debug, Clone)]
1113pub struct HuxleyModel {
1114    /// Stiffness of a single crossbridge \[N/m\].
1115    pub crossbridge_stiffness: f64,
1116    /// Maximum stroke distance h \[m\] — range of positive attachment.
1117    pub h: f64,
1118    /// Attachment rate constant f1 \[s⁻¹\].
1119    pub f1: f64,
1120    /// Detachment rate in positive region g1 \[s⁻¹\].
1121    pub g1: f64,
1122    /// Detachment rate in negative region g2 \[s⁻¹\].
1123    pub g2: f64,
1124    /// Number density of crossbridges per unit length \[m⁻¹\].
1125    pub crossbridge_density: f64,
1126}
1127impl HuxleyModel {
1128    /// Construct a new Huxley model with explicit parameters.
1129    pub fn new(
1130        crossbridge_stiffness: f64,
1131        h: f64,
1132        f1: f64,
1133        g1: f64,
1134        g2: f64,
1135        crossbridge_density: f64,
1136    ) -> Self {
1137        Self {
1138            crossbridge_stiffness,
1139            h,
1140            f1,
1141            g1,
1142            g2,
1143            crossbridge_density,
1144        }
1145    }
1146    /// Default fast-twitch skeletal muscle parameters.
1147    pub fn fast_twitch() -> Self {
1148        Self::new(2.0e-3, 11e-9, 3.68e9, 1.0e9, 3.68e9, 5.0e14)
1149    }
1150    /// Steady-state attached fraction `n_ss(x)` at extension `x` \[m\].
1151    ///
1152    /// Derived analytically from `(f+g)*n = f` in the attachment zone.
1153    pub fn steady_state_fraction(&self, x: f64) -> f64 {
1154        if x > 0.0 && x < self.h {
1155            self.f1 / (self.f1 + self.g1)
1156        } else {
1157            0.0
1158        }
1159    }
1160    /// Isometric (zero velocity) force per unit cross-sectional area \[Pa\].
1161    ///
1162    /// Integrates `n_ss(x) * k * x` over the crossbridge extension range.
1163    pub fn isometric_force(&self) -> f64 {
1164        let n_ss = self.f1 / (self.f1 + self.g1);
1165        self.crossbridge_density * n_ss * self.crossbridge_stiffness * self.h * self.h / 2.0
1166    }
1167    /// Force–velocity relationship using the Huxley model at sliding velocity `v` \[m/s\].
1168    ///
1169    /// Positive `v` = shortening (crossbridges move from positive to negative extension).
1170    /// Returns force relative to isometric force (dimensionless).
1171    pub fn force_velocity_ratio(&self, v: f64) -> f64 {
1172        if v.abs() < 1e-30 {
1173            return 1.0;
1174        }
1175        let f1 = self.f1;
1176        let g1 = self.g1;
1177        let g2 = self.g2;
1178        let h = self.h;
1179        if v > 0.0 {
1180            let phi = v / (h * (f1 + g1));
1181            let numerator = 1.0 - phi * (g2 / (f1 + g2));
1182            numerator.max(0.0)
1183        } else {
1184            let phi = (-v) / (h * (f1 + g1));
1185            (1.0 + phi * g2 / (f1 + g2)).min(1.8)
1186        }
1187    }
1188    /// Average power output at shortening velocity `v` \[W per unit area, Pa·m/s\].
1189    pub fn power_output(&self, v: f64) -> f64 {
1190        let f0 = self.isometric_force();
1191        f0 * self.force_velocity_ratio(v) * v
1192    }
1193    /// Optimal shortening velocity for maximum power \[m/s\].
1194    ///
1195    /// Uses a numerical golden-section search over \[0, v_max\].
1196    pub fn optimal_velocity(&self, v_max: f64) -> f64 {
1197        let mut a = 0.0f64;
1198        let mut b = v_max;
1199        let phi = (5.0_f64.sqrt() - 1.0) / 2.0;
1200        let tol = 1e-9;
1201        let mut c = b - phi * (b - a);
1202        let mut d = a + phi * (b - a);
1203        while (b - a).abs() > tol {
1204            if self.power_output(c) < self.power_output(d) {
1205                a = c;
1206            } else {
1207                b = d;
1208            }
1209            c = b - phi * (b - a);
1210            d = a + phi * (b - a);
1211        }
1212        (a + b) / 2.0
1213    }
1214}