1#![allow(dead_code)]
11#![allow(clippy::too_many_arguments)]
12
13use std::f64::consts::PI;
14
15pub fn langevin_function(x: f64) -> f64 {
24 if x.abs() < 1e-6 {
25 x / 3.0
26 } else {
27 x.cosh() / x.sinh() - 1.0 / x
28 }
29}
30
31pub fn inverse_langevin_approx(y: f64) -> f64 {
35 let y2 = y * y;
36 y * (3.0 - y2) / (1.0 - y2)
37}
38
39#[derive(Debug, Clone)]
48pub struct FreelyJointedChain {
49 pub n_segments: usize,
51 pub segment_length: f64,
53}
54
55impl FreelyJointedChain {
56 pub fn new(n: usize, l: f64) -> Self {
58 Self {
59 n_segments: n,
60 segment_length: l,
61 }
62 }
63
64 pub fn contour_length(&self) -> f64 {
66 self.n_segments as f64 * self.segment_length
67 }
68
69 pub fn end_to_end_rms(&self) -> f64 {
71 self.segment_length * (self.n_segments as f64).sqrt()
72 }
73
74 pub fn force_extension(&self, x: f64, kt: f64) -> f64 {
82 let x_clamped = x.clamp(1e-6, 1.0 - 1e-6);
83 (kt / self.segment_length) * inverse_langevin_approx(x_clamped)
84 }
85
86 pub fn langevin(x: f64) -> f64 {
88 langevin_function(x)
89 }
90}
91
92#[derive(Debug, Clone)]
100pub struct WormLikeChain {
101 pub persistence_length: f64,
103 pub contour_length: f64,
105}
106
107impl WormLikeChain {
108 pub fn new(lp: f64, lc: f64) -> Self {
110 Self {
111 persistence_length: lp,
112 contour_length: lc,
113 }
114 }
115
116 pub fn end_to_end_rms(&self) -> f64 {
120 (2.0 * self.persistence_length * self.contour_length).sqrt()
121 }
122
123 pub fn force_extension(&self, extension: f64, kt: f64) -> f64 {
131 let lc = self.contour_length;
132 let x = (extension / lc).clamp(0.0, 1.0 - 1e-6);
133 let factor = kt / self.persistence_length;
134 factor * (0.25 / (1.0 - x).powi(2) - 0.25 + x)
135 }
136}
137
138pub fn rouse_relaxation_time(n: usize, eta: f64, b: f64, kt: f64) -> f64 {
150 let n_f = n as f64;
151 n_f * n_f * b * b * eta / (3.0 * PI * PI * kt)
152}
153
154pub fn zimm_relaxation_time(_n: usize, eta: f64, rg: f64, kt: f64) -> f64 {
162 eta * rg.powi(3) / kt
163}
164
165#[derive(Debug, Clone)]
174pub struct RubberElasticity {
175 pub n_chains: f64,
177 pub kt: f64,
179 pub crosslink_density: f64,
181}
182
183impl RubberElasticity {
184 pub fn new(n_chains: f64, kt: f64, crosslink_density: f64) -> Self {
186 Self {
187 n_chains,
188 kt,
189 crosslink_density,
190 }
191 }
192
193 pub fn shear_modulus(&self) -> f64 {
195 self.n_chains * self.kt
196 }
197
198 pub fn bulk_modulus(&self) -> f64 {
200 let g = self.shear_modulus();
201 2.0 / 3.0 * g + self.crosslink_density * self.kt
202 }
203
204 pub fn strain_energy(&self, lambda: f64) -> f64 {
210 let g = self.shear_modulus();
211 let i1 = lambda * lambda + 2.0 / lambda;
213 g / 2.0 * (i1 - 3.0)
214 }
215
216 pub fn stress_stretch(lambda: f64) -> f64 {
220 lambda - 1.0 / (lambda * lambda)
222 }
223}
224
225pub fn flory_huggins_free_energy(phi: f64, n: usize, chi: f64) -> f64 {
238 let phi = phi.clamp(1e-10, 1.0 - 1e-10);
239 let n_f = n as f64;
240 phi * phi.ln() / n_f + (1.0 - phi) * (1.0 - phi).ln() + chi * phi * (1.0 - phi)
241}
242
243pub fn spinodal_composition(chi: f64, n1: usize, n2: usize) -> (f64, f64) {
253 let n1_f = n1 as f64;
255 let n2_f = n2 as f64;
256 let d2f = |phi: f64| -> f64 {
257 let phi = phi.clamp(1e-10, 1.0 - 1e-10);
258 1.0 / (n1_f * phi) + 1.0 / (n2_f * (1.0 - phi)) - 2.0 * chi
259 };
260 let find_root = |a: f64, b: f64| -> f64 {
262 let mut lo = a;
263 let mut hi = b;
264 for _ in 0..60 {
265 let mid = 0.5 * (lo + hi);
266 if d2f(lo) * d2f(mid) <= 0.0 {
267 hi = mid;
268 } else {
269 lo = mid;
270 }
271 }
272 0.5 * (lo + hi)
273 };
274
275 let chi_crit = chi_critical(n1, n2);
276 if chi <= chi_crit {
277 (0.5, 0.5)
279 } else {
280 let phi_low = find_root(1e-6, 0.5 - 1e-6);
281 let phi_high = find_root(0.5 + 1e-6, 1.0 - 1e-6);
282 (phi_low, phi_high)
283 }
284}
285
286pub fn chi_critical(n1: usize, n2: usize) -> f64 {
290 let term = 1.0 / (n1 as f64).sqrt() + 1.0 / (n2 as f64).sqrt();
291 term * term / 2.0
292}
293
294pub fn radius_of_gyration_ideal(n: usize, b: f64) -> f64 {
304 b * ((n as f64) / 6.0).sqrt()
305}
306
307pub fn polymer_viscosity_mark_houwink(k: f64, alpha: f64, mw: f64) -> f64 {
314 k * mw.powf(alpha)
315}
316
317#[cfg(test)]
322mod tests {
323 use super::*;
324
325 const EPS: f64 = 1e-9;
326
327 #[test]
330 fn test_fjc_contour_length() {
331 let fjc = FreelyJointedChain::new(100, 0.38e-9);
332 let expected = 100.0 * 0.38e-9;
333 assert!((fjc.contour_length() - expected).abs() < EPS);
334 }
335
336 #[test]
337 fn test_fjc_end_to_end_rms_scales_sqrt_n() {
338 let b = 0.38e-9_f64;
340 let fjc100 = FreelyJointedChain::new(100, b);
341 let fjc400 = FreelyJointedChain::new(400, b);
342 let ratio = fjc400.end_to_end_rms() / fjc100.end_to_end_rms();
343 assert!((ratio - 2.0).abs() < 1e-10, "ratio={ratio}");
344 }
345
346 #[test]
347 fn test_fjc_rms_smaller_than_contour() {
348 let fjc = FreelyJointedChain::new(1000, 1e-9);
349 assert!(fjc.end_to_end_rms() < fjc.contour_length());
350 }
351
352 #[test]
353 fn test_fjc_force_extension_positive() {
354 let fjc = FreelyJointedChain::new(100, 1e-9);
355 let f = fjc.force_extension(0.5, 4.1e-21);
356 assert!(f > 0.0, "force should be positive, got {f}");
357 }
358
359 #[test]
360 fn test_fjc_force_extension_increases() {
361 let fjc = FreelyJointedChain::new(100, 1e-9);
362 let kt = 4.1e-21_f64;
363 let f1 = fjc.force_extension(0.3, kt);
364 let f2 = fjc.force_extension(0.8, kt);
365 assert!(f2 > f1, "force should increase with extension");
366 }
367
368 #[test]
371 fn test_langevin_zero() {
372 assert!(langevin_function(0.0).abs() < 1e-6);
374 }
375
376 #[test]
377 fn test_langevin_approaches_one_for_large_x() {
378 let val = langevin_function(100.0);
380 assert!(
381 val > 0.98 && val <= 1.0,
382 "L(100)={val} should be in (0.98, 1]"
383 );
384 }
385
386 #[test]
387 fn test_langevin_positive_for_positive_x() {
388 assert!(langevin_function(1.0) > 0.0);
389 }
390
391 #[test]
392 fn test_langevin_odd_function() {
393 let val_pos = langevin_function(2.0);
394 let val_neg = langevin_function(-2.0);
395 assert!((val_pos + val_neg).abs() < EPS, "L should be odd");
396 }
397
398 #[test]
399 fn test_langevin_less_than_one() {
400 assert!(langevin_function(5.0) < 1.0);
401 }
402
403 #[test]
406 fn test_inverse_langevin_near_zero() {
407 assert!(inverse_langevin_approx(0.0).abs() < EPS);
409 }
410
411 #[test]
412 fn test_inverse_langevin_increases() {
413 let v1 = inverse_langevin_approx(0.3);
414 let v2 = inverse_langevin_approx(0.6);
415 assert!(v2 > v1);
416 }
417
418 #[test]
419 fn test_inverse_langevin_large_for_y_near_one() {
420 let v = inverse_langevin_approx(0.99);
421 assert!(v > 10.0, "inverse Langevin near 1 should be large, got {v}");
422 }
423
424 #[test]
427 fn test_wlc_end_to_end_rms_less_than_contour() {
428 let wlc = WormLikeChain::new(50e-9, 1000e-9);
429 assert!(wlc.end_to_end_rms() < wlc.contour_length);
430 }
431
432 #[test]
433 fn test_wlc_end_to_end_rms_formula() {
434 let lp = 50e-9_f64;
435 let lc = 1000e-9_f64;
436 let wlc = WormLikeChain::new(lp, lc);
437 let expected = (2.0 * lp * lc).sqrt();
438 assert!((wlc.end_to_end_rms() - expected).abs() < EPS);
439 }
440
441 #[test]
442 fn test_wlc_force_extension_positive() {
443 let wlc = WormLikeChain::new(50e-9, 1000e-9);
444 let f = wlc.force_extension(500e-9, 4.1e-21);
445 assert!(f > 0.0, "WLC force should be positive, got {f}");
446 }
447
448 #[test]
449 fn test_wlc_force_extension_increases() {
450 let wlc = WormLikeChain::new(50e-9, 1000e-9);
451 let kt = 4.1e-21_f64;
452 let f1 = wlc.force_extension(200e-9, kt);
453 let f2 = wlc.force_extension(900e-9, kt);
454 assert!(f2 > f1, "WLC force should increase with extension");
455 }
456
457 #[test]
460 fn test_rouse_relaxation_scales_n_squared() {
461 let eta = 1e-3_f64;
462 let b = 1e-9_f64;
463 let kt = 4.1e-21_f64;
464 let t100 = rouse_relaxation_time(100, eta, b, kt);
465 let t200 = rouse_relaxation_time(200, eta, b, kt);
466 let ratio = t200 / t100;
467 assert!((ratio - 4.0).abs() < 1e-9, "ratio={ratio}");
468 }
469
470 #[test]
471 fn test_rouse_relaxation_positive() {
472 let t = rouse_relaxation_time(50, 1e-3, 1e-9, 4.1e-21);
473 assert!(t > 0.0);
474 }
475
476 #[test]
477 fn test_zimm_relaxation_positive() {
478 let t = zimm_relaxation_time(100, 1e-3, 10e-9, 4.1e-21);
479 assert!(t > 0.0);
480 }
481
482 #[test]
483 fn test_zimm_relaxation_scales_rg_cubed() {
484 let eta = 1e-3_f64;
485 let kt = 4.1e-21_f64;
486 let t1 = zimm_relaxation_time(100, eta, 10e-9, kt);
487 let t2 = zimm_relaxation_time(100, eta, 20e-9, kt);
488 let ratio = t2 / t1;
489 assert!((ratio - 8.0).abs() < 1e-9, "ratio={ratio}");
490 }
491
492 #[test]
495 fn test_rubber_shear_modulus_equals_n_kt() {
496 let n = 1e25_f64;
497 let kt = 4.1e-21_f64;
498 let re = RubberElasticity::new(n, kt, 1e22);
499 let g = re.shear_modulus();
500 assert!((g - n * kt).abs() < 1e-6, "G={g}, expected n*kT={}", n * kt);
501 }
502
503 #[test]
504 fn test_rubber_bulk_modulus_positive() {
505 let re = RubberElasticity::new(1e25, 4.1e-21, 1e22);
506 assert!(re.bulk_modulus() > 0.0);
507 }
508
509 #[test]
510 fn test_rubber_strain_energy_zero_at_lambda_one() {
511 let re = RubberElasticity::new(1e25, 4.1e-21, 1e22);
512 let w = re.strain_energy(1.0);
513 assert!(w.abs() < 1e-10, "W at λ=1 should be 0, got {w}");
514 }
515
516 #[test]
517 fn test_rubber_strain_energy_positive_for_stretch() {
518 let re = RubberElasticity::new(1e25, 4.1e-21, 1e22);
519 assert!(re.strain_energy(2.0) > 0.0);
520 }
521
522 #[test]
523 fn test_rubber_stress_stretch_formula() {
524 assert!(RubberElasticity::stress_stretch(1.0).abs() < EPS);
526 let expected = 2.0 - 1.0 / 4.0;
528 assert!((RubberElasticity::stress_stretch(2.0) - expected).abs() < EPS);
529 }
530
531 #[test]
534 fn test_flory_huggins_symmetric_at_half() {
535 let fh_half = flory_huggins_free_energy(0.5, 100, 0.0);
537 let fh_half2 = flory_huggins_free_energy(0.5, 100, 0.0);
538 assert!((fh_half - fh_half2).abs() < EPS);
539 }
540
541 #[test]
542 fn test_flory_huggins_chi_zero_is_entropy_only() {
543 let phi = 0.3_f64;
545 let n = 50_usize;
546 let expected = phi * phi.ln() / n as f64 + (1.0 - phi) * (1.0 - phi).ln();
547 let val = flory_huggins_free_energy(phi, n, 0.0);
548 assert!((val - expected).abs() < EPS);
549 }
550
551 #[test]
552 fn test_flory_huggins_negative_for_mixing_favorable() {
553 assert!(flory_huggins_free_energy(0.3, 1, 0.0) < 0.0);
555 }
556
557 #[test]
560 fn test_chi_critical_decreases_with_n() {
561 let chi_n10 = chi_critical(10, 10);
562 let chi_n100 = chi_critical(100, 100);
563 assert!(chi_n100 < chi_n10, "chi_c should decrease with N");
564 }
565
566 #[test]
567 fn test_chi_critical_symmetric_blend() {
568 let n = 100_usize;
570 let expected = 2.0 / n as f64;
571 let got = chi_critical(n, n);
572 assert!(
573 (got - expected).abs() < 1e-10,
574 "chi_c={got}, expected={expected}"
575 );
576 }
577
578 #[test]
579 fn test_spinodal_below_critical_chi_returns_midpoint() {
580 let chi_c = chi_critical(100, 100);
581 let (lo, hi) = spinodal_composition(chi_c * 0.5, 100, 100);
582 assert!((lo - 0.5).abs() < 1e-6);
583 assert!((hi - 0.5).abs() < 1e-6);
584 }
585
586 #[test]
587 fn test_spinodal_above_critical_chi_gives_two_points() {
588 let chi_c = chi_critical(100, 100);
589 let (lo, hi) = spinodal_composition(chi_c * 2.0, 100, 100);
590 assert!(lo < 0.5, "low spinodal should be < 0.5, got {lo}");
591 assert!(hi > 0.5, "high spinodal should be > 0.5, got {hi}");
592 }
593
594 #[test]
597 fn test_radius_of_gyration_formula() {
598 let n = 100_usize;
599 let b = 1e-9_f64;
600 let expected = b * ((n as f64) / 6.0).sqrt();
601 let got = radius_of_gyration_ideal(n, b);
602 assert!((got - expected).abs() < EPS);
603 }
604
605 #[test]
606 fn test_radius_of_gyration_positive() {
607 assert!(radius_of_gyration_ideal(100, 1e-9) > 0.0);
608 }
609
610 #[test]
611 fn test_radius_of_gyration_scales_sqrt_n() {
612 let b = 1e-9_f64;
613 let r100 = radius_of_gyration_ideal(100, b);
614 let r400 = radius_of_gyration_ideal(400, b);
615 let ratio = r400 / r100;
616 assert!((ratio - 2.0).abs() < 1e-10, "ratio={ratio}");
617 }
618
619 #[test]
622 fn test_mark_houwink_formula() {
623 let k = 1e-4_f64;
624 let alpha = 0.7_f64;
625 let mw = 1e6_f64;
626 let expected = k * mw.powf(alpha);
627 let got = polymer_viscosity_mark_houwink(k, alpha, mw);
628 assert!((got - expected).abs() < EPS);
629 }
630
631 #[test]
632 fn test_mark_houwink_increases_with_mw() {
633 let v1 = polymer_viscosity_mark_houwink(1e-4, 0.7, 1e5);
634 let v2 = polymer_viscosity_mark_houwink(1e-4, 0.7, 1e6);
635 assert!(v2 > v1);
636 }
637
638 #[test]
641 fn test_fjc_langevin_static() {
642 let v = FreelyJointedChain::langevin(1.0);
643 assert!(v > 0.0 && v < 1.0);
644 }
645
646 #[test]
649 fn test_fjc_single_segment() {
650 let fjc = FreelyJointedChain::new(1, 1e-9);
651 assert!((fjc.contour_length() - 1e-9).abs() < EPS);
652 assert!((fjc.end_to_end_rms() - 1e-9).abs() < EPS);
653 }
654
655 #[test]
656 fn test_wlc_rod_limit_lp_much_greater_than_lc() {
657 let lp = 1.0_f64; let lc = 1e-6_f64; let wlc = WormLikeChain::new(lp, lc);
661 let rms = wlc.end_to_end_rms();
662 let expected = (2.0 * lp * lc).sqrt();
663 assert!((rms - expected).abs() < EPS);
664 }
665
666 #[test]
667 fn test_rubber_stress_zero_at_lambda_one_normalised() {
668 assert!(RubberElasticity::stress_stretch(1.0).abs() < EPS);
670 }
671}