1pub const K_BOLTZMANN: f64 = 1.380649e-23;
17
18pub const N_AVOGADRO: f64 = 6.02214076e23;
20
21#[allow(dead_code)]
26pub struct FreelyJointedChain {
27 pub n_segments: f64,
29 pub kuhn_length: f64,
31 pub temperature: f64,
33}
34
35impl FreelyJointedChain {
36 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 pub fn mean_square_end_to_end(&self) -> f64 {
52 self.n_segments * self.kuhn_length * self.kuhn_length
53 }
54
55 pub fn rms_end_to_end(&self) -> f64 {
57 self.mean_square_end_to_end().sqrt()
58 }
59
60 pub fn contour_length(&self) -> f64 {
62 self.n_segments * self.kuhn_length
63 }
64
65 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 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 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#[allow(dead_code)]
95pub struct WormLikeChainPolymer {
96 pub persistence_length: f64,
98 pub contour_length: f64,
100 pub temperature: f64,
102}
103
104impl WormLikeChainPolymer {
105 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 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 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 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#[allow(dead_code)]
169pub struct RubberElasticityNeoHookean {
170 pub shear_modulus: f64,
172 pub bulk_modulus: f64,
174 pub network_density: f64,
176 pub temperature: f64,
178}
179
180impl RubberElasticityNeoHookean {
181 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 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 pub fn lame_lambda(&self) -> f64 {
215 self.bulk_modulus - (2.0 / 3.0) * self.shear_modulus
216 }
217
218 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 pub fn uniaxial_stress(&self, stretch: f64) -> f64 {
236 self.shear_modulus * (stretch * stretch - 1.0 / stretch)
237 }
238
239 pub fn shear_stress(&self, shear_strain: f64) -> f64 {
241 self.shear_modulus * shear_strain
242 }
243}
244
245#[allow(dead_code)]
250pub struct ViscoelasticPolymer {
251 pub elastic_modulus: f64,
253 pub viscosity: f64,
255 pub relaxation_time: f64,
257}
258
259impl ViscoelasticPolymer {
260 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 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 pub fn maxwell_creep_compliance(&self, time: f64) -> f64 {
285 1.0 / self.elastic_modulus + time / self.viscosity
286 }
287
288 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 pub fn kelvin_voigt_stress(&self, strain: f64, strain_rate: f64) -> f64 {
303 self.elastic_modulus * strain + self.viscosity * strain_rate
304 }
305
306 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 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 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#[allow(dead_code)]
329pub struct GlassTransition {
330 pub t_ref: f64,
332 pub c1: f64,
334 pub c2: f64,
336 pub t_g: f64,
338}
339
340impl GlassTransition {
341 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 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 pub fn shift_factor(&self, temperature: f64) -> f64 {
362 10.0_f64.powf(self.log_shift_factor(temperature))
363 }
364
365 pub fn is_rubbery(&self, temperature: f64) -> bool {
367 temperature > self.t_g
368 }
369
370 pub fn relaxation_time(&self, t_ref_tau: f64, temperature: f64) -> f64 {
372 t_ref_tau * self.shift_factor(temperature)
373 }
374}
375
376#[allow(dead_code)]
381pub struct PolymerDegradation {
382 pub initial_mw: f64,
384 pub rate_constant: f64,
386 pub autocatalytic_coeff: f64,
388}
389
390impl PolymerDegradation {
391 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 pub fn molecular_weight(&self, time: f64) -> f64 {
407 self.initial_mw * (-self.rate_constant * time).exp()
408 }
409
410 pub fn degree_of_degradation(&self, time: f64) -> f64 {
412 1.0 - self.molecular_weight(time) / self.initial_mw
413 }
414
415 pub fn half_life(&self) -> f64 {
417 std::f64::consts::LN_2 / self.rate_constant
418 }
419
420 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 pub fn strength_retention(&self, time: f64) -> f64 {
431 (self.molecular_weight(time) / self.initial_mw).sqrt()
432 }
433}
434
435#[allow(dead_code)]
437#[derive(Debug, Clone, PartialEq)]
438pub enum CopolymerMorphology {
439 Disordered,
441 Spheres,
443 Cylinders,
445 Gyroid,
447 Lamellar,
449}
450
451#[allow(dead_code)]
455pub struct DiblockCopolymer {
456 pub degree_of_polymerization: f64,
458 pub volume_fraction_a: f64,
460 pub chi_parameter: f64,
462 pub monomer_volume: f64,
464}
465
466impl DiblockCopolymer {
467 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 pub fn chi_n(&self) -> f64 {
490 self.chi_parameter * self.degree_of_polymerization
491 }
492
493 pub fn critical_chi_n() -> f64 {
495 10.495
496 }
497
498 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 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 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#[allow(dead_code)]
545pub struct EntanglementModel {
546 pub density: f64,
548 pub molecular_weight: f64,
550 pub entanglement_mw: f64,
552 pub temperature: f64,
554 pub friction_coeff: f64,
556 pub segment_length: f64,
558}
559
560impl EntanglementModel {
561 #[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 pub fn plateau_modulus(&self) -> f64 {
591 let r_gas = 8.314; self.density * r_gas * self.temperature / self.entanglement_mw
593 }
594
595 pub fn entanglement_number(&self) -> f64 {
597 self.molecular_weight / self.entanglement_mw
598 }
599
600 pub fn rouse_time(&self) -> f64 {
602 let n = self.molecular_weight / 0.1e-3; 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 pub fn reptation_time(&self) -> f64 {
610 let z = self.entanglement_number();
611 z * z * z * self.rouse_time() / (z * z)
612 }
613
614 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 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 #[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 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 #[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 #[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 #[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 #[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); assert!(
698 f > 0.0,
699 "Force should be positive at extension, got {:.6e}",
700 f
701 );
702 }
703
704 #[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 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 #[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 #[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 #[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 #[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 #[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); 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 #[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 #[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 #[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 #[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 #[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); 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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
907 fn test_diblock_disordered_below_critical() {
908 let copol = DiblockCopolymer::new(50.0, 0.5, 0.1, 1e-28);
909 assert_eq!(copol.morphology(), CopolymerMorphology::Disordered);
911 }
912
913 #[test]
915 fn test_diblock_lamellar_symmetric() {
916 let copol = DiblockCopolymer::new(100.0, 0.5, 0.105, 1e-28);
917 assert_eq!(copol.morphology(), CopolymerMorphology::Lamellar);
919 }
920
921 #[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 #[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 #[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 #[test]
944 fn test_pdms_plateau_modulus() {
945 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 #[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 #[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 #[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 #[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 #[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 #[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 assert!(ext2 > ext1, "Extension should increase with force");
1028 }
1029
1030 #[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}