Skip to main content

oxiphysics_materials/
biomedical_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Biomedical material models: soft tissue, bone, blood, hydrogels.
5//!
6//! Implements physics-based constitutive models for biological materials:
7//! - [`SoftTissue`]: Fung hyperelastic and Holzapfel-Gasser-Ogden (HGO) models
8//! - [`BoneModel`]: Cortical/cancellous bone with Gibson-Ashby and Wolff's law
9//! - [`BloodRheology`]: Carreau-Yasuda and Casson non-Newtonian models
10//! - [`HydrogelModel`]: Flory-Rehner equilibrium swelling theory
11//! - [`CartilageModel`]: Mow biphasic theory for articular cartilage
12//! - [`ArterialWall`]: Three-layer arterial wall with residual stress
13//! - [`DegradablePolymer`]: Hydrolytic degradation kinetics
14
15/// Type alias for a 3D vector represented as a plain array.
16pub type Vec3 = [f64; 3];
17
18/// Type alias for a symmetric 3×3 Cauchy-Green strain tensor stored as
19/// \[E11, E22, E33, E12, E13, E23\].
20pub type StrainTensor = [f64; 6];
21
22// ---------------------------------------------------------------------------
23// SoftTissue
24// ---------------------------------------------------------------------------
25
26/// Biomedical soft-tissue constitutive model.
27///
28/// Implements two hyperelastic frameworks commonly used in biomechanics:
29///
30/// 1. **Fung exponential** (quasi-linear viscoelastic): strain-energy
31///    `W = C*(exp(Q) - 1)` where `Q` is a quadratic form in the Green-Lagrange
32///    strain components.
33/// 2. **Holzapfel-Gasser-Ogden (HGO)**: fiber-reinforced model for arterial
34///    walls, with an isotropic neo-Hookean matrix and two fiber families.
35/// 3. **Active stress**: additive active-passive decomposition for cardiac muscle.
36#[derive(Debug, Clone)]
37pub struct SoftTissue {
38    /// Material parameter C \[Pa\] for the Fung model ground-matrix stiffness.
39    pub fung_c: f64,
40    /// Fung exponential coefficients \[b11, b22, b33, b12, b13, b23\] (dimensionless).
41    pub fung_b: [f64; 6],
42    /// HGO: neo-Hookean ground-matrix parameter μ \[Pa\].
43    pub hgo_mu: f64,
44    /// HGO: fiber stiffness k1 \[Pa\].
45    pub hgo_k1: f64,
46    /// HGO: fiber exponential nonlinearity k2 (dimensionless).
47    pub hgo_k2: f64,
48    /// HGO: fiber dispersion parameter κ ∈ \[0, 1/3\].
49    pub hgo_kappa: f64,
50    /// HGO: mean fiber angle θ \[rad\] relative to the circumferential direction.
51    pub fiber_angle: f64,
52    /// Active stress magnitude \[Pa\] (cardiac muscle).
53    pub active_stress: f64,
54}
55
56impl SoftTissue {
57    /// Construct a new `SoftTissue` with Fung parameters.
58    ///
59    /// # Arguments
60    /// * `fung_c` – Ground-matrix stiffness \[Pa\].
61    /// * `fung_b` – Exponential coefficients \[b11, b22, b33, b12, b13, b23\].
62    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    /// Construct a new `SoftTissue` with HGO (fiber-reinforced) parameters.
76    ///
77    /// # Arguments
78    /// * `mu`          – Neo-Hookean ground-matrix shear modulus \[Pa\].
79    /// * `k1`          – Fiber stiffness parameter \[Pa\].
80    /// * `k2`          – Fiber nonlinearity (dimensionless).
81    /// * `kappa`       – Fiber dispersion parameter ∈ \[0, 1/3\].
82    /// * `fiber_angle` – Mean fiber angle relative to circumferential axis \[rad\].
83    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    /// Compute the Fung strain-energy density W \[J/m³\].
97    ///
98    /// # Formula
99    /// `W = C * (exp(Q) – 1)` where `Q = Σ b_ij * E_i * E_j`.
100    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    /// Compute Fung second Piola-Kirchhoff stress S11 in the 1-direction \[Pa\].
113    ///
114    /// Computed as `∂W/∂E11 = 2C * (b11*E11 + b12*E22 + b13*E33) * exp(Q)`.
115    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    /// Compute HGO fiber pseudo-invariant I4 for one fiber family.
128    ///
129    /// I4 = λ_f² = C : (a ⊗ a) where **a** is the fiber direction unit vector.
130    /// Returns the stretch squared along the fiber.
131    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        // Right Cauchy-Green tensor diagonal: C11 = λ_circ², C22 = λ_axial².
135        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    /// Compute HGO fiber strain-energy density \[J/m³\].
141    ///
142    /// `W_fiber = k1/(2k2) * Σ (exp(k2*(κ*I1 + (1-3κ)*I4 - 1)²) - 1)`
143    /// Only contributes when fibers are under tension (I4 > 1).
144    #[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        // Only fibers under tension contribute.
156        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    /// Compute total HGO strain energy including isotropic neo-Hookean matrix \[J/m³\].
164    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    /// Compute total stress including passive hyperelastic and active components.
179    ///
180    /// Returns Cauchy stress σ_11 in the fiber direction.
181    pub fn total_stress_with_active(&self, strain: &StrainTensor) -> f64 {
182        self.fung_stress_s11(strain) + self.active_stress
183    }
184
185    /// Set the active stress magnitude (for cardiac simulation).
186    pub fn set_active_stress(&mut self, sigma_a: f64) {
187        self.active_stress = sigma_a;
188    }
189}
190
191// ---------------------------------------------------------------------------
192// BoneModel
193// ---------------------------------------------------------------------------
194
195/// Bone tissue constitutive model (cortical + cancellous).
196///
197/// Implements:
198/// - Transversely isotropic elasticity for cortical bone.
199/// - Gibson-Ashby cellular foam model for trabecular bone.
200/// - Kopperdahl-Keaveny mineral-density to modulus correlation.
201/// - Simplified Wolff's law density adaptation.
202#[derive(Debug, Clone)]
203pub struct BoneModel {
204    /// Apparent density ρ \[g/cm³\] (0.1–2.0 for cancellous, ~1.9 for cortical).
205    pub density: f64,
206    /// Mineral volume fraction (ash fraction) \[0..1\].
207    pub mineral_fraction: f64,
208    /// Axial Young's modulus E_axial \[GPa\] (cortical, along osteons).
209    pub e_axial: f64,
210    /// Transverse Young's modulus E_transverse \[GPa\] (cortical).
211    pub e_transverse: f64,
212    /// Shear modulus G \[GPa\] (cortical).
213    pub shear_modulus: f64,
214    /// Trabecular thickness t_b \[mm\].
215    pub trabecular_thickness: f64,
216    /// Trabecular length l_b \[mm\].
217    pub trabecular_length: f64,
218}
219
220impl BoneModel {
221    /// Construct a BoneModel with explicit mechanical properties.
222    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    /// Kopperdahl-Keaveny correlation: density \[g/cm³\] → Young's modulus \[MPa\].
241    ///
242    /// `E = 6850 * ρ^1.49` for apparent density in g/cm³ (trabecular bone).
243    pub fn density_to_modulus_kk(density: f64) -> f64 {
244        6850.0 * density.powf(1.49)
245    }
246
247    /// Gibson-Ashby foam model: relative density → relative Young's modulus.
248    ///
249    /// `E/E_s = C * (ρ/ρ_s)^2` where C ≈ 1.0, ρ_s = solid bone density (~1.9 g/cm³).
250    pub fn gibson_ashby_modulus(&self, solid_modulus_gpa: f64) -> f64 {
251        let rho_s = 1.9_f64; // solid cortical bone density g/cm³
252        let relative_density = (self.density / rho_s).min(1.0);
253        solid_modulus_gpa * relative_density * relative_density
254    }
255
256    /// Compute trabecular bone apparent modulus using Gibson-Ashby.
257    ///
258    /// Returns modulus in GPa.
259    pub fn trabecular_modulus(&self) -> f64 {
260        // Solid bone modulus ~18 GPa.
261        self.gibson_ashby_modulus(18.0)
262    }
263
264    /// Apply Wolff's law density adaptation.
265    ///
266    /// Updates the apparent density based on the daily stress stimulus `σ_ref`
267    /// compared to the homeostatic stimulus `σ_ref`.
268    ///
269    /// `dρ/dt = k * (σ_actual - σ_ref)` with lazy zone ±s.
270    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    /// Compute bone yield strength \[MPa\] from mineral fraction (Currey).
283    ///
284    /// `σ_y = 94.0 * mineral_fraction^3.84`
285    pub fn yield_strength(&self) -> f64 {
286        94.0 * self.mineral_fraction.powf(3.84)
287    }
288}
289
290// ---------------------------------------------------------------------------
291// BloodRheology
292// ---------------------------------------------------------------------------
293
294/// Non-Newtonian blood rheology models.
295///
296/// Implements:
297/// - **Carreau-Yasuda**: shear-thinning viscosity with 5 parameters.
298/// - **Casson**: yield-stress fluid model.
299/// - **Hematocrit-dependent** viscosity (Quemada-type).
300#[derive(Debug, Clone)]
301pub struct BloodRheology {
302    /// Zero-shear viscosity η_0 \[Pa·s\].
303    pub eta_0: f64,
304    /// Infinite-shear viscosity η_∞ \[Pa·s\].
305    pub eta_inf: f64,
306    /// Relaxation time λ \[s\].
307    pub lambda: f64,
308    /// Carreau-Yasuda exponent a (transition parameter).
309    pub carreau_a: f64,
310    /// Power-law index n (shear-thinning for n < 1).
311    pub power_n: f64,
312    /// Casson yield stress τ_y \[Pa\].
313    pub yield_stress: f64,
314    /// Casson plastic viscosity η_c \[Pa·s\].
315    pub casson_eta: f64,
316    /// Hematocrit volume fraction H ∈ \[0, 1\].
317    pub hematocrit: f64,
318}
319
320impl BloodRheology {
321    /// Construct a `BloodRheology` with physiological blood parameters.
322    ///
323    /// Default: Carreau-Yasuda fitted to whole blood at 37 °C.
324    pub fn new_physiological() -> Self {
325        Self {
326            eta_0: 0.056,    // 56 mPa·s
327            eta_inf: 0.0035, // 3.5 mPa·s (plasma viscosity)
328            lambda: 3.313,
329            carreau_a: 2.0,
330            power_n: 0.3568,
331            yield_stress: 0.004, // 4 mPa
332            casson_eta: 0.0035,
333            hematocrit: 0.45,
334        }
335    }
336
337    /// Construct with explicit parameters.
338    #[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    /// Carreau-Yasuda viscosity at shear rate γ̇ \[s⁻¹\].
362    ///
363    /// `η(γ̇) = η_∞ + (η_0 - η_∞) * (1 + (λγ̇)^a)^((n-1)/a)`
364    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    /// Casson model: shear stress from shear rate \[Pa\].
371    ///
372    /// `√τ = √τ_y + √(η_c * |γ̇|)` for `|γ̇| > 0`, else τ = 0.
373    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    /// Casson effective viscosity η_eff = τ/γ̇ \[Pa·s\].
383    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; // Newtonian limit at rest
387        }
388        self.casson_stress(shear_rate) / shear_rate
389    }
390
391    /// Quemada-type hematocrit-dependent viscosity \[Pa·s\].
392    ///
393    /// `η = η_plasma * (1 - H/2)^(-2)` where H = hematocrit.
394    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    /// Check if flow is effectively Newtonian (high shear limit).
400    ///
401    /// Returns true if Carreau viscosity is within 1 % of η_∞.
402    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// ---------------------------------------------------------------------------
409// HydrogelModel
410// ---------------------------------------------------------------------------
411
412/// Flory-Rehner equilibrium swelling model for hydrogels.
413///
414/// Balances elastic (network) and mixing (polymer-solvent) free energies to
415/// find the equilibrium swelling ratio Q.
416#[derive(Debug, Clone)]
417pub struct HydrogelModel {
418    /// Flory-Huggins interaction parameter χ (dimensionless).
419    pub chi: f64,
420    /// Number of chain segments between cross-links N (Kuhn monomers).
421    pub n_chains: f64,
422    /// Reference polymer volume fraction ν_0 (dry state, typically 1.0).
423    pub nu_0: f64,
424    /// Molar volume of solvent \[m³/mol\].
425    pub molar_volume_solvent: f64,
426    /// Shear modulus of dry network G_0 \[Pa\].
427    pub shear_modulus: f64,
428}
429
430impl HydrogelModel {
431    /// Construct a `HydrogelModel` with standard parameters.
432    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, // water
438            shear_modulus,
439        }
440    }
441
442    /// Mixing free energy (Flory-Huggins) chemical potential term \[dimensionless\].
443    ///
444    /// `Δμ_mix / (RT) = ln(1-φ) + φ + χ*φ²`  where φ = polymer volume fraction.
445    pub fn mixing_chemical_potential(&self, phi: f64) -> f64 {
446        (1.0 - phi).ln() + phi + self.chi * phi * phi
447    }
448
449    /// Elastic free energy chemical potential term \[dimensionless\].
450    ///
451    /// `Δμ_el / (RT) = (Vs/Vref) * N^(-1) * (φ/2 - φ^(1/3))`
452    pub fn elastic_chemical_potential(&self, phi: f64) -> f64 {
453        // Simplified Flory-Rehner elastic term.
454        (1.0 / self.n_chains) * (phi / 2.0 - phi.powf(1.0 / 3.0))
455    }
456
457    /// Total chemical potential (mixing + elastic) for equilibrium condition.
458    pub fn total_chemical_potential(&self, phi: f64) -> f64 {
459        self.mixing_chemical_potential(phi) + self.elastic_chemical_potential(phi)
460    }
461
462    /// Find equilibrium swelling ratio Q = V_swollen / V_dry numerically.
463    ///
464    /// Solves `μ_total(φ) = 0` by bisection over φ ∈ \[1e-4, 1-1e-4\].
465    /// Returns volumetric swelling ratio Q = 1/φ_eq.
466    pub fn equilibrium_swelling_ratio(&self) -> f64 {
467        // Bisection: find φ such that μ_total(φ) = 0.
468        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 no sign change, return the boundary with smaller |μ|.
473        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// ---------------------------------------------------------------------------
494// CartilageModel
495// ---------------------------------------------------------------------------
496
497/// Mow biphasic model for articular cartilage.
498///
499/// Treats cartilage as a mixture of:
500/// - **Solid phase**: elastic porous matrix.
501/// - **Fluid phase**: interstitial water (nearly incompressible).
502///
503/// Models creep and stress relaxation under compressive loading.
504#[derive(Debug, Clone)]
505pub struct CartilageModel {
506    /// Aggregate modulus H_A \[MPa\].
507    pub aggregate_modulus: f64,
508    /// Permeability k \[m⁴/(N·s)\].
509    pub permeability: f64,
510    /// Poisson's ratio ν_s of solid matrix (0 to 0.5).
511    pub poisson_solid: f64,
512    /// Thickness of cartilage layer h \[mm\].
513    pub thickness_mm: f64,
514}
515
516impl CartilageModel {
517    /// Construct a `CartilageModel` with standard articular cartilage parameters.
518    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    /// Characteristic diffusion time scale t* = h²/(H_A*k) \[s\].
533    ///
534    /// Controls the rate of fluid exudation under constant load.
535    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    /// Creep deformation at time t under constant stress σ₀ \[normalized\].
541    ///
542    /// Approximated as: `u(t)/u_∞ ≈ 1 - exp(-t / t*)`.
543    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    /// Stress relaxation at time t under constant strain ε₀ \[MPa\].
550    ///
551    /// Approximated as: `σ(t) = σ_0 * exp(-t / t*)`.
552    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    /// Compute Lamé parameter λ \[MPa\] for the solid matrix.
559    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// ---------------------------------------------------------------------------
567// ArterialWall
568// ---------------------------------------------------------------------------
569
570/// Three-layer arterial wall model (intima / media / adventitia).
571///
572/// Each layer has distinct HGO fiber properties and thickness fractions.
573/// Residual stress is captured via the opening angle method.
574#[derive(Debug, Clone)]
575pub struct ArterialWall {
576    /// Intima layer HGO tissue model.
577    pub intima: SoftTissue,
578    /// Media layer HGO tissue model.
579    pub media: SoftTissue,
580    /// Adventitia layer HGO tissue model.
581    pub adventitia: SoftTissue,
582    /// Fractional thickness of each layer \[intima, media, adventitia\] (sums to 1).
583    pub layer_fractions: [f64; 3],
584    /// Opening angle α \[rad\] representing residual stress state.
585    pub opening_angle: f64,
586    /// Inner (lumen) radius in reference configuration r_i \[mm\].
587    pub inner_radius: f64,
588    /// Outer radius in reference configuration r_o \[mm\].
589    pub outer_radius: f64,
590}
591
592impl ArterialWall {
593    /// Construct an `ArterialWall` with typical human aorta parameters.
594    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, // ~31 degrees
604            inner_radius: 9.0,
605            outer_radius: 13.5,
606        }
607    }
608
609    /// Compute residual circumferential strain at the inner wall.
610    ///
611    /// Uses the opening-angle formula: λ_θ = π*r/(α_0*r_0) where α_0 = opening angle.
612    pub fn residual_strain_inner(&self) -> f64 {
613        // Simplified opening-angle residual strain.
614        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    /// Compute wall stress at given circumferential stretch ratio λ_θ.
620    ///
621    /// Returns the wall-averaged circumferential stress \[Pa\].
622    pub fn wall_stress_at_stretch(&self, lambda_circ: f64) -> f64 {
623        let lambda_axial = 1.1; // physiological axial pre-stretch
624        let lambda_radial = 1.0 / (lambda_circ * lambda_axial); // incompressibility
625        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// ---------------------------------------------------------------------------
641// DegradablePolymer
642// ---------------------------------------------------------------------------
643
644/// Hydrolytic degradation model for biodegradable polymers (PLA/PLGA).
645///
646/// Tracks molecular weight decrease and property loss due to ester bond
647/// hydrolysis.
648#[derive(Debug, Clone)]
649pub struct DegradablePolymer {
650    /// Initial number-average molecular weight M_n0 \[g/mol\].
651    pub mw_initial: f64,
652    /// Current number-average molecular weight M_n \[g/mol\].
653    pub mw_current: f64,
654    /// Hydrolysis rate constant k_h \[1/day\] (environment-dependent).
655    pub hydrolysis_rate: f64,
656    /// Autocatalysis factor β (dimensionless, > 0 for bulk erosion).
657    pub autocatalysis: f64,
658    /// Critical molecular weight below which material loses integrity \[g/mol\].
659    pub mw_critical: f64,
660}
661
662impl DegradablePolymer {
663    /// Construct a `DegradablePolymer` representing PLGA (50:50).
664    pub fn new_plga_50_50() -> Self {
665        Self {
666            mw_initial: 100_000.0, // 100 kDa
667            mw_current: 100_000.0,
668            hydrolysis_rate: 0.003, // ~0.3 %/day
669            autocatalysis: 0.5,
670            mw_critical: 5_000.0, // 5 kDa loss of integrity
671        }
672    }
673
674    /// Construct a `DegradablePolymer` with explicit parameters.
675    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    /// Advance degradation by time step Δt \[days\].
691    ///
692    /// Uses: `dM_n/dt = -k_h * (1 + β * (M_n0 - M_n)/M_n0) * M_n`
693    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    /// Run degradation simulation for `days` total time with time step `dt`.
701    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    /// Returns the relative molecular weight retention M_n / M_n0 ∈ \[0, 1\].
709    pub fn mw_retention(&self) -> f64 {
710        self.mw_current / self.mw_initial
711    }
712
713    /// Returns true if the polymer has degraded below the critical molecular weight.
714    pub fn is_failed(&self) -> bool {
715        self.mw_current <= self.mw_critical
716    }
717
718    /// Estimate Young's modulus retention based on M_n (semi-empirical).
719    ///
720    /// `E/E_0 = (M_n / M_n0)^0.5` (Fox-Flory type relation).
721    pub fn modulus_retention(&self) -> f64 {
722        self.mw_retention().sqrt()
723    }
724}
725
726// ---------------------------------------------------------------------------
727// Tests
728// ---------------------------------------------------------------------------
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733
734    const EPS: f64 = 1e-10;
735
736    // -- SoftTissue / Fung model --
737
738    /// Fung strain energy is zero at zero strain.
739    #[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    /// Fung stress is zero at zero strain.
747    #[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    /// Fung model: stress increases with strain (exponential stiffening).
755    #[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    /// Fung strain energy is positive for any nonzero strain.
767    #[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    /// Fung stress increases exponentially (ratio test).
775    #[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    /// Active stress adds to passive Fung stress.
791    #[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    // -- HGO model --
804
805    /// HGO fiber energy is zero when fibers are compressed (I4 ≤ 1).
806    #[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        // Circumferential stretch < 1 → compression.
810        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    /// HGO fiber energy is positive under tension.
818    #[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    /// HGO total energy ≥ neo-Hookean matrix energy (fibers add positive contribution).
829    #[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    /// HGO I4 equals 1 at zero deformation (identity).
843    #[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    // -- BoneModel --
854
855    /// Kopperdahl-Keaveny: modulus increases with density.
856    #[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    /// Gibson-Ashby modulus scales as (ρ/ρ_s)².
865    #[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; // (1.0/0.5)^2 = 4
872        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    /// Wolff's law: density increases when stress exceeds reference.
880    #[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    /// Wolff's law: density decreases under disuse (stress < reference).
893    #[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    /// Yield strength increases with mineral fraction.
906    #[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    // -- BloodRheology --
917
918    /// Carreau-Yasuda: shear thinning (dη/dγ̇ < 0).
919    #[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    /// Carreau-Yasuda: at very high shear rate, approaches η_∞.
931    #[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    /// Casson model: zero shear rate gives zero stress.
941    #[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    /// Casson model: stress increases with shear rate.
952    #[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    /// Hematocrit viscosity > plasma viscosity.
961    #[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    /// Casson: positive shear gives positive stress.
971    #[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    /// Carreau-Yasuda: viscosity is bounded between η_∞ and η_0.
981    #[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    // -- HydrogelModel --
994
995    /// Flory-Rehner mixing chemical potential is negative at low polymer fraction.
996    #[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    /// Equilibrium swelling ratio Q > 1 (gel swells).
1007    #[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    /// More cross-links (shorter chains) → larger magnitude elastic restoring potential.
1018    ///
1019    /// At the same polymer fraction φ < φ_eq, the elastic chemical potential is negative
1020    /// (it drives the solvent OUT of the gel). A tighter network has a more negative
1021    /// elastic potential (larger restoring force), so |mu_tight| > |mu_loose|.
1022    #[test]
1023    fn test_hydrogel_crosslink_reduces_swelling() {
1024        // Compare elastic chemical potential at the same phi.
1025        // Tighter gel (more cross-links): smaller n_chains.
1026        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        // Tight gel has more elastic resistance: |mu_tight| > |mu_loose|.
1032        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    // -- CartilageModel --
1041
1042    /// Characteristic time is positive.
1043    #[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    /// Creep response increases with time.
1054    #[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    /// Stress relaxation decreases with time.
1063    #[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    // -- ArterialWall --
1072
1073    /// Wall stress increases with circumferential stretch.
1074    #[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    /// Layer fractions sum to 1.
1086    #[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    // -- DegradablePolymer --
1097
1098    /// Molecular weight decreases after degradation.
1099    #[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    /// MW retention ∈ \[0, 1\].
1111    #[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    /// Modulus retention ≤ 1.
1123    #[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    /// Degradation is faster with higher hydrolysis rate.
1135    #[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    /// Autocatalysis accelerates degradation beyond simple first-order.
1150    #[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}