Skip to main content

oxiphysics_materials/
polymers_materials.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3//! Polymer material models: rubber elasticity, viscoelasticity, degradation.
4//!
5//! Provides models for:
6//! - Freely Jointed Chain (FJC) polymer model
7//! - Worm-Like Chain (WLC) for semi-flexible polymers
8//! - Neo-Hookean rubber elasticity
9//! - Viscoelastic Maxwell and Kelvin-Voigt models
10//! - Glass transition WLF equation
11//! - Polymer degradation kinetics
12//! - Diblock copolymer microphase separation
13//! - Entanglement and reptation dynamics
14
15/// Boltzmann constant in J/K
16pub const K_BOLTZMANN: f64 = 1.380649e-23;
17
18/// Avogadro's number
19pub const N_AVOGADRO: f64 = 6.02214076e23;
20
21/// Freely Jointed Chain polymer model.
22///
23/// Models a polymer as N rigid segments of length b (Kuhn length),
24/// with free rotation about each joint.
25#[allow(dead_code)]
26pub struct FreelyJointedChain {
27    /// Number of Kuhn segments
28    pub n_segments: f64,
29    /// Kuhn segment length \[m\]
30    pub kuhn_length: f64,
31    /// Temperature \[K\]
32    pub temperature: f64,
33}
34
35impl FreelyJointedChain {
36    /// Create a new Freely Jointed Chain model.
37    ///
38    /// # Arguments
39    /// * `n_segments` - Number of Kuhn segments N
40    /// * `kuhn_length` - Kuhn length b \[m\]
41    /// * `temperature` - Temperature \[K\]
42    pub fn new(n_segments: f64, kuhn_length: f64, temperature: f64) -> Self {
43        Self {
44            n_segments,
45            kuhn_length,
46            temperature,
47        }
48    }
49
50    /// Mean square end-to-end distance: `R²` = N * b².
51    pub fn mean_square_end_to_end(&self) -> f64 {
52        self.n_segments * self.kuhn_length * self.kuhn_length
53    }
54
55    /// RMS end-to-end distance: `R²`^(1/2) = b * sqrt(N).
56    pub fn rms_end_to_end(&self) -> f64 {
57        self.mean_square_end_to_end().sqrt()
58    }
59
60    /// Contour length: L_c = N * b.
61    pub fn contour_length(&self) -> f64 {
62        self.n_segments * self.kuhn_length
63    }
64
65    /// Inverse Langevin function approximation (Padé approximant).
66    ///
67    /// L_inv(x) ≈ x * (3 - x²) / (1 - x²) for |x| < 1.
68    pub fn inverse_langevin(x: f64) -> f64 {
69        let x = x.clamp(-0.9999, 0.9999);
70        x * (3.0 - x * x) / (1.0 - x * x)
71    }
72
73    /// Force-extension relationship: F = (kT/b) * L_inv(R/L_c).
74    ///
75    /// # Arguments
76    /// * `extension` - End-to-end distance R \[m\], must be < contour length
77    pub fn force_extension(&self, extension: f64) -> f64 {
78        let l_c = self.contour_length();
79        let ratio = (extension / l_c).clamp(-0.9999, 0.9999);
80        let kbt = K_BOLTZMANN * self.temperature;
81        (kbt / self.kuhn_length) * Self::inverse_langevin(ratio)
82    }
83
84    /// Entropic spring constant at small extensions: k = 3kT / (N*b²).
85    pub fn spring_constant(&self) -> f64 {
86        3.0 * K_BOLTZMANN * self.temperature
87            / (self.n_segments * self.kuhn_length * self.kuhn_length)
88    }
89}
90
91/// Worm-Like Chain polymer model for semi-flexible polymers.
92///
93/// Uses the Marko-Siggia interpolation formula for force-extension.
94#[allow(dead_code)]
95pub struct WormLikeChainPolymer {
96    /// Persistence length L_p \[m\]
97    pub persistence_length: f64,
98    /// Contour length L_c \[m\]
99    pub contour_length: f64,
100    /// Temperature \[K\]
101    pub temperature: f64,
102}
103
104impl WormLikeChainPolymer {
105    /// Create a new Worm-Like Chain model.
106    ///
107    /// # Arguments
108    /// * `persistence_length` - Persistence length L_p \[m\]
109    /// * `contour_length` - Contour length L_c \[m\]
110    /// * `temperature` - Temperature \[K\]
111    pub fn new(persistence_length: f64, contour_length: f64, temperature: f64) -> Self {
112        Self {
113            persistence_length,
114            contour_length,
115            temperature,
116        }
117    }
118
119    /// Force-extension using Marko-Siggia interpolation:
120    /// F = (kT/L_p) * \[1/(4*(1-x)²) - 1/4 + x\]
121    /// where x = R/L_c.
122    ///
123    /// # Arguments
124    /// * `extension` - End-to-end distance R \[m\], must be < contour length
125    pub fn force_extension(&self, extension: f64) -> f64 {
126        let x = (extension / self.contour_length).clamp(0.0, 0.9999);
127        let kbt = K_BOLTZMANN * self.temperature;
128        (kbt / self.persistence_length) * (0.25 / (1.0 - x).powi(2) - 0.25 + x)
129    }
130
131    /// Extension at a given force (numerical inversion via bisection).
132    ///
133    /// # Arguments
134    /// * `force` - Applied force \[N\]
135    pub fn extension_at_force(&self, force: f64) -> f64 {
136        if force <= 0.0 {
137            return 0.0;
138        }
139        let mut lo = 0.0_f64;
140        let mut hi = 0.9999 * self.contour_length;
141        for _ in 0..60 {
142            let mid = 0.5 * (lo + hi);
143            if self.force_extension(mid) < force {
144                lo = mid;
145            } else {
146                hi = mid;
147            }
148        }
149        0.5 * (lo + hi)
150    }
151
152    /// Mean square end-to-end distance in the WLC model:
153    /// `R²` = 2 * L_p * L_c * \[1 - (L_p/L_c)*(1 - exp(-L_c/L_p))\]
154    pub fn mean_square_end_to_end(&self) -> f64 {
155        let ratio = self.contour_length / self.persistence_length;
156        2.0 * self.persistence_length
157            * self.contour_length
158            * (1.0 - (1.0 / ratio) * (1.0 - (-ratio).exp()))
159    }
160}
161
162/// Neo-Hookean rubber elasticity model.
163///
164/// Strain energy density: W = (μ/2)*(I_1 - 3) - μ*ln(J) + (λ/2)*ln²(J)
165/// where I_1 is the first invariant of the left Cauchy-Green tensor,
166/// J is the volumetric deformation, μ is the shear modulus,
167/// λ is the Lamé first parameter.
168#[allow(dead_code)]
169pub struct RubberElasticityNeoHookean {
170    /// Shear modulus μ = nkT \[Pa\]
171    pub shear_modulus: f64,
172    /// Bulk modulus K \[Pa\]
173    pub bulk_modulus: f64,
174    /// Network chain density n \[chains/m³\]
175    pub network_density: f64,
176    /// Temperature \[K\]
177    pub temperature: f64,
178}
179
180impl RubberElasticityNeoHookean {
181    /// Create Neo-Hookean rubber model.
182    ///
183    /// # Arguments
184    /// * `network_density` - Number of network chains per unit volume \[1/m³\]
185    /// * `bulk_modulus` - Bulk modulus K \[Pa\]
186    /// * `temperature` - Temperature \[K\]
187    pub fn new(network_density: f64, bulk_modulus: f64, temperature: f64) -> Self {
188        let shear_modulus = network_density * K_BOLTZMANN * temperature;
189        Self {
190            shear_modulus,
191            bulk_modulus,
192            network_density,
193            temperature,
194        }
195    }
196
197    /// Create with explicit shear modulus.
198    ///
199    /// # Arguments
200    /// * `shear_modulus` - Shear modulus μ \[Pa\]
201    /// * `bulk_modulus` - Bulk modulus K \[Pa\]
202    pub fn from_moduli(shear_modulus: f64, bulk_modulus: f64) -> Self {
203        let temperature = 298.15;
204        let network_density = shear_modulus / (K_BOLTZMANN * temperature);
205        Self {
206            shear_modulus,
207            bulk_modulus,
208            network_density,
209            temperature,
210        }
211    }
212
213    /// Lamé first parameter: λ = K - (2/3)*μ.
214    pub fn lame_lambda(&self) -> f64 {
215        self.bulk_modulus - (2.0 / 3.0) * self.shear_modulus
216    }
217
218    /// Strain energy density W = (μ/2)*(I_1 - 3) - μ*ln(J) + (λ/2)*ln²(J).
219    ///
220    /// # Arguments
221    /// * `i1` - First invariant of left Cauchy-Green tensor (= λ₁² + λ₂² + λ₃²)
222    /// * `j` - Determinant of deformation gradient J = λ₁*λ₂*λ₃
223    pub fn strain_energy_density(&self, i1: f64, j: f64) -> f64 {
224        let mu = self.shear_modulus;
225        let lambda = self.lame_lambda();
226        let ln_j = j.ln();
227        (mu / 2.0) * (i1 - 3.0) - mu * ln_j + (lambda / 2.0) * ln_j * ln_j
228    }
229
230    /// Cauchy stress in uniaxial tension: σ = μ*(λ² - 1/λ) / V
231    /// For incompressible rubber (J=1): σ = μ*(λ² - λ^(-1)).
232    ///
233    /// # Arguments
234    /// * `stretch` - Principal stretch ratio λ (= 1 at no deformation)
235    pub fn uniaxial_stress(&self, stretch: f64) -> f64 {
236        self.shear_modulus * (stretch * stretch - 1.0 / stretch)
237    }
238
239    /// Shear stress: τ = μ * γ (for small shear γ).
240    pub fn shear_stress(&self, shear_strain: f64) -> f64 {
241        self.shear_modulus * shear_strain
242    }
243}
244
245/// Viscoelastic polymer model (Maxwell + Kelvin-Voigt).
246///
247/// Maxwell model: spring E in series with dashpot η.
248/// Kelvin-Voigt model: spring E in parallel with dashpot η.
249#[allow(dead_code)]
250pub struct ViscoelasticPolymer {
251    /// Elastic modulus E \[Pa\]
252    pub elastic_modulus: f64,
253    /// Viscosity η \[Pa·s\]
254    pub viscosity: f64,
255    /// Relaxation time τ = η/E \[s\]
256    pub relaxation_time: f64,
257}
258
259impl ViscoelasticPolymer {
260    /// Create viscoelastic polymer model.
261    ///
262    /// # Arguments
263    /// * `elastic_modulus` - Young's modulus E \[Pa\]
264    /// * `viscosity` - Dynamic viscosity η \[Pa·s\]
265    pub fn new(elastic_modulus: f64, viscosity: f64) -> Self {
266        let relaxation_time = viscosity / elastic_modulus;
267        Self {
268            elastic_modulus,
269            viscosity,
270            relaxation_time,
271        }
272    }
273
274    /// Maxwell stress relaxation: σ(t) = E * ε_0 * exp(-t/τ).
275    ///
276    /// # Arguments
277    /// * `initial_strain` - Applied strain ε_0
278    /// * `time` - Time \[s\]
279    pub fn maxwell_stress_relaxation(&self, initial_strain: f64, time: f64) -> f64 {
280        self.elastic_modulus * initial_strain * (-time / self.relaxation_time).exp()
281    }
282
283    /// Maxwell creep compliance: J(t) = (1/E) + t/η.
284    pub fn maxwell_creep_compliance(&self, time: f64) -> f64 {
285        1.0 / self.elastic_modulus + time / self.viscosity
286    }
287
288    /// Kelvin-Voigt creep: ε(t) = (σ/E) * (1 - exp(-t/τ)).
289    ///
290    /// # Arguments
291    /// * `applied_stress` - Applied stress σ \[Pa\]
292    /// * `time` - Time \[s\]
293    pub fn kelvin_voigt_creep(&self, applied_stress: f64, time: f64) -> f64 {
294        (applied_stress / self.elastic_modulus) * (1.0 - (-time / self.relaxation_time).exp())
295    }
296
297    /// Kelvin-Voigt stress: σ = E*ε + η*dε/dt.
298    ///
299    /// # Arguments
300    /// * `strain` - Current strain ε
301    /// * `strain_rate` - Strain rate dε/dt \[1/s\]
302    pub fn kelvin_voigt_stress(&self, strain: f64, strain_rate: f64) -> f64 {
303        self.elastic_modulus * strain + self.viscosity * strain_rate
304    }
305
306    /// Storage modulus E'(ω) = E * ω²τ² / (1 + ω²τ²).
307    pub fn storage_modulus(&self, angular_freq: f64) -> f64 {
308        let wt = angular_freq * self.relaxation_time;
309        self.elastic_modulus * wt * wt / (1.0 + wt * wt)
310    }
311
312    /// Loss modulus E''(ω) = E * ωτ / (1 + ω²τ²).
313    pub fn loss_modulus(&self, angular_freq: f64) -> f64 {
314        let wt = angular_freq * self.relaxation_time;
315        self.elastic_modulus * wt / (1.0 + wt * wt)
316    }
317
318    /// Loss tangent tan(δ) = E''/E'.
319    pub fn loss_tangent(&self, angular_freq: f64) -> f64 {
320        let wt = angular_freq * self.relaxation_time;
321        1.0 / wt
322    }
323}
324
325/// Glass transition temperature model using the WLF equation.
326///
327/// log₁₀(a_T) = -C₁*(T - T_ref) / (C₂ + (T - T_ref))
328#[allow(dead_code)]
329pub struct GlassTransition {
330    /// Reference temperature T_ref \[K\]
331    pub t_ref: f64,
332    /// WLF constant C₁ (dimensionless)
333    pub c1: f64,
334    /// WLF constant C₂ \[K\]
335    pub c2: f64,
336    /// Glass transition temperature T_g \[K\]
337    pub t_g: f64,
338}
339
340impl GlassTransition {
341    /// Create glass transition model with WLF parameters.
342    ///
343    /// Universal constants at T_g: C₁ = 17.44, C₂ = 51.6 K.
344    ///
345    /// # Arguments
346    /// * `t_g` - Glass transition temperature \[K\]
347    /// * `t_ref` - Reference temperature \[K\] (often = T_g)
348    /// * `c1` - WLF C₁ constant (default 17.44)
349    /// * `c2` - WLF C₂ constant \[K\] (default 51.6)
350    pub fn new(t_g: f64, t_ref: f64, c1: f64, c2: f64) -> Self {
351        Self { t_ref, c1, c2, t_g }
352    }
353
354    /// WLF shift factor log₁₀(a_T) = -C₁*(T-T_ref)/(C₂+(T-T_ref)).
355    pub fn log_shift_factor(&self, temperature: f64) -> f64 {
356        let dt = temperature - self.t_ref;
357        -self.c1 * dt / (self.c2 + dt)
358    }
359
360    /// Shift factor a_T = 10^(log₁₀(a_T)).
361    pub fn shift_factor(&self, temperature: f64) -> f64 {
362        10.0_f64.powf(self.log_shift_factor(temperature))
363    }
364
365    /// Check if temperature is above glass transition.
366    pub fn is_rubbery(&self, temperature: f64) -> bool {
367        temperature > self.t_g
368    }
369
370    /// Relaxation time at temperature T relative to reference.
371    pub fn relaxation_time(&self, t_ref_tau: f64, temperature: f64) -> f64 {
372        t_ref_tau * self.shift_factor(temperature)
373    }
374}
375
376/// Polymer degradation model (hydrolysis kinetics).
377///
378/// First-order: M_n(t) = M_n0 * exp(-k*t)
379/// Autocatalytic: dM/dt = -k * M * \[H⁺\]
380#[allow(dead_code)]
381pub struct PolymerDegradation {
382    /// Initial number-average molecular weight M_n0 \[g/mol\]
383    pub initial_mw: f64,
384    /// First-order degradation rate constant k \[1/s\]
385    pub rate_constant: f64,
386    /// Autocatalytic coefficient α (dimensionless)
387    pub autocatalytic_coeff: f64,
388}
389
390impl PolymerDegradation {
391    /// Create polymer degradation model.
392    ///
393    /// # Arguments
394    /// * `initial_mw` - Initial molecular weight M_n0 \[g/mol\]
395    /// * `rate_constant` - Degradation rate k \[1/s\]
396    /// * `autocatalytic_coeff` - Autocatalytic coefficient α
397    pub fn new(initial_mw: f64, rate_constant: f64, autocatalytic_coeff: f64) -> Self {
398        Self {
399            initial_mw,
400            rate_constant,
401            autocatalytic_coeff,
402        }
403    }
404
405    /// First-order hydrolysis: M_n(t) = M_n0 * exp(-k*t).
406    pub fn molecular_weight(&self, time: f64) -> f64 {
407        self.initial_mw * (-self.rate_constant * time).exp()
408    }
409
410    /// Degree of degradation: α = 1 - M_n(t)/M_n0.
411    pub fn degree_of_degradation(&self, time: f64) -> f64 {
412        1.0 - self.molecular_weight(time) / self.initial_mw
413    }
414
415    /// Half-life: t_{1/2} = ln(2) / k.
416    pub fn half_life(&self) -> f64 {
417        std::f64::consts::LN_2 / self.rate_constant
418    }
419
420    /// Autocatalytic degradation rate: dM/dt = -k * M * (1 + α * (1 - M/M0)).
421    ///
422    /// # Arguments
423    /// * `current_mw` - Current molecular weight \[g/mol\]
424    pub fn autocatalytic_rate(&self, current_mw: f64) -> f64 {
425        let degradation = 1.0 - current_mw / self.initial_mw;
426        -self.rate_constant * current_mw * (1.0 + self.autocatalytic_coeff * degradation)
427    }
428
429    /// Remaining mechanical strength ratio ~ (M_n/M_n0)^(1/2).
430    pub fn strength_retention(&self, time: f64) -> f64 {
431        (self.molecular_weight(time) / self.initial_mw).sqrt()
432    }
433}
434
435/// Morphology type in diblock copolymer phase diagram.
436#[allow(dead_code)]
437#[derive(Debug, Clone, PartialEq)]
438pub enum CopolymerMorphology {
439    /// Disordered (mixed) phase
440    Disordered,
441    /// Body-centered cubic spheres
442    Spheres,
443    /// Hexagonally packed cylinders
444    Cylinders,
445    /// Gyroid bicontinuous network
446    Gyroid,
447    /// Lamellar (alternating layers)
448    Lamellar,
449}
450
451/// Diblock copolymer microphase separation model.
452///
453/// Based on Flory-Huggins theory and self-consistent field theory (SCFT).
454#[allow(dead_code)]
455pub struct DiblockCopolymer {
456    /// Degree of polymerization N
457    pub degree_of_polymerization: f64,
458    /// Volume fraction of block A, f_A ∈ (0, 1)
459    pub volume_fraction_a: f64,
460    /// Flory-Huggins interaction parameter χ
461    pub chi_parameter: f64,
462    /// Monomer reference volume \[m³\]
463    pub monomer_volume: f64,
464}
465
466impl DiblockCopolymer {
467    /// Create diblock copolymer model.
468    ///
469    /// # Arguments
470    /// * `degree_of_polymerization` - Total chain length N
471    /// * `volume_fraction_a` - Volume fraction of A block, f_A
472    /// * `chi_parameter` - Flory-Huggins χ parameter
473    /// * `monomer_volume` - Reference volume per monomer \[m³\]
474    pub fn new(
475        degree_of_polymerization: f64,
476        volume_fraction_a: f64,
477        chi_parameter: f64,
478        monomer_volume: f64,
479    ) -> Self {
480        Self {
481            degree_of_polymerization,
482            volume_fraction_a,
483            chi_parameter,
484            monomer_volume,
485        }
486    }
487
488    /// χN segregation parameter.
489    pub fn chi_n(&self) -> f64 {
490        self.chi_parameter * self.degree_of_polymerization
491    }
492
493    /// Mean-field critical χN = 10.495 for symmetric diblock (f_A = 0.5).
494    pub fn critical_chi_n() -> f64 {
495        10.495
496    }
497
498    /// Predict equilibrium morphology based on χN and f_A.
499    ///
500    /// Phase boundaries from SCFT (approximate):
501    /// - Disordered: χN < 10.5
502    /// - Spheres: f_A < 0.15 or f_A > 0.85
503    /// - Cylinders: 0.15 ≤ f_A < 0.30 or 0.70 < f_A ≤ 0.85
504    /// - Gyroid: 0.30 ≤ f_A < 0.35 or 0.65 < f_A ≤ 0.70
505    /// - Lamellar: 0.35 ≤ f_A ≤ 0.65
506    pub fn morphology(&self) -> CopolymerMorphology {
507        if self.chi_n() < 10.5 {
508            return CopolymerMorphology::Disordered;
509        }
510        let f = self.volume_fraction_a;
511        if !(0.15..=0.85).contains(&f) {
512            CopolymerMorphology::Spheres
513        } else if (0.15..0.30).contains(&f) || (f > 0.70 && f <= 0.85) {
514            CopolymerMorphology::Cylinders
515        } else if (0.30..0.35).contains(&f) || (f > 0.65 && f <= 0.70) {
516            CopolymerMorphology::Gyroid
517        } else {
518            CopolymerMorphology::Lamellar
519        }
520    }
521
522    /// Flory-Huggins free energy of mixing per monomer:
523    /// ΔG_mix / (nkT) = (f_A/N_A)*ln(f_A) + (f_B/N_B)*ln(f_B) + χ*f_A*f_B
524    pub fn free_energy_of_mixing(&self) -> f64 {
525        let f_a = self.volume_fraction_a;
526        let f_b = 1.0 - f_a;
527        let n = self.degree_of_polymerization;
528        (f_a / n) * f_a.ln() + (f_b / n) * f_b.ln() + self.chi_parameter * f_a * f_b
529    }
530
531    /// Lamellar period d ~ b*N^(2/3) * (χN)^(1/6) (strong segregation limit).
532    ///
533    /// # Arguments
534    /// * `segment_length` - Statistical segment length b \[m\]
535    pub fn lamellar_period(&self, segment_length: f64) -> f64 {
536        let n = self.degree_of_polymerization;
537        segment_length * n.powf(2.0 / 3.0) * self.chi_n().powf(1.0 / 6.0)
538    }
539}
540
541/// Entanglement and reptation dynamics model.
542///
543/// Describes chain diffusion, plateau modulus, and Rouse-reptation crossover.
544#[allow(dead_code)]
545pub struct EntanglementModel {
546    /// Polymer density ρ \[kg/m³\]
547    pub density: f64,
548    /// Number-average molecular weight M_n \[kg/mol\]
549    pub molecular_weight: f64,
550    /// Entanglement molecular weight M_e \[kg/mol\]
551    pub entanglement_mw: f64,
552    /// Temperature \[K\]
553    pub temperature: f64,
554    /// Monomeric friction coefficient ζ_0 \[N·s/m\]
555    pub friction_coeff: f64,
556    /// Segment length b \[m\]
557    pub segment_length: f64,
558}
559
560impl EntanglementModel {
561    /// Create entanglement model.
562    ///
563    /// # Arguments
564    /// * `density` - Polymer density \[kg/m³\]
565    /// * `molecular_weight` - Polymer M_n \[kg/mol\]
566    /// * `entanglement_mw` - Entanglement M_e \[kg/mol\]
567    /// * `temperature` - Temperature \[K\]
568    /// * `friction_coeff` - Monomeric friction coefficient \[N·s/m\]
569    /// * `segment_length` - Statistical segment length \[m\]
570    #[allow(clippy::too_many_arguments)]
571    pub fn new(
572        density: f64,
573        molecular_weight: f64,
574        entanglement_mw: f64,
575        temperature: f64,
576        friction_coeff: f64,
577        segment_length: f64,
578    ) -> Self {
579        Self {
580            density,
581            molecular_weight,
582            entanglement_mw,
583            temperature,
584            friction_coeff,
585            segment_length,
586        }
587    }
588
589    /// Plateau modulus G_N = ρ*R*T / M_e \[Pa\].
590    pub fn plateau_modulus(&self) -> f64 {
591        let r_gas = 8.314; // J/(mol·K)
592        self.density * r_gas * self.temperature / self.entanglement_mw
593    }
594
595    /// Number of entanglements per chain Z = M_n / M_e.
596    pub fn entanglement_number(&self) -> f64 {
597        self.molecular_weight / self.entanglement_mw
598    }
599
600    /// Rouse relaxation time: τ_R = ζ_0 * N² * b² / (6π² * kT).
601    pub fn rouse_time(&self) -> f64 {
602        let n = self.molecular_weight / 0.1e-3; // approximate monomer count
603        self.friction_coeff * n * n * self.segment_length * self.segment_length
604            / (6.0 * std::f64::consts::PI.powi(2) * K_BOLTZMANN * self.temperature)
605    }
606
607    /// Reptation (terminal) time: τ_d = 3 * Z³ * τ_e
608    /// (simplified: τ_d ~ Z³ * τ_R).
609    pub fn reptation_time(&self) -> f64 {
610        let z = self.entanglement_number();
611        z * z * z * self.rouse_time() / (z * z)
612    }
613
614    /// Self-diffusion coefficient in reptation regime: D ~ kT*M_e / (ζ*N²).
615    pub fn diffusion_coefficient(&self) -> f64 {
616        let n = self.molecular_weight / 0.1e-3;
617        K_BOLTZMANN * self.temperature * self.entanglement_mw
618            / (self.friction_coeff * n * n * 0.1e-3)
619    }
620
621    /// Is the chain entangled? (M_n > 2 * M_e is the common criterion)
622    pub fn is_entangled(&self) -> bool {
623        self.molecular_weight > 2.0 * self.entanglement_mw
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630
631    const EPS: f64 = 1e-9;
632
633    // 1. FJC end-to-end scales as sqrt(N)
634    #[test]
635    fn test_fjc_rms_scales_sqrt_n() {
636        let fjc1 = FreelyJointedChain::new(100.0, 1e-9, 300.0);
637        let fjc2 = FreelyJointedChain::new(400.0, 1e-9, 300.0);
638        let r1 = fjc1.rms_end_to_end();
639        let r2 = fjc2.rms_end_to_end();
640        // r2/r1 should be sqrt(400/100) = 2
641        assert!(
642            (r2 / r1 - 2.0).abs() < 1e-10,
643            "RMS end-to-end should scale as sqrt(N): ratio={:.6}",
644            r2 / r1
645        );
646    }
647
648    // 2. FJC contour length = N*b
649    #[test]
650    fn test_fjc_contour_length() {
651        let b = 3e-10;
652        let n = 200.0;
653        let fjc = FreelyJointedChain::new(n, b, 300.0);
654        let l_c = fjc.contour_length();
655        assert!(
656            (l_c - n * b).abs() < EPS,
657            "Contour length should be N*b={:.6e}, got {:.6e}",
658            n * b,
659            l_c
660        );
661    }
662
663    // 3. FJC mean square end-to-end = N*b²
664    #[test]
665    fn test_fjc_mean_square_end_to_end() {
666        let b = 2e-10;
667        let n = 50.0;
668        let fjc = FreelyJointedChain::new(n, b, 300.0);
669        let r2 = fjc.mean_square_end_to_end();
670        let expected = n * b * b;
671        assert!(
672            (r2 - expected).abs() < EPS,
673            "Mean square end-to-end should be {:.6e}, got {:.6e}",
674            expected,
675            r2
676        );
677    }
678
679    // 4. FJC spring constant scales as 1/N
680    #[test]
681    fn test_fjc_spring_constant_scales_inv_n() {
682        let fjc1 = FreelyJointedChain::new(100.0, 1e-9, 300.0);
683        let fjc2 = FreelyJointedChain::new(200.0, 1e-9, 300.0);
684        let k1 = fjc1.spring_constant();
685        let k2 = fjc2.spring_constant();
686        assert!(
687            (k1 / k2 - 2.0).abs() < 1e-10,
688            "Spring constant should scale as 1/N"
689        );
690    }
691
692    // 5. FJC force extension is positive and finite
693    #[test]
694    fn test_fjc_force_extension_positive() {
695        let fjc = FreelyJointedChain::new(100.0, 1e-9, 300.0);
696        let f = fjc.force_extension(50e-9); // half contour length
697        assert!(
698            f > 0.0,
699            "Force should be positive at extension, got {:.6e}",
700            f
701        );
702    }
703
704    // 6. WLC extension approaches L as F → ∞
705    #[test]
706    fn test_wlc_extension_limit() {
707        let lp = 50e-9;
708        let lc = 1e-6;
709        let wlc = WormLikeChainPolymer::new(lp, lc, 300.0);
710        // Very large force (100 pN >> kT/L_p): extension should be > 0.98 * L_c
711        let ext = wlc.extension_at_force(1e-10);
712        assert!(
713            ext / lc > 0.98,
714            "Extension should approach L_c at large force: {:.6}",
715            ext / lc
716        );
717    }
718
719    // 7. WLC force increases with extension
720    #[test]
721    fn test_wlc_force_monotone() {
722        let wlc = WormLikeChainPolymer::new(50e-9, 1e-6, 300.0);
723        let f1 = wlc.force_extension(0.3e-6);
724        let f2 = wlc.force_extension(0.7e-6);
725        assert!(f2 > f1, "WLC force should increase with extension");
726    }
727
728    // 8. WLC mean square end-to-end is positive
729    #[test]
730    fn test_wlc_mean_square_positive() {
731        let wlc = WormLikeChainPolymer::new(50e-9, 1e-6, 300.0);
732        let r2 = wlc.mean_square_end_to_end();
733        assert!(r2 > 0.0, "Mean square end-to-end should be positive");
734    }
735
736    // 9. Neo-Hookean stress proportional to μ
737    #[test]
738    fn test_neohookean_stress_proportional_mu() {
739        let rubber1 = RubberElasticityNeoHookean::from_moduli(1e6, 2e9);
740        let rubber2 = RubberElasticityNeoHookean::from_moduli(2e6, 2e9);
741        let stretch = 1.5;
742        let s1 = rubber1.uniaxial_stress(stretch);
743        let s2 = rubber2.uniaxial_stress(stretch);
744        assert!(
745            (s2 / s1 - 2.0).abs() < 1e-10,
746            "Stress should be proportional to μ: ratio={:.6}",
747            s2 / s1
748        );
749    }
750
751    // 10. Neo-Hookean stress is zero at stretch = 1
752    #[test]
753    fn test_neohookean_zero_stress_at_rest() {
754        let rubber = RubberElasticityNeoHookean::from_moduli(1e6, 2e9);
755        let stress = rubber.uniaxial_stress(1.0);
756        assert!(
757            stress.abs() < 1e-9,
758            "Stress at rest (λ=1) should be zero, got {:.6}",
759            stress
760        );
761    }
762
763    // 11. Neo-Hookean strain energy is zero at identity
764    #[test]
765    fn test_neohookean_zero_energy_at_identity() {
766        let rubber = RubberElasticityNeoHookean::from_moduli(1e6, 2e9);
767        let w = rubber.strain_energy_density(3.0, 1.0); // I1=3 for no deformation
768        assert!(
769            w.abs() < 1e-9,
770            "Strain energy at I1=3,J=1 should be zero, got {:.6}",
771            w
772        );
773    }
774
775    // 12. Maxwell stress relaxes to zero
776    #[test]
777    fn test_maxwell_relaxes_to_zero() {
778        let poly = ViscoelasticPolymer::new(1e6, 1e3);
779        let stress_long = poly.maxwell_stress_relaxation(0.01, 100.0 * poly.relaxation_time);
780        assert!(
781            stress_long < 1e-30,
782            "Maxwell stress should relax to zero, got {:.6e}",
783            stress_long
784        );
785    }
786
787    // 13. Maxwell relaxation at t=0 equals E*ε₀
788    #[test]
789    fn test_maxwell_initial_stress() {
790        let poly = ViscoelasticPolymer::new(2e6, 5e3);
791        let eps0 = 0.02;
792        let s0 = poly.maxwell_stress_relaxation(eps0, 0.0);
793        assert!(
794            (s0 - poly.elastic_modulus * eps0).abs() < 1e-9,
795            "Initial Maxwell stress should be E*ε₀"
796        );
797    }
798
799    // 14. Kelvin-Voigt creep saturates at σ/E
800    #[test]
801    fn test_kelvin_voigt_creep_saturation() {
802        let poly = ViscoelasticPolymer::new(1e6, 1e4);
803        let sigma = 1e4;
804        let eps_sat = poly.kelvin_voigt_creep(sigma, 100.0 * poly.relaxation_time);
805        let expected = sigma / poly.elastic_modulus;
806        assert!(
807            (eps_sat - expected).abs() / expected < 1e-4,
808            "KV creep should saturate at σ/E={:.6e}, got {:.6e}",
809            expected,
810            eps_sat
811        );
812    }
813
814    // 15. Kelvin-Voigt creep at t=0 is zero
815    #[test]
816    fn test_kelvin_voigt_creep_zero_initial() {
817        let poly = ViscoelasticPolymer::new(1e6, 1e4);
818        let eps0 = poly.kelvin_voigt_creep(1e4, 0.0);
819        assert!(
820            eps0.abs() < EPS,
821            "KV creep at t=0 should be zero, got {:.6e}",
822            eps0
823        );
824    }
825
826    // 16. WLF shift factor at T_ref = 1
827    #[test]
828    fn test_wlf_shift_factor_at_tref() {
829        let gt = GlassTransition::new(200.0, 200.0, 17.44, 51.6);
830        let at = gt.shift_factor(200.0); // T = T_ref
831        assert!(
832            (at - 1.0).abs() < 1e-10,
833            "Shift factor at T_ref should be 1.0, got {:.6}",
834            at
835        );
836    }
837
838    // 17. WLF log shift factor = 0 at T_ref
839    #[test]
840    fn test_wlf_log_shift_zero_at_tref() {
841        let gt = GlassTransition::new(200.0, 200.0, 17.44, 51.6);
842        let log_at = gt.log_shift_factor(200.0);
843        assert!(
844            log_at.abs() < EPS,
845            "Log shift factor at T_ref should be 0, got {:.6}",
846            log_at
847        );
848    }
849
850    // 18. WLF: above T_ref, shift factor < 1 (polymer flows faster)
851    #[test]
852    fn test_wlf_above_tref_shift_less_than_one() {
853        let gt = GlassTransition::new(200.0, 200.0, 17.44, 51.6);
854        let at = gt.shift_factor(250.0);
855        assert!(
856            at < 1.0,
857            "Above T_ref, shift factor should be < 1 (faster relaxation)"
858        );
859    }
860
861    // 19. Polymer degradation first-order kinetics
862    #[test]
863    fn test_degradation_first_order() {
864        let deg = PolymerDegradation::new(100e3, 0.001, 0.0);
865        let mw_half = deg.molecular_weight(deg.half_life());
866        assert!(
867            (mw_half - 50e3).abs() / 50e3 < 1e-10,
868            "Molecular weight at half-life should be M_n0/2"
869        );
870    }
871
872    // 20. Degradation at t=0 gives M_n0
873    #[test]
874    fn test_degradation_initial_mw() {
875        let deg = PolymerDegradation::new(50e3, 0.01, 1.0);
876        let mw0 = deg.molecular_weight(0.0);
877        assert!(
878            (mw0 - 50e3).abs() < EPS,
879            "Molecular weight at t=0 should be M_n0"
880        );
881    }
882
883    // 21. Degradation degree of degradation increases with time
884    #[test]
885    fn test_degradation_degree_increases() {
886        let deg = PolymerDegradation::new(100e3, 0.001, 0.5);
887        let d1 = deg.degree_of_degradation(100.0);
888        let d2 = deg.degree_of_degradation(1000.0);
889        assert!(d2 > d1, "Degree of degradation should increase with time");
890    }
891
892    // 22. Strength retention decreases with degradation
893    #[test]
894    fn test_strength_retention_decreases() {
895        let deg = PolymerDegradation::new(100e3, 0.001, 0.0);
896        let s0 = deg.strength_retention(0.0);
897        let s1 = deg.strength_retention(1000.0);
898        assert!(
899            (s0 - 1.0).abs() < EPS,
900            "Strength retention at t=0 should be 1"
901        );
902        assert!(s1 < s0, "Strength retention should decrease over time");
903    }
904
905    // 23. Diblock: χN < 10.5 → disordered
906    #[test]
907    fn test_diblock_disordered_below_critical() {
908        let copol = DiblockCopolymer::new(50.0, 0.5, 0.1, 1e-28);
909        // χN = 0.1 * 50 = 5 < 10.5
910        assert_eq!(copol.morphology(), CopolymerMorphology::Disordered);
911    }
912
913    // 24. Diblock: χN = 10.5, f=0.5 → lamellar
914    #[test]
915    fn test_diblock_lamellar_symmetric() {
916        let copol = DiblockCopolymer::new(100.0, 0.5, 0.105, 1e-28);
917        // χN = 10.5
918        assert_eq!(copol.morphology(), CopolymerMorphology::Lamellar);
919    }
920
921    // 25. Diblock: χN = 20, f=0.1 → spheres
922    #[test]
923    fn test_diblock_spheres_asymmetric() {
924        let copol = DiblockCopolymer::new(100.0, 0.1, 0.2, 1e-28);
925        assert_eq!(copol.morphology(), CopolymerMorphology::Spheres);
926    }
927
928    // 26. Diblock: χN = 20, f=0.22 → cylinders
929    #[test]
930    fn test_diblock_cylinders() {
931        let copol = DiblockCopolymer::new(100.0, 0.22, 0.2, 1e-28);
932        assert_eq!(copol.morphology(), CopolymerMorphology::Cylinders);
933    }
934
935    // 27. Diblock: χN = 20, f=0.32 → gyroid
936    #[test]
937    fn test_diblock_gyroid() {
938        let copol = DiblockCopolymer::new(100.0, 0.32, 0.2, 1e-28);
939        assert_eq!(copol.morphology(), CopolymerMorphology::Gyroid);
940    }
941
942    // 28. PDMS plateau modulus ~ 0.2 MPa
943    #[test]
944    fn test_pdms_plateau_modulus() {
945        // PDMS: density=970 kg/m³, M_e=12000 g/mol = 12 kg/mol, T=298K
946        // G_N = ρ*R*T/M_e = 970 * 8.314 * 298 / 12 ≈ 200 kPa
947        let model = EntanglementModel::new(970.0, 1.0, 12.0, 298.0, 1e-11, 4.3e-10);
948        let g_n = model.plateau_modulus();
949        let expected = 970.0 * 8.314 * 298.0 / 12.0;
950        assert!(
951            (g_n - expected).abs() / expected < 0.01,
952            "PDMS G_N should be ~{:.0} Pa, got {:.6e}",
953            expected,
954            g_n
955        );
956    }
957
958    // 29. Entanglement: Z = M_n / M_e
959    #[test]
960    fn test_entanglement_number() {
961        let model = EntanglementModel::new(900.0, 0.1, 0.01, 300.0, 1e-11, 5e-10);
962        let z = model.entanglement_number();
963        assert!(
964            (z - 10.0).abs() < EPS,
965            "Z should be M/M_e = 10, got {:.6}",
966            z
967        );
968    }
969
970    // 30. Entangled check: M > 2*M_e
971    #[test]
972    fn test_entanglement_check() {
973        let entangled = EntanglementModel::new(900.0, 0.1, 0.01, 300.0, 1e-11, 5e-10);
974        let unentangled = EntanglementModel::new(900.0, 0.015, 0.01, 300.0, 1e-11, 5e-10);
975        assert!(
976            entangled.is_entangled(),
977            "M=0.1 > 2*M_e=0.02 should be entangled"
978        );
979        assert!(
980            !unentangled.is_entangled(),
981            "M=0.015 < 2*M_e=0.02 should be unentangled"
982        );
983    }
984
985    // 31. Diblock free energy of mixing is finite
986    #[test]
987    fn test_diblock_free_energy_finite() {
988        let copol = DiblockCopolymer::new(100.0, 0.5, 0.2, 1e-28);
989        let dg = copol.free_energy_of_mixing();
990        assert!(dg.is_finite(), "Free energy of mixing should be finite");
991    }
992
993    // 32. Viscoelastic loss tangent at ω*τ = 1 equals 1
994    #[test]
995    fn test_loss_tangent_at_wt1() {
996        let poly = ViscoelasticPolymer::new(1e6, 1e4);
997        let tau = poly.relaxation_time;
998        let tan_d = poly.loss_tangent(1.0 / tau);
999        assert!(
1000            (tan_d - 1.0).abs() < 1e-10,
1001            "Loss tangent at ω*τ=1 should be 1, got {:.6}",
1002            tan_d
1003        );
1004    }
1005
1006    // 33. Maxwell creep compliance increases with time
1007    #[test]
1008    fn test_maxwell_creep_compliance_increases() {
1009        let poly = ViscoelasticPolymer::new(1e6, 1e4);
1010        let j1 = poly.maxwell_creep_compliance(1.0);
1011        let j2 = poly.maxwell_creep_compliance(2.0);
1012        assert!(
1013            j2 > j1,
1014            "Maxwell creep compliance should increase with time"
1015        );
1016    }
1017
1018    // 34. WLC Marko-Siggia at low force gives approximately linear response
1019    #[test]
1020    fn test_wlc_linear_at_low_force() {
1021        let lp = 50e-9;
1022        let lc = 1e-6;
1023        let wlc = WormLikeChainPolymer::new(lp, lc, 300.0);
1024        let ext1 = wlc.extension_at_force(1e-15);
1025        let ext2 = wlc.extension_at_force(2e-15);
1026        // At very low force, extension is small and roughly proportional to force
1027        assert!(ext2 > ext1, "Extension should increase with force");
1028    }
1029
1030    // 35. Plateau modulus scales with temperature
1031    #[test]
1032    fn test_plateau_modulus_scales_temperature() {
1033        let model1 = EntanglementModel::new(970.0, 1.0, 0.012, 300.0, 1e-11, 4.3e-10);
1034        let model2 = EntanglementModel::new(970.0, 1.0, 0.012, 600.0, 1e-11, 4.3e-10);
1035        let g1 = model1.plateau_modulus();
1036        let g2 = model2.plateau_modulus();
1037        assert!(
1038            (g2 / g1 - 2.0).abs() < 1e-10,
1039            "Plateau modulus should double when T doubles: ratio={:.6}",
1040            g2 / g1
1041        );
1042    }
1043}