Skip to main content

oxiphysics_materials/viscoelastic/
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::*;
7/// Kelvin-Voigt chain (series of KV elements).
8///
9/// Creep compliance J(t) = sum_i (1/E_i) * (1 - exp(-t*E_i/eta_i))
10#[derive(Debug, Clone)]
11pub struct KelvinVoigtChain {
12    /// Individual KV elements.
13    pub elements: Vec<KelvinVoigtModel>,
14}
15impl KelvinVoigtChain {
16    /// Create a new KV chain.
17    pub fn new() -> Self {
18        Self {
19            elements: Vec::new(),
20        }
21    }
22}
23impl Default for KelvinVoigtChain {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28impl KelvinVoigtChain {
29    /// Add a Kelvin-Voigt element with elastic modulus `e` and viscosity `eta`.
30    pub fn add_element(&mut self, e: f64, eta: f64) {
31        self.elements.push(KelvinVoigtModel::new(e, eta));
32    }
33    /// Creep compliance J(t) = sum_i J_i(t).
34    pub fn creep_compliance(&self, t: f64) -> f64 {
35        self.elements.iter().map(|kv| kv.creep_compliance(t)).sum()
36    }
37    /// Number of elements in the chain.
38    pub fn n_elements(&self) -> usize {
39        self.elements.len()
40    }
41    /// Retardation times (tau_i = eta_i / E_i) for all elements.
42    pub fn retardation_times(&self) -> Vec<f64> {
43        self.elements
44            .iter()
45            .map(|kv| kv.viscosity / kv.elastic_modulus)
46            .collect()
47    }
48}
49/// Fractional Standard Linear Solid (FSLS).
50///
51/// Replaces the dashpot in the classical SLS with a springpot:
52///
53///   E*(ω) = E∞ + E₁(iωτ)^α / (1 + (iωτ)^α)
54///
55/// where τ = (C/E₁)^(1/α) is the characteristic relaxation time.
56#[derive(Debug, Clone)]
57pub struct FractionalSls {
58    /// Equilibrium modulus E∞ (Pa).
59    pub e_inf: f64,
60    /// Non-equilibrium spring modulus E₁ (Pa).
61    pub e1: f64,
62    /// Springpot element.
63    pub springpot: SpringPot,
64}
65impl FractionalSls {
66    /// Create a new Fractional SLS.
67    pub fn new(e_inf: f64, e1: f64, springpot: SpringPot) -> Self {
68        Self {
69            e_inf,
70            e1,
71            springpot,
72        }
73    }
74    /// Characteristic relaxation time τ = (C / E₁)^(1/α).
75    pub fn relaxation_time(&self) -> f64 {
76        let alpha = self.springpot.alpha;
77        (self.springpot.quasi_property / self.e1).powf(1.0 / alpha)
78    }
79    /// Storage modulus E'(ω).
80    ///
81    /// E'(ω) = E∞ + E₁ (ωτ)^α \[(ωτ)^α + cos(απ/2)\] / \[1 + 2(ωτ)^α cos(απ/2) + (ωτ)^(2α)\]
82    pub fn storage_modulus(&self, omega: f64) -> f64 {
83        let pi = std::f64::consts::PI;
84        let tau = self.relaxation_time();
85        let wt = (omega * tau).powf(self.springpot.alpha);
86        let cos_ap2 = (self.springpot.alpha * pi / 2.0).cos();
87        let denom = 1.0 + 2.0 * wt * cos_ap2 + wt * wt;
88        self.e_inf + self.e1 * wt * (wt + cos_ap2) / denom
89    }
90    /// Loss modulus E''(ω).
91    ///
92    /// E''(ω) = E₁ (ωτ)^α sin(απ/2) / \[1 + 2(ωτ)^α cos(απ/2) + (ωτ)^(2α)\]
93    pub fn loss_modulus(&self, omega: f64) -> f64 {
94        let pi = std::f64::consts::PI;
95        let tau = self.relaxation_time();
96        let wt = (omega * tau).powf(self.springpot.alpha);
97        let cos_ap2 = (self.springpot.alpha * pi / 2.0).cos();
98        let sin_ap2 = (self.springpot.alpha * pi / 2.0).sin();
99        let denom = 1.0 + 2.0 * wt * cos_ap2 + wt * wt;
100        self.e1 * wt * sin_ap2 / denom
101    }
102    /// Loss tangent tan δ = E'' / E'.
103    pub fn loss_tangent(&self, omega: f64) -> f64 {
104        let ep = self.storage_modulus(omega);
105        let epp = self.loss_modulus(omega);
106        if ep.abs() < 1e-30 { 0.0 } else { epp / ep }
107    }
108    /// Glassy (short-time) modulus E₀ = E∞ + E₁.
109    pub fn glassy_modulus(&self) -> f64 {
110        self.e_inf + self.e1
111    }
112}
113/// Prony series representation for relaxation modulus.
114///
115/// E(t) = E_inf + sum_i g_i * exp(-t / tau_i)
116#[derive(Debug, Clone)]
117pub struct PronySeries {
118    /// Equilibrium modulus E_inf.
119    pub e_inf: f64,
120    /// Prony coefficients (g_i, tau_i).
121    pub terms: Vec<(f64, f64)>,
122}
123impl PronySeries {
124    /// Create a new Prony series.
125    pub fn new(e_inf: f64) -> Self {
126        Self {
127            e_inf,
128            terms: Vec::new(),
129        }
130    }
131    /// Add a term (g_i, tau_i).
132    pub fn add_term(&mut self, g: f64, tau: f64) {
133        self.terms.push((g, tau));
134    }
135    /// Relaxation modulus E(t).
136    pub fn relaxation_modulus(&self, t: f64) -> f64 {
137        let sum: f64 = self
138            .terms
139            .iter()
140            .map(|&(g, tau)| g * (-t / tau).exp())
141            .sum();
142        self.e_inf + sum
143    }
144    /// Storage modulus E'(omega).
145    pub fn storage_modulus(&self, omega: f64) -> f64 {
146        let sum: f64 = self
147            .terms
148            .iter()
149            .map(|&(g, tau)| {
150                let wt = omega * tau;
151                g * wt * wt / (1.0 + wt * wt)
152            })
153            .sum();
154        self.e_inf + sum
155    }
156    /// Loss modulus E''(omega).
157    pub fn loss_modulus(&self, omega: f64) -> f64 {
158        self.terms
159            .iter()
160            .map(|&(g, tau)| {
161                let wt = omega * tau;
162                g * wt / (1.0 + wt * wt)
163            })
164            .sum()
165    }
166    /// Convert to a GeneralizedMaxwell model (each term becomes a Maxwell element with weight 1).
167    pub fn to_generalized_maxwell(&self) -> GeneralizedMaxwell {
168        let mut gm = GeneralizedMaxwell::new(self.e_inf);
169        for &(g, tau) in &self.terms {
170            gm.add_element(MaxwellModel::new(g, g * tau), 1.0);
171        }
172        gm
173    }
174    /// Compute the relaxation modulus G(t) from the Prony series.
175    ///
176    /// The shear relaxation modulus (often denoted G(t) for rubbery / soft materials)
177    /// is expressed as a Prony series of decaying exponentials:
178    ///
179    /// G(t) = G_inf + Σᵢ gᵢ · exp(-t / τᵢ)
180    ///
181    /// This is equivalent to `relaxation_modulus` but the naming emphasises the
182    /// *shear* relaxation context used in polymer viscoelasticity.
183    ///
184    /// # Arguments
185    /// * `t` - Time \[s\] (t ≥ 0)
186    ///
187    /// # Returns
188    /// Shear relaxation modulus G(t) \[Pa\]
189    pub fn compute_relaxation_modulus(&self, t: f64) -> f64 {
190        let sum: f64 = self
191            .terms
192            .iter()
193            .map(|&(g, tau)| g * (-t / tau).exp())
194            .sum();
195        self.e_inf + sum
196    }
197}
198/// Scott-Blair (springpot) element: intermediate between spring and dashpot.
199///
200/// The springpot constitutive law uses the fractional Caputo derivative:
201///
202///   σ(t) = C · D^α ε(t)
203///
204/// where α ∈ \[0, 1\] interpolates between a purely elastic spring (α=0)
205/// and a purely viscous dashpot (α=1).
206///
207/// For sinusoidal excitation ε = ε₀ exp(iωt) the complex modulus is:
208///
209///   E*(ω) = C · (iω)^α
210///
211/// giving storage and loss moduli:
212///
213///   E'(ω) = C ω^α cos(α π/2)
214///   E''(ω) = C ω^α sin(α π/2)
215#[derive(Debug, Clone)]
216pub struct SpringPot {
217    /// Quasi-property C (Pa·s^α).
218    pub quasi_property: f64,
219    /// Fractional order α ∈ \[0, 1\].
220    pub alpha: f64,
221}
222impl SpringPot {
223    /// Create a new Scott-Blair springpot.
224    ///
225    /// # Panics
226    /// Panics (debug only) if `alpha` is outside `[0, 1]`.
227    pub fn new(quasi_property: f64, alpha: f64) -> Self {
228        debug_assert!((0.0..=1.0).contains(&alpha), "alpha must be in [0, 1]");
229        Self {
230            quasi_property,
231            alpha,
232        }
233    }
234    /// Storage modulus E'(ω) = C ω^α cos(απ/2).
235    pub fn storage_modulus(&self, omega: f64) -> f64 {
236        let pi = std::f64::consts::PI;
237        self.quasi_property * omega.powf(self.alpha) * (self.alpha * pi / 2.0).cos()
238    }
239    /// Loss modulus E''(ω) = C ω^α sin(απ/2).
240    pub fn loss_modulus(&self, omega: f64) -> f64 {
241        let pi = std::f64::consts::PI;
242        self.quasi_property * omega.powf(self.alpha) * (self.alpha * pi / 2.0).sin()
243    }
244    /// Loss tangent tan δ = E'' / E' = tan(απ/2).
245    ///
246    /// Note: independent of frequency — a hallmark of fractional models.
247    pub fn loss_tangent(&self) -> f64 {
248        let pi = std::f64::consts::PI;
249        (self.alpha * pi / 2.0).tan()
250    }
251    /// Complex modulus magnitude |E*(ω)| = C ω^α.
252    pub fn complex_modulus_magnitude(&self, omega: f64) -> f64 {
253        self.quasi_property * omega.powf(self.alpha)
254    }
255    /// Phase angle δ = α π/2 (radians).
256    pub fn phase_angle(&self) -> f64 {
257        self.alpha * std::f64::consts::FRAC_PI_2
258    }
259}
260/// Generalized Maxwell model (Prony series).
261///
262/// E(t) = E_inf + sum w_i * E_i * exp(-t / tau_i)
263#[derive(Debug, Clone)]
264pub struct GeneralizedMaxwell {
265    /// Long-time (equilibrium) spring modulus E_inf (Pa)
266    pub spring_inf: f64,
267    /// Individual Maxwell elements
268    pub maxwell_elements: Vec<MaxwellModel>,
269    /// Weights for each Maxwell element
270    pub weights: Vec<f64>,
271}
272impl GeneralizedMaxwell {
273    /// Create a new Generalized Maxwell model with equilibrium modulus `spring_inf`.
274    pub fn new(spring_inf: f64) -> Self {
275        Self {
276            spring_inf,
277            maxwell_elements: Vec::new(),
278            weights: Vec::new(),
279        }
280    }
281    /// Add a Maxwell element with the given weight.
282    pub fn add_element(&mut self, model: MaxwellModel, weight: f64) {
283        self.maxwell_elements.push(model);
284        self.weights.push(weight);
285    }
286    /// Relaxation modulus E(t) = E_inf + sum w_i * E_i * exp(-t / tau_i).
287    pub fn relaxation_modulus(&self, t: f64) -> f64 {
288        let sum: f64 = self
289            .maxwell_elements
290            .iter()
291            .zip(self.weights.iter())
292            .map(|(m, &w)| w * m.relaxation_modulus(t))
293            .sum();
294        self.spring_inf + sum
295    }
296    /// Return Prony coefficients as (E_i * w_i, tau_i) pairs.
297    pub fn prony_coefficients(&self) -> Vec<(f64, f64)> {
298        self.maxwell_elements
299            .iter()
300            .zip(self.weights.iter())
301            .map(|(m, &w)| (m.elastic_modulus * w, m.relaxation_time()))
302            .collect()
303    }
304    /// Storage modulus E'(omega) = E_inf + sum g_i * (omega*tau_i)^2 / (1 + (omega*tau_i)^2).
305    pub fn storage_modulus(&self, omega: f64) -> f64 {
306        let sum: f64 = self
307            .maxwell_elements
308            .iter()
309            .zip(self.weights.iter())
310            .map(|(m, &w)| {
311                let tau = m.relaxation_time();
312                let wt = omega * tau;
313                w * m.elastic_modulus * wt * wt / (1.0 + wt * wt)
314            })
315            .sum();
316        self.spring_inf + sum
317    }
318    /// Loss modulus E''(omega) = sum g_i * omega*tau_i / (1 + (omega*tau_i)^2).
319    pub fn loss_modulus(&self, omega: f64) -> f64 {
320        self.maxwell_elements
321            .iter()
322            .zip(self.weights.iter())
323            .map(|(m, &w)| {
324                let tau = m.relaxation_time();
325                let wt = omega * tau;
326                w * m.elastic_modulus * wt / (1.0 + wt * wt)
327            })
328            .sum()
329    }
330    /// Loss tangent tan(delta) = E''(omega) / E'(omega).
331    pub fn loss_tangent(&self, omega: f64) -> f64 {
332        let ep = self.storage_modulus(omega);
333        let epp = self.loss_modulus(omega);
334        if ep.abs() < 1e-30 { 0.0 } else { epp / ep }
335    }
336    /// Compute tan(δ) — the material loss tangent at angular frequency ω.
337    ///
338    /// tan(δ) = G''(ω) / G'(ω)
339    ///
340    /// This is the ratio of energy dissipated per cycle to the peak stored
341    /// energy, and is a direct measure of the material damping capacity.
342    /// Values of tan(δ) >> 1 indicate a predominantly viscous response;
343    /// values << 1 indicate predominantly elastic behaviour.
344    ///
345    /// # Arguments
346    /// * `omega` - Angular frequency ω \[rad/s\]
347    ///
348    /// # Returns
349    /// Loss tangent tan(δ) \[dimensionless, ≥ 0\]
350    pub fn compute_tan_delta(&self, omega: f64) -> f64 {
351        let g_prime = self.storage_modulus(omega);
352        let g_double_prime = self.loss_modulus(omega);
353        if g_prime.abs() < 1e-30 {
354            f64::INFINITY
355        } else {
356            g_double_prime / g_prime
357        }
358    }
359    /// Short-time (glassy) modulus E0 = E_inf + sum w_i * E_i.
360    pub fn glassy_modulus(&self) -> f64 {
361        let sum: f64 = self
362            .maxwell_elements
363            .iter()
364            .zip(self.weights.iter())
365            .map(|(m, &w)| w * m.elastic_modulus)
366            .sum();
367        self.spring_inf + sum
368    }
369    /// Number of Maxwell elements.
370    pub fn n_elements(&self) -> usize {
371        self.maxwell_elements.len()
372    }
373}
374/// WLF (Williams-Landel-Ferry) shift factor.
375///
376/// log10(a_T) = -C1 * (T - T_ref) / (C2 + (T - T_ref))
377#[derive(Debug, Clone)]
378pub struct WlfShift {
379    /// WLF constant C1.
380    pub c1: f64,
381    /// WLF constant C2 (K or degC).
382    pub c2: f64,
383    /// Reference temperature T_ref (K or degC).
384    pub t_ref: f64,
385}
386impl WlfShift {
387    /// Create a new WLF shift factor model.
388    pub fn new(c1: f64, c2: f64, t_ref: f64) -> Self {
389        Self { c1, c2, t_ref }
390    }
391    /// Compute log10(a_T) at temperature T.
392    pub fn log_shift_factor(&self, t: f64) -> f64 {
393        let dt = t - self.t_ref;
394        -self.c1 * dt / (self.c2 + dt)
395    }
396    /// Compute the shift factor a_T at temperature T.
397    pub fn shift_factor(&self, t: f64) -> f64 {
398        10.0_f64.powf(self.log_shift_factor(t))
399    }
400    /// Shift a frequency by the temperature factor: omega_reduced = omega * a_T.
401    pub fn shifted_frequency(&self, omega: f64, t: f64) -> f64 {
402        omega * self.shift_factor(t)
403    }
404}
405/// Maxwell element (spring + dashpot in series) -- new API.
406///
407/// Uses field names `elastic_modulus` and `viscosity` to distinguish from the
408/// legacy [`Maxwell`] struct.
409#[derive(Debug, Clone)]
410pub struct MaxwellModel {
411    /// Young's modulus E (Pa)
412    pub elastic_modulus: f64,
413    /// Dynamic viscosity eta (Pa*s)
414    pub viscosity: f64,
415}
416impl MaxwellModel {
417    /// Create a new Maxwell model with elastic modulus `e` and viscosity `eta`.
418    pub fn new(e: f64, eta: f64) -> Self {
419        Self {
420            elastic_modulus: e,
421            viscosity: eta,
422        }
423    }
424    /// Relaxation time tau = eta / E.
425    pub fn relaxation_time(&self) -> f64 {
426        self.viscosity / self.elastic_modulus
427    }
428    /// Relaxation modulus E(t) = E * exp(-t / tau).
429    pub fn relaxation_modulus(&self, t: f64) -> f64 {
430        let tau = self.relaxation_time();
431        self.elastic_modulus * (-t / tau).exp()
432    }
433    /// Creep compliance J(t) = 1/E + t/eta.
434    pub fn creep_compliance(&self, t: f64) -> f64 {
435        1.0 / self.elastic_modulus + t / self.viscosity
436    }
437    /// Stress update using exact integration of the Maxwell ODE.
438    ///
439    /// Given old stress `sigma_old`, strain rate `epsilon_dot`, and time step `dt`:
440    ///
441    /// sigma_new = sigma_old * exp(-dt/tau) + E * epsilon_dot * tau * (1 - exp(-dt/tau))
442    pub fn stress_update(&self, _epsilon: f64, epsilon_dot: f64, sigma_old: f64, dt: f64) -> f64 {
443        let tau = self.relaxation_time();
444        let exp_term = (-dt / tau).exp();
445        sigma_old * exp_term + self.elastic_modulus * epsilon_dot * tau * (1.0 - exp_term)
446    }
447    /// Storage modulus E'(omega) = E * (omega*tau)^2 / (1 + (omega*tau)^2).
448    pub fn storage_modulus(&self, omega: f64) -> f64 {
449        let tau = self.relaxation_time();
450        let wt = omega * tau;
451        self.elastic_modulus * wt * wt / (1.0 + wt * wt)
452    }
453    /// Loss modulus E''(omega) = E * omega*tau / (1 + (omega*tau)^2).
454    pub fn loss_modulus(&self, omega: f64) -> f64 {
455        let tau = self.relaxation_time();
456        let wt = omega * tau;
457        self.elastic_modulus * wt / (1.0 + wt * wt)
458    }
459    /// Loss tangent tan(delta) = E'' / E' = 1 / (omega * tau).
460    pub fn loss_tangent(&self, omega: f64) -> f64 {
461        let tau = self.relaxation_time();
462        1.0 / (omega * tau)
463    }
464    /// Complex modulus magnitude |E*| = sqrt(E'^2 + E''^2).
465    pub fn complex_modulus_magnitude(&self, omega: f64) -> f64 {
466        let ep = self.storage_modulus(omega);
467        let epp = self.loss_modulus(omega);
468        (ep * ep + epp * epp).sqrt()
469    }
470    /// Compute the dynamic (complex) modulus components (G', G'') for a Maxwell element.
471    ///
472    /// For a single Maxwell element in shear:
473    ///
474    /// G'(ω)  = G · (ωτ)² / (1 + (ωτ)²)   \[storage / elastic part\]
475    /// G''(ω) = G · ωτ    / (1 + (ωτ)²)   \[loss / viscous part\]
476    ///
477    /// # Arguments
478    /// * `omega` - Angular frequency ω \[rad/s\]
479    ///
480    /// # Returns
481    /// `(G_prime, G_double_prime)` — storage and loss moduli \[Pa\]
482    pub fn compute_dynamic_modulus(&self, omega: f64) -> (f64, f64) {
483        let tau = self.relaxation_time();
484        let wt = omega * tau;
485        let denom = 1.0 + wt * wt;
486        let g_prime = self.elastic_modulus * wt * wt / denom;
487        let g_double_prime = self.elastic_modulus * wt / denom;
488        (g_prime, g_double_prime)
489    }
490}
491/// Power-law creep compliance model.
492///
493/// J(t) = J₀ + J₁ t^n
494///
495/// where J₀ = 1/E_g is the instantaneous (glassy) compliance, J₁ is the
496/// creep coefficient, and n ∈ (0, 1] is the power-law exponent.
497///
498/// This form is often used for polymers and metals at elevated temperature.
499#[derive(Debug, Clone)]
500pub struct PowerLawCreep {
501    /// Instantaneous compliance J₀ = 1/E_g (1/Pa).
502    pub j0: f64,
503    /// Power-law coefficient J₁ (1/(Pa·s^n)).
504    pub j1: f64,
505    /// Power-law exponent n ∈ (0, 1].
506    pub n: f64,
507}
508impl PowerLawCreep {
509    /// Create a new power-law creep model.
510    pub fn new(j0: f64, j1: f64, n: f64) -> Self {
511        Self { j0, j1, n }
512    }
513    /// Creep compliance J(t) = J₀ + J₁ t^n.
514    pub fn creep_compliance(&self, t: f64) -> f64 {
515        self.j0 + self.j1 * t.powf(self.n)
516    }
517    /// Creep strain under constant stress σ₀.
518    pub fn creep_strain(&self, sigma0: f64, t: f64) -> f64 {
519        sigma0 * self.creep_compliance(t)
520    }
521    /// Creep rate d J/dt = J₁ n t^(n-1).
522    pub fn creep_rate(&self, t: f64) -> f64 {
523        if t <= 0.0 {
524            return if self.n < 1.0 {
525                f64::INFINITY
526            } else {
527                self.j1 * self.n
528            };
529        }
530        self.j1 * self.n * t.powf(self.n - 1.0)
531    }
532    /// Retardation spectrum approximation (Alfrey approximation).
533    ///
534    /// L(τ) ≈ -J₁ n (τ^n) / (d ln J / d ln t) ≈ J₁ n τ^(n-1) / τ
535    ///      = J₁ n τ^(n-1)  (valid for n << 1)
536    ///
537    /// Returns L at retardation time τ.
538    pub fn retardation_spectrum(&self, tau: f64) -> f64 {
539        if tau <= 0.0 {
540            return 0.0;
541        }
542        self.j1 * self.n * tau.powf(self.n - 1.0)
543    }
544}
545/// Kelvin-Voigt element (spring + dashpot in parallel) -- new API.
546///
547/// Uses field names `elastic_modulus` and `viscosity`.
548#[derive(Debug, Clone)]
549pub struct KelvinVoigtModel {
550    /// Young's modulus E (Pa)
551    pub elastic_modulus: f64,
552    /// Dynamic viscosity eta (Pa*s)
553    pub viscosity: f64,
554}
555impl KelvinVoigtModel {
556    /// Create a new Kelvin-Voigt model with elastic modulus `e` and viscosity `eta`.
557    pub fn new(e: f64, eta: f64) -> Self {
558        Self {
559            elastic_modulus: e,
560            viscosity: eta,
561        }
562    }
563    /// Creep compliance J(t) = (1/E) * (1 - exp(-t*E/eta)).
564    pub fn creep_compliance(&self, t: f64) -> f64 {
565        (1.0 / self.elastic_modulus) * (1.0 - (-t * self.elastic_modulus / self.viscosity).exp())
566    }
567    /// Relaxation modulus for KV model (t > 0).
568    ///
569    /// Strictly E(t) = E + eta*delta(t); for t > 0 this returns E.
570    pub fn relaxation_modulus(&self, _t: f64) -> f64 {
571        self.elastic_modulus
572    }
573    /// Strain update using exact integration of the KV ODE under constant stress.
574    ///
575    /// epsilon_new = epsilon_old * exp(-E*dt/eta) + (sigma/E) * (1 - exp(-E*dt/eta))
576    pub fn strain_update(&self, sigma: f64, epsilon_old: f64, dt: f64) -> f64 {
577        let exp_term = (-self.elastic_modulus * dt / self.viscosity).exp();
578        epsilon_old * exp_term + (sigma / self.elastic_modulus) * (1.0 - exp_term)
579    }
580    /// Storage modulus E'(omega) = E.
581    pub fn storage_modulus(&self, _omega: f64) -> f64 {
582        self.elastic_modulus
583    }
584    /// Loss modulus E''(omega) = eta * omega.
585    pub fn loss_modulus(&self, omega: f64) -> f64 {
586        self.viscosity * omega
587    }
588    /// Loss tangent tan(delta) = eta*omega / E.
589    pub fn loss_tangent(&self, omega: f64) -> f64 {
590        self.viscosity * omega / self.elastic_modulus
591    }
592}
593/// Kelvin-Voigt viscoelastic model.
594///
595/// sigma = E*epsilon + eta*epsilon_dot  (elastic + dashpot in parallel)
596#[derive(Debug, Clone)]
597pub struct KelvinVoigt {
598    /// Young's modulus E (Pa)
599    pub young_modulus: f64,
600    /// Dynamic viscosity eta (Pa*s)
601    pub viscosity: f64,
602}
603impl KelvinVoigt {
604    /// Create a new Kelvin-Voigt model.
605    pub fn new(young_modulus: f64, viscosity: f64) -> Self {
606        Self {
607            young_modulus,
608            viscosity,
609        }
610    }
611    /// Compute stress given strain and strain rate.
612    ///
613    /// sigma = E*epsilon + eta*epsilon_dot
614    pub fn stress(&self, strain: f64, strain_rate: f64) -> f64 {
615        self.young_modulus * strain + self.viscosity * strain_rate
616    }
617    /// Compute creep strain as a function of time under constant stress sigma0.
618    ///
619    /// epsilon(t) = (sigma0/E) * (1 - exp(-E*t/eta))
620    pub fn creep_strain(&self, sigma0: f64, t: f64) -> f64 {
621        let tau = self.relaxation_time();
622        (sigma0 / self.young_modulus) * (1.0 - (-t / tau).exp())
623    }
624    /// Time constant tau = eta/E.
625    pub fn relaxation_time(&self) -> f64 {
626        self.viscosity / self.young_modulus
627    }
628    /// Compute the creep compliance J(t) for the Kelvin-Voigt model.
629    ///
630    /// Under a step stress σ₀ applied at t = 0, the creep compliance is:
631    ///
632    /// J(t) = (1/E) · (1 − exp(−t/τ))
633    ///
634    /// where τ = η/E is the retardation time. The compliance represents the
635    /// strain per unit applied stress at time t.
636    ///
637    /// Note: unlike the Maxwell model, J(t) saturates to 1/E rather than
638    /// growing without bound, reflecting the restoring elastic spring.
639    ///
640    /// # Arguments
641    /// * `t` - Time elapsed since load application \[s\] (t ≥ 0)
642    ///
643    /// # Returns
644    /// Creep compliance J(t) \[1/Pa\]
645    pub fn compute_creep_compliance(&self, t: f64) -> f64 {
646        let tau = self.relaxation_time();
647        (1.0 / self.young_modulus) * (1.0 - (-t / tau).exp())
648    }
649}
650/// Arrhenius shift factor.
651///
652/// ln(a_T) = (E_a / R) * (1/T - 1/T_ref)
653#[derive(Debug, Clone)]
654pub struct ArrheniusShift {
655    /// Activation energy E_a (J/mol).
656    pub activation_energy: f64,
657    /// Gas constant R = 8.314 J/(mol*K).
658    pub r: f64,
659    /// Reference temperature T_ref (K).
660    pub t_ref: f64,
661}
662impl ArrheniusShift {
663    /// Create a new Arrhenius shift model.
664    pub fn new(activation_energy: f64, t_ref: f64) -> Self {
665        Self {
666            activation_energy,
667            r: 8.314,
668            t_ref,
669        }
670    }
671    /// Compute ln(a_T) at temperature T.
672    pub fn ln_shift_factor(&self, t: f64) -> f64 {
673        (self.activation_energy / self.r) * (1.0 / t - 1.0 / self.t_ref)
674    }
675    /// Compute the shift factor a_T at temperature T (must be in Kelvin).
676    pub fn shift_factor(&self, t: f64) -> f64 {
677        self.ln_shift_factor(t).exp()
678    }
679    /// Shift a frequency by the temperature factor.
680    pub fn shifted_frequency(&self, omega: f64, t: f64) -> f64 {
681        omega * self.shift_factor(t)
682    }
683}
684/// Nonlinear Kelvin-Voigt model with power-law dashpot.
685///
686/// Constitutive law: σ = E·ε + η |dε/dt|^(m-1) · dε/dt
687///
688/// For m=1 this reduces to the classical (linear) Kelvin-Voigt model.
689/// For m≠1 the dashpot is nonlinear (strain-rate hardening/softening).
690#[derive(Debug, Clone)]
691pub struct NonlinearKelvinVoigt {
692    /// Elastic stiffness E (Pa).
693    pub young_modulus: f64,
694    /// Viscosity coefficient η (Pa·s^m).
695    pub viscosity: f64,
696    /// Rate sensitivity exponent m (m=1 → linear).
697    pub m: f64,
698}
699impl NonlinearKelvinVoigt {
700    /// Create a new nonlinear KV model.
701    pub fn new(young_modulus: f64, viscosity: f64, m: f64) -> Self {
702        Self {
703            young_modulus,
704            viscosity,
705            m,
706        }
707    }
708    /// Compute stress.
709    ///
710    /// σ = E·ε + η · sign(dε/dt) · |dε/dt|^m
711    pub fn stress(&self, strain: f64, strain_rate: f64) -> f64 {
712        let sign = if strain_rate >= 0.0 { 1.0 } else { -1.0 };
713        self.young_modulus * strain + self.viscosity * sign * strain_rate.abs().powf(self.m)
714    }
715    /// Linear Kelvin-Voigt relaxation time τ = η / E (valid for m=1 only).
716    pub fn linear_relaxation_time(&self) -> f64 {
717        self.viscosity / self.young_modulus
718    }
719    /// Effective dynamic modulus at frequency ω and amplitude ε₀.
720    ///
721    /// For a sinusoidal input ε = ε₀ sin(ωt) the apparent viscous stress
722    /// amplitude is proportional to (ω ε₀)^m so:
723    ///
724    ///   |E*(ω, ε₀)| ≈ sqrt(E² + (η (ω ε₀)^(m-1) ω)²)
725    ///
726    /// This reduces to the linear result for m=1.
727    pub fn effective_dynamic_modulus(&self, omega: f64, epsilon0: f64) -> f64 {
728        let viscous_term = self.viscosity * (omega * epsilon0).powf(self.m - 1.0) * omega;
729        (self.young_modulus.powi(2) + viscous_term.powi(2)).sqrt()
730    }
731}
732/// Burgers model: KV element in series with Maxwell element.
733///
734/// Total strain: ε = ε_spring + ε_KV + ε_dashpot
735///
736/// Under constant stress σ₀:
737///
738///   ε(t) = σ₀/E_M + (σ₀/E_KV)(1 - exp(-E_KV t / η_KV)) + σ₀ t / η_M
739///
740/// where the three terms are instantaneous elastic, delayed elastic
741/// (retarded), and steady-state viscous flow.
742#[derive(Debug, Clone)]
743pub struct Burgers {
744    /// Maxwell spring modulus E_M (Pa).
745    pub e_maxwell: f64,
746    /// Maxwell dashpot viscosity η_M (Pa·s).
747    pub eta_maxwell: f64,
748    /// Kelvin-Voigt spring modulus E_KV (Pa).
749    pub e_kv: f64,
750    /// Kelvin-Voigt dashpot viscosity η_KV (Pa·s).
751    pub eta_kv: f64,
752}
753impl Burgers {
754    /// Create a new Burgers model.
755    pub fn new(e_maxwell: f64, eta_maxwell: f64, e_kv: f64, eta_kv: f64) -> Self {
756        Self {
757            e_maxwell,
758            eta_maxwell,
759            e_kv,
760            eta_kv,
761        }
762    }
763    /// Retardation time of the KV element τ_KV = η_KV / E_KV.
764    pub fn retardation_time(&self) -> f64 {
765        self.eta_kv / self.e_kv
766    }
767    /// Creep compliance J(t) under constant stress (three-term expression).
768    pub fn creep_compliance(&self, t: f64) -> f64 {
769        let tau_kv = self.retardation_time();
770        1.0 / self.e_maxwell
771            + (1.0 / self.e_kv) * (1.0 - (-t / tau_kv).exp())
772            + t / self.eta_maxwell
773    }
774    /// Creep strain under constant stress σ₀.
775    pub fn creep_strain(&self, sigma0: f64, t: f64) -> f64 {
776        sigma0 * self.creep_compliance(t)
777    }
778    /// Instantaneous compliance J(0) = 1 / E_M.
779    pub fn instantaneous_compliance(&self) -> f64 {
780        1.0 / self.e_maxwell
781    }
782    /// Long-time creep rate dε/dt → σ₀ / η_M (Newtonian viscous flow).
783    pub fn steady_state_creep_rate(&self, sigma0: f64) -> f64 {
784        sigma0 / self.eta_maxwell
785    }
786    /// Storage modulus E'(ω) (linearised, valid for small deformations).
787    ///
788    /// For the Burgers model in the frequency domain:
789    ///
790    ///   1/E*(ω) = 1/(iω η_M) + 1/\[E_M + iω η_M_spring\] + 1/\[E_KV + iω η_KV\]
791    ///
792    /// Here we use the simpler series combination of a Maxwell and a KV element.
793    pub fn storage_modulus(&self, omega: f64) -> f64 {
794        let tau_m = self.eta_maxwell / self.e_maxwell;
795        let wt_m = omega * tau_m;
796        let ep_maxwell = self.e_maxwell * wt_m * wt_m / (1.0 + wt_m * wt_m);
797        let ep_kv = self.e_kv;
798        if ep_maxwell < 1.0 {
799            return ep_kv;
800        }
801        1.0 / (1.0 / ep_maxwell + 1.0 / ep_kv)
802    }
803    /// Loss modulus E''(ω).
804    pub fn loss_modulus(&self, omega: f64) -> f64 {
805        let tau_m = self.eta_maxwell / self.e_maxwell;
806        let wt_m = omega * tau_m;
807        let epp_maxwell = self.e_maxwell * wt_m / (1.0 + wt_m * wt_m);
808        let epp_kv = self.eta_kv * omega;
809        epp_maxwell + epp_kv
810    }
811}
812/// Kohlrausch-Williams-Watts (KWW) stretched-exponential relaxation.
813///
814/// Relaxation modulus:
815///
816///   E(t) = (E₀ - E∞) · exp(-(t/τ)^β) + E∞
817///
818/// where β ∈ (0, 1] is the stretching exponent and τ is the characteristic
819/// relaxation time.  β=1 recovers the classical Maxwell form.
820///
821/// This model is widely used for glassy and amorphous materials.
822#[derive(Debug, Clone)]
823pub struct Kww {
824    /// Glassy modulus E₀ (Pa).
825    pub e0: f64,
826    /// Equilibrium modulus E∞ (Pa).
827    pub e_inf: f64,
828    /// Characteristic relaxation time τ (s).
829    pub tau: f64,
830    /// Stretching exponent β ∈ (0, 1].
831    pub beta: f64,
832}
833impl Kww {
834    /// Create a new KWW model.
835    pub fn new(e0: f64, e_inf: f64, tau: f64, beta: f64) -> Self {
836        Self {
837            e0,
838            e_inf,
839            tau,
840            beta,
841        }
842    }
843    /// Relaxation modulus E(t) = (E₀-E∞)·exp(-(t/τ)^β) + E∞.
844    pub fn relaxation_modulus(&self, t: f64) -> f64 {
845        let phi = (-(t / self.tau).powf(self.beta)).exp();
846        (self.e0 - self.e_inf) * phi + self.e_inf
847    }
848    /// Mean relaxation time ⟨τ⟩ = (τ/β) Γ(1/β).
849    ///
850    /// Uses Stirling/Lanczos approximation of Γ.
851    pub fn mean_relaxation_time(&self) -> f64 {
852        self.tau / self.beta * gamma_approx(1.0 / self.beta)
853    }
854    /// Normalised relaxation function φ(t) ∈ \[0, 1\].
855    pub fn phi(&self, t: f64) -> f64 {
856        (-(t / self.tau).powf(self.beta)).exp()
857    }
858    /// Half-relaxation time t_{1/2} where φ(t) = 0.5.
859    ///
860    /// t_{1/2} = τ (ln 2)^(1/β)
861    pub fn half_relaxation_time(&self) -> f64 {
862        self.tau * (2.0_f64.ln()).powf(1.0 / self.beta)
863    }
864}
865/// Maxwell viscoelastic model.
866///
867/// depsilon/dt = (1/E)*dsigma/dt + sigma/eta  (elastic + dashpot in series)
868#[derive(Debug, Clone)]
869pub struct Maxwell {
870    /// Young's modulus E (Pa)
871    pub young_modulus: f64,
872    /// Dynamic viscosity eta (Pa*s)
873    pub viscosity: f64,
874}
875impl Maxwell {
876    /// Create a new Maxwell model.
877    pub fn new(young_modulus: f64, viscosity: f64) -> Self {
878        Self {
879            young_modulus,
880            viscosity,
881        }
882    }
883    /// Time constant tau = eta/E.
884    pub fn relaxation_time(&self) -> f64 {
885        self.viscosity / self.young_modulus
886    }
887    /// Stress relaxation under constant strain epsilon0.
888    ///
889    /// sigma(t) = E * epsilon0 * exp(-t/tau)
890    pub fn stress_relaxation(&self, epsilon0: f64, t: f64) -> f64 {
891        let tau = self.relaxation_time();
892        self.young_modulus * epsilon0 * (-t / tau).exp()
893    }
894}
895/// Standard Linear Solid (Zener model): springs E1, E2 and dashpot eta.
896#[derive(Debug, Clone)]
897pub struct StandardLinearSolid {
898    /// Equilibrium spring modulus (Pa)
899    pub e1: f64,
900    /// Non-equilibrium spring modulus (Pa)
901    pub e2: f64,
902    /// Dashpot viscosity eta (Pa*s)
903    pub eta: f64,
904}
905impl StandardLinearSolid {
906    /// Create a new Standard Linear Solid model.
907    pub fn new(e1: f64, e2: f64, eta: f64) -> Self {
908        Self { e1, e2, eta }
909    }
910    /// Time constant tau = eta/E2.
911    pub fn relaxation_time(&self) -> f64 {
912        self.eta / self.e2
913    }
914    /// Long-time (equilibrium) modulus E_inf = E1.
915    pub fn long_time_modulus(&self) -> f64 {
916        self.e1
917    }
918    /// Short-time (glassy) modulus E0 = E1 + E2.
919    pub fn short_time_modulus(&self) -> f64 {
920        self.e1 + self.e2
921    }
922    /// Relaxation modulus E(t) = E1 + E2 * exp(-t/tau).
923    pub fn relaxation_modulus(&self, t: f64) -> f64 {
924        let tau = self.relaxation_time();
925        self.e1 + self.e2 * (-t / tau).exp()
926    }
927    /// Creep compliance J(t) for the SLS model.
928    ///
929    /// J(t) = 1/E1 - (E2 / (E1*(E1+E2))) * exp(-E1*t / (eta*(1 + E1/E2)))
930    /// Simplified form: J(t) = 1/(E1+E2) + (E2/(E1*(E1+E2))) * (1 - exp(-t/tau_c))
931    /// where tau_c = eta * (E1 + E2) / (E1 * E2)
932    pub fn creep_compliance(&self, t: f64) -> f64 {
933        let e_sum = self.e1 + self.e2;
934        let tau_c = self.eta * e_sum / (self.e1 * self.e2);
935        1.0 / e_sum + (self.e2 / (self.e1 * e_sum)) * (1.0 - (-t / tau_c).exp())
936    }
937}
938/// Time-Temperature Superposition master curve builder.
939///
940/// Given a set of isothermal relaxation modulus curves and a shift-factor
941/// model, this struct accumulates (reduced_time, modulus) pairs to form
942/// a master curve at the reference temperature.
943#[derive(Debug, Clone)]
944pub struct MasterCurveBuilder {
945    /// Reference temperature T_ref (K or °C — must be consistent).
946    pub t_ref: f64,
947    /// Accumulated (log10 t_reduced, modulus) data points.
948    pub data: Vec<(f64, f64)>,
949}
950impl MasterCurveBuilder {
951    /// Create a new empty master-curve builder.
952    pub fn new(t_ref: f64) -> Self {
953        Self {
954            t_ref,
955            data: Vec::new(),
956        }
957    }
958    /// Add an isothermal data point measured at temperature `t_meas`,
959    /// time `t_meas_time` (s), and relaxation modulus `e_t` (Pa).
960    ///
961    /// Uses a WLF shift factor to map to reduced time.
962    pub fn add_point_wlf(&mut self, wlf: &WlfShift, t_meas: f64, t_meas_time: f64, e_t: f64) {
963        let log_at = wlf.log_shift_factor(t_meas);
964        let log_t_reduced = t_meas_time.log10() + log_at;
965        self.data.push((log_t_reduced, e_t));
966    }
967    /// Add an isothermal data point using an Arrhenius shift factor.
968    pub fn add_point_arrhenius(
969        &mut self,
970        arr: &ArrheniusShift,
971        t_meas: f64,
972        t_meas_time: f64,
973        e_t: f64,
974    ) {
975        let ln_at = arr.ln_shift_factor(t_meas);
976        let log_at = ln_at / std::f64::consts::LN_10;
977        let log_t_reduced = t_meas_time.log10() + log_at;
978        self.data.push((log_t_reduced, e_t));
979    }
980    /// Sort the master-curve data by reduced time.
981    pub fn sort(&mut self) {
982        self.data
983            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
984    }
985    /// Linearly interpolate the master-curve modulus at log10(t_reduced) = log_t.
986    pub fn interpolate(&self, log_t: f64) -> Option<f64> {
987        if self.data.len() < 2 {
988            return None;
989        }
990        for w in self.data.windows(2) {
991            let (x0, y0) = w[0];
992            let (x1, y1) = w[1];
993            if log_t >= x0 && log_t <= x1 {
994                let frac = (log_t - x0) / (x1 - x0);
995                return Some(y0 + frac * (y1 - y0));
996            }
997        }
998        None
999    }
1000    /// Number of data points accumulated.
1001    pub fn n_points(&self) -> usize {
1002        self.data.len()
1003    }
1004}
1005/// Continuous retardation spectrum L(τ) modelled as a log-normal distribution.
1006///
1007/// Creep compliance: J(t) = J₀ + ∫ L(τ)/τ · (1 - exp(-t/τ)) d ln τ
1008///
1009/// For the log-normal spectrum:
1010///
1011///   L(τ) = J_lm / (σ √(2π)) · exp(-(ln(τ/τ_c))² / (2σ²))
1012///
1013/// The integral is evaluated numerically over a wide range of τ.
1014#[derive(Debug, Clone)]
1015pub struct LogNormalRetardation {
1016    /// Instantaneous compliance J₀ (1/Pa).
1017    pub j0: f64,
1018    /// Total creep compliance amplitude J_lm (1/Pa).
1019    pub j_lm: f64,
1020    /// Central retardation time τ_c (s).
1021    pub tau_c: f64,
1022    /// Log-width σ (dimensionless, > 0).
1023    pub sigma: f64,
1024}
1025impl LogNormalRetardation {
1026    /// Create a new log-normal retardation model.
1027    pub fn new(j0: f64, j_lm: f64, tau_c: f64, sigma: f64) -> Self {
1028        Self {
1029            j0,
1030            j_lm,
1031            tau_c,
1032            sigma,
1033        }
1034    }
1035    /// L(τ): log-normal retardation spectrum evaluated at τ.
1036    pub fn spectrum(&self, tau: f64) -> f64 {
1037        if tau <= 0.0 {
1038            return 0.0;
1039        }
1040        let x = (tau / self.tau_c).ln() / self.sigma;
1041        let pi = std::f64::consts::PI;
1042        self.j_lm / (self.sigma * (2.0 * pi).sqrt()) * (-0.5 * x * x).exp()
1043    }
1044    /// Creep compliance J(t) evaluated by Gauss-Legendre quadrature (20 pts)
1045    /// over ln τ ∈ \[ln τ_c - 5σ, ln τ_c + 5σ\].
1046    pub fn creep_compliance(&self, t: f64) -> f64 {
1047        let ln_lo = self.tau_c.ln() - 5.0 * self.sigma;
1048        let ln_hi = self.tau_c.ln() + 5.0 * self.sigma;
1049        let n = 200usize;
1050        let d_ln = (ln_hi - ln_lo) / n as f64;
1051        let integral: f64 = (0..n)
1052            .map(|i| {
1053                let ln_tau = ln_lo + (i as f64 + 0.5) * d_ln;
1054                let tau = ln_tau.exp();
1055                let l_tau = self.spectrum(tau);
1056                l_tau * (1.0 - (-t / tau).exp()) * d_ln
1057            })
1058            .sum();
1059        self.j0 + integral
1060    }
1061    /// Storage compliance J'(ω) = J₀ + ∫ L(τ)/(1 + (ωτ)²) d ln τ.
1062    pub fn storage_compliance(&self, omega: f64) -> f64 {
1063        let ln_lo = self.tau_c.ln() - 5.0 * self.sigma;
1064        let ln_hi = self.tau_c.ln() + 5.0 * self.sigma;
1065        let n = 200usize;
1066        let d_ln = (ln_hi - ln_lo) / n as f64;
1067        let integral: f64 = (0..n)
1068            .map(|i| {
1069                let ln_tau = ln_lo + (i as f64 + 0.5) * d_ln;
1070                let tau = ln_tau.exp();
1071                let l_tau = self.spectrum(tau);
1072                let wt2 = (omega * tau).powi(2);
1073                l_tau / (1.0 + wt2) * d_ln
1074            })
1075            .sum();
1076        self.j0 + integral
1077    }
1078    /// Loss compliance J''(ω) = ∫ L(τ) ωτ/(1 + (ωτ)²) d ln τ.
1079    pub fn loss_compliance(&self, omega: f64) -> f64 {
1080        let ln_lo = self.tau_c.ln() - 5.0 * self.sigma;
1081        let ln_hi = self.tau_c.ln() + 5.0 * self.sigma;
1082        let n = 200usize;
1083        let d_ln = (ln_hi - ln_lo) / n as f64;
1084        (0..n)
1085            .map(|i| {
1086                let ln_tau = ln_lo + (i as f64 + 0.5) * d_ln;
1087                let tau = ln_tau.exp();
1088                let l_tau = self.spectrum(tau);
1089                let wt = omega * tau;
1090                l_tau * wt / (1.0 + wt * wt) * d_ln
1091            })
1092            .sum()
1093    }
1094}