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}