1#![allow(dead_code)]
12
13struct SmRng {
19 state: u64,
20}
21
22impl SmRng {
23 fn new(seed: u64) -> Self {
24 Self { state: seed.max(1) }
25 }
26
27 fn next_u64(&mut self) -> u64 {
28 self.state = self
29 .state
30 .wrapping_mul(6_364_136_223_846_793_005)
31 .wrapping_add(1_442_695_040_888_963_407);
32 self.state
33 }
34
35 fn next_f64(&mut self) -> f64 {
36 (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
46pub enum EnsembleKind {
47 Canonical,
49 GrandCanonical,
51 Microcanonical,
53}
54
55#[derive(Debug, Clone)]
60pub struct PartitionFunction {
61 pub kind: EnsembleKind,
63 pub energy_levels: Vec<f64>,
65 pub degeneracies: Vec<f64>,
67 pub temperature: f64,
69 pub chemical_potential: f64,
71 pub particle_numbers: Vec<f64>,
73}
74
75impl PartitionFunction {
76 pub fn canonical(energy_levels: Vec<f64>, degeneracies: Vec<f64>, temperature: f64) -> Self {
83 Self {
84 kind: EnsembleKind::Canonical,
85 energy_levels,
86 degeneracies,
87 temperature,
88 chemical_potential: 0.0,
89 particle_numbers: vec![],
90 }
91 }
92
93 pub fn grand_canonical(
95 energy_levels: Vec<f64>,
96 degeneracies: Vec<f64>,
97 particle_numbers: Vec<f64>,
98 temperature: f64,
99 chemical_potential: f64,
100 ) -> Self {
101 Self {
102 kind: EnsembleKind::GrandCanonical,
103 energy_levels,
104 degeneracies,
105 temperature,
106 chemical_potential,
107 particle_numbers,
108 }
109 }
110
111 pub fn microcanonical(energy_levels: Vec<f64>, degeneracies: Vec<f64>) -> Self {
113 Self {
114 kind: EnsembleKind::Microcanonical,
115 energy_levels,
116 degeneracies,
117 temperature: 0.0,
118 chemical_potential: 0.0,
119 particle_numbers: vec![],
120 }
121 }
122
123 pub fn canonical_z(&self) -> f64 {
127 if self.temperature <= 0.0 {
128 return 0.0;
129 }
130 let beta = 1.0 / self.temperature;
131 self.energy_levels
132 .iter()
133 .zip(self.degeneracies.iter())
134 .map(|(&e, &g)| g * (-beta * e).exp())
135 .sum()
136 }
137
138 pub fn grand_canonical_z(&self) -> f64 {
140 if self.temperature <= 0.0 {
141 return 0.0;
142 }
143 let beta = 1.0 / self.temperature;
144 let mu = self.chemical_potential;
145 let n_iter = self
146 .particle_numbers
147 .iter()
148 .chain(std::iter::repeat(&0.0_f64));
149 self.energy_levels
150 .iter()
151 .zip(self.degeneracies.iter())
152 .zip(n_iter)
153 .map(|((&e, &g), &n)| g * (-beta * (e - mu * n)).exp())
154 .sum()
155 }
156
157 pub fn density_of_states(&self, target_energy: f64, tolerance: f64) -> f64 {
161 self.energy_levels
162 .iter()
163 .zip(self.degeneracies.iter())
164 .filter(|&(&e, _)| (e - target_energy).abs() <= tolerance)
165 .map(|(_, &g)| g)
166 .sum()
167 }
168
169 pub fn free_energy(&self) -> f64 {
171 let z = self.canonical_z();
172 if z <= 0.0 {
173 return f64::INFINITY;
174 }
175 -self.temperature * z.ln()
176 }
177
178 pub fn average_energy(&self) -> f64 {
180 if self.temperature <= 0.0 {
181 return 0.0;
182 }
183 let beta = 1.0 / self.temperature;
184 let z = self.canonical_z();
185 if z <= 0.0 {
186 return 0.0;
187 }
188 let num: f64 = self
189 .energy_levels
190 .iter()
191 .zip(self.degeneracies.iter())
192 .map(|(&e, &g)| g * e * (-beta * e).exp())
193 .sum();
194 num / z
195 }
196
197 pub fn entropy(&self) -> f64 {
199 if self.temperature <= 0.0 {
200 return 0.0;
201 }
202 (self.average_energy() - self.free_energy()) / self.temperature
203 }
204
205 pub fn heat_capacity(&self) -> f64 {
207 if self.temperature <= 0.0 {
208 return 0.0;
209 }
210 let beta = 1.0 / self.temperature;
211 let z = self.canonical_z();
212 if z <= 0.0 {
213 return 0.0;
214 }
215 let e_avg = self.average_energy();
216 let e2_avg: f64 = self
217 .energy_levels
218 .iter()
219 .zip(self.degeneracies.iter())
220 .map(|(&e, &g)| g * e * e * (-beta * e).exp())
221 .sum::<f64>()
222 / z;
223 (e2_avg - e_avg * e_avg) / (self.temperature * self.temperature)
224 }
225}
226
227#[derive(Debug, Clone)]
235pub struct BoltzmannDistribution {
236 pub energies: Vec<f64>,
238 pub degeneracies: Vec<f64>,
240 pub temperature: f64,
242}
243
244impl BoltzmannDistribution {
245 pub fn new(energies: Vec<f64>, degeneracies: Vec<f64>, temperature: f64) -> Self {
247 Self {
248 energies,
249 degeneracies,
250 temperature,
251 }
252 }
253
254 pub fn uniform(energies: Vec<f64>, temperature: f64) -> Self {
256 let n = energies.len();
257 Self::new(energies, vec![1.0; n], temperature)
258 }
259
260 pub fn weight(&self, i: usize) -> f64 {
262 if self.temperature <= 0.0 {
263 return 0.0;
264 }
265 let beta = 1.0 / self.temperature;
266 self.degeneracies[i] * (-beta * self.energies[i]).exp()
267 }
268
269 pub fn partition_function(&self) -> f64 {
271 (0..self.energies.len()).map(|i| self.weight(i)).sum()
272 }
273
274 pub fn probability(&self, i: usize) -> f64 {
276 let z = self.partition_function();
277 if z <= 0.0 {
278 return 0.0;
279 }
280 self.weight(i) / z
281 }
282
283 pub fn probabilities(&self) -> Vec<f64> {
285 let z = self.partition_function();
286 if z <= 0.0 {
287 return vec![0.0; self.energies.len()];
288 }
289 (0..self.energies.len())
290 .map(|i| self.weight(i) / z)
291 .collect()
292 }
293
294 pub fn entropy(&self) -> f64 {
296 self.probabilities()
297 .iter()
298 .filter(|&&p| p > 0.0)
299 .map(|&p| -p * p.ln())
300 .sum()
301 }
302
303 pub fn free_energy(&self) -> f64 {
305 let z = self.partition_function();
306 if z <= 0.0 {
307 return f64::INFINITY;
308 }
309 -self.temperature * z.ln()
310 }
311
312 pub fn mean_energy(&self) -> f64 {
314 let probs = self.probabilities();
315 probs
316 .iter()
317 .zip(self.energies.iter())
318 .map(|(&p, &e)| p * e)
319 .sum()
320 }
321}
322
323#[derive(Debug, Clone)]
332pub struct IsingModel {
333 pub dim: usize,
335 pub lx: usize,
337 pub ly: usize,
339 pub j_coupling: f64,
341 pub field: f64,
343 pub spins: Vec<i8>,
345}
346
347impl IsingModel {
348 pub fn new_1d(n: usize, j_coupling: f64, field: f64) -> Self {
350 Self {
351 dim: 1,
352 lx: n,
353 ly: 1,
354 j_coupling,
355 field,
356 spins: vec![1i8; n],
357 }
358 }
359
360 pub fn new_2d(lx: usize, ly: usize, j_coupling: f64, field: f64) -> Self {
362 Self {
363 dim: 2,
364 lx,
365 ly,
366 j_coupling,
367 field,
368 spins: vec![1i8; lx * ly],
369 }
370 }
371
372 pub fn randomise(&mut self, seed: u64) {
374 let mut rng = SmRng::new(seed);
375 for s in self.spins.iter_mut() {
376 *s = if rng.next_f64() < 0.5 { 1 } else { -1 };
377 }
378 }
379
380 pub fn energy(&self) -> f64 {
382 let mut e = 0.0_f64;
383 let n = self.spins.len();
384 if self.dim == 1 {
385 for i in 0..n {
386 let j = (i + 1) % n;
387 e -= self.j_coupling * (self.spins[i] as f64) * (self.spins[j] as f64);
388 e -= self.field * self.spins[i] as f64;
389 }
390 } else {
391 for row in 0..self.ly {
392 for col in 0..self.lx {
393 let idx = row * self.lx + col;
394 let right = row * self.lx + (col + 1) % self.lx;
395 let down = ((row + 1) % self.ly) * self.lx + col;
396 e -= self.j_coupling
397 * (self.spins[idx] as f64)
398 * (self.spins[right] as f64 + self.spins[down] as f64);
399 e -= self.field * self.spins[idx] as f64;
400 }
401 }
402 }
403 e
404 }
405
406 pub fn magnetisation(&self) -> f64 {
408 let total: i64 = self.spins.iter().map(|&s| s as i64).sum();
409 total as f64 / self.spins.len() as f64
410 }
411
412 fn delta_energy(&self, idx: usize) -> f64 {
414 let s = self.spins[idx] as f64;
415 let neighbour_sum: f64 = if self.dim == 1 {
416 let n = self.spins.len();
417 let left = (idx + n - 1) % n;
418 let right = (idx + 1) % n;
419 self.spins[left] as f64 + self.spins[right] as f64
420 } else {
421 let row = idx / self.lx;
422 let col = idx % self.lx;
423 let left = row * self.lx + (col + self.lx - 1) % self.lx;
424 let right = row * self.lx + (col + 1) % self.lx;
425 let up = ((row + self.ly - 1) % self.ly) * self.lx + col;
426 let down = ((row + 1) % self.ly) * self.lx + col;
427 self.spins[left] as f64
428 + self.spins[right] as f64
429 + self.spins[up] as f64
430 + self.spins[down] as f64
431 };
432 2.0 * s * (self.j_coupling * neighbour_sum + self.field)
433 }
434
435 pub fn metropolis_sweep(&mut self, n_sweeps: usize, temperature: f64, seed: u64) {
439 let mut rng = SmRng::new(seed);
440 let n = self.spins.len();
441 for _ in 0..n_sweeps {
442 for _attempt in 0..n {
443 let idx = (rng.next_u64() as usize) % n;
444 let de = self.delta_energy(idx);
445 if de <= 0.0 || rng.next_f64() < (-de / temperature).exp() {
446 self.spins[idx] = -self.spins[idx];
447 }
448 }
449 }
450 }
451
452 pub fn susceptibility(
455 &mut self,
456 temperature: f64,
457 n_eq: usize,
458 n_samples: usize,
459 seed: u64,
460 ) -> f64 {
461 self.metropolis_sweep(n_eq, temperature, seed);
462 let n = self.spins.len();
463 let mut m_sum = 0.0_f64;
464 let mut m2_sum = 0.0_f64;
465 let mut rng = SmRng::new(seed.wrapping_add(1));
466 for _ in 0..n_samples {
467 for _attempt in 0..n {
469 let idx = (rng.next_u64() as usize) % n;
470 let de = self.delta_energy(idx);
471 if de <= 0.0 || rng.next_f64() < (-de / temperature).exp() {
472 self.spins[idx] = -self.spins[idx];
473 }
474 }
475 let m = self.magnetisation();
476 m_sum += m;
477 m2_sum += m * m;
478 }
479 let m_avg = m_sum / n_samples as f64;
480 let m2_avg = m2_sum / n_samples as f64;
481 (m2_avg - m_avg * m_avg) * n as f64 / temperature
482 }
483
484 pub fn critical_temperature_1d_approx(&self) -> f64 {
486 2.0 * self.j_coupling.abs()
488 }
489
490 pub fn mean_field_tc_2d(&self) -> f64 {
493 4.0 * self.j_coupling
494 }
495
496 pub fn onsager_tc(&self) -> f64 {
499 2.0 * self.j_coupling / (1.0 + 2.0_f64.sqrt()).ln()
500 }
501}
502
503#[derive(Debug, Clone)]
512pub struct PhaseTransition {
513 pub a0: f64,
515 pub tc: f64,
517 pub b: f64,
519 pub c: f64,
521 pub temperature: f64,
523}
524
525impl PhaseTransition {
526 pub fn new(a0: f64, tc: f64, b: f64, c: f64, temperature: f64) -> Self {
528 Self {
529 a0,
530 tc,
531 b,
532 c,
533 temperature,
534 }
535 }
536
537 pub fn a_coeff(&self) -> f64 {
539 self.a0 * (self.temperature - self.tc)
540 }
541
542 pub fn order_parameter(&self) -> f64 {
547 let a = self.a_coeff();
548 if a >= 0.0 {
549 return 0.0;
550 }
551 if self.b > 0.0 {
552 (-a / self.b).sqrt()
553 } else {
554 0.0
555 }
556 }
557
558 pub fn free_energy_density(&self, phi: f64) -> f64 {
560 let a = self.a_coeff();
561 a * phi * phi / 2.0 + self.b * phi.powi(4) / 4.0 + self.c * phi.powi(6) / 6.0
562 }
563
564 pub fn reduced_temperature(&self) -> f64 {
566 if self.tc == 0.0 {
567 return 0.0;
568 }
569 (self.temperature - self.tc) / self.tc
570 }
571
572 pub fn mean_field_beta_exponent(&self) -> f64 {
574 0.5
575 }
576
577 pub fn susceptibility(&self) -> f64 {
579 let a = self.a_coeff().abs();
580 if a < 1e-15 {
581 return f64::INFINITY;
582 }
583 1.0 / (2.0 * a)
584 }
585}
586
587#[derive(Debug, Clone)]
596pub struct FluctuationDissipation {
597 pub temperature: f64,
599}
600
601impl FluctuationDissipation {
602 pub fn new(temperature: f64) -> Self {
604 Self { temperature }
605 }
606
607 pub fn einstein_relation(&self, mobility: f64) -> f64 {
611 mobility * self.temperature
612 }
613
614 pub fn mobility_from_diffusivity(&self, diffusivity: f64) -> f64 {
616 if self.temperature <= 0.0 {
617 return 0.0;
618 }
619 diffusivity / self.temperature
620 }
621
622 pub fn noise_power_spectral_density(&self, resistance: f64) -> f64 {
627 4.0 * self.temperature * resistance
628 }
629
630 pub fn response_function_imaginary(&self, omega: f64, omega0: f64, gamma: f64) -> f64 {
635 let denom = (omega0 * omega0 - omega * omega).powi(2) + gamma * gamma * omega * omega;
636 if denom < 1e-30 {
637 return 0.0;
638 }
639 gamma * omega / denom
640 }
641
642 pub fn response_function_real(&self, omega: f64, omega0: f64, gamma: f64) -> f64 {
646 let denom = (omega0 * omega0 - omega * omega).powi(2) + gamma * gamma * omega * omega;
647 if denom < 1e-30 {
648 return 0.0;
649 }
650 (omega0 * omega0 - omega * omega) / denom
651 }
652
653 pub fn fluctuation_spectrum(&self, omega: f64, omega0: f64, gamma: f64) -> f64 {
657 if omega.abs() < 1e-30 {
658 return 0.0;
659 }
660 2.0 * self.temperature * self.response_function_imaginary(omega, omega0, gamma) / omega
661 }
662}
663
664#[derive(Debug, Clone)]
673pub struct GreenKubo {
674 pub dt: f64,
676 pub temperature: f64,
678 pub volume: f64,
680}
681
682impl GreenKubo {
683 pub fn new(dt: f64, temperature: f64, volume: f64) -> Self {
685 Self {
686 dt,
687 temperature,
688 volume,
689 }
690 }
691
692 pub fn autocorrelation(signal: &[f64]) -> Vec<f64> {
697 let n = signal.len();
698 if n == 0 {
699 return vec![];
700 }
701 let mean: f64 = signal.iter().sum::<f64>() / n as f64;
702 let centered: Vec<f64> = signal.iter().map(|&x| x - mean).collect();
703 let norm: f64 = centered.iter().map(|&x| x * x).sum();
704 if norm < 1e-30 {
705 return vec![0.0; n];
706 }
707 (0..n)
708 .map(|lag| {
709 let count = n - lag;
710 let sum: f64 = (0..count).map(|i| centered[i] * centered[i + lag]).sum();
711 sum / norm
712 })
713 .collect()
714 }
715
716 pub fn diffusivity_from_vacf(&self, vacf: &[f64]) -> f64 {
721 let n = vacf.len();
723 if n == 0 {
724 return 0.0;
725 }
726 let integral: f64 = (0..n - 1)
727 .map(|i| 0.5 * (vacf[i] + vacf[i + 1]) * self.dt)
728 .sum();
729 integral / 3.0
730 }
731
732 pub fn shear_viscosity(&self, stress_acf: &[f64]) -> f64 {
737 if self.temperature <= 0.0 || self.volume <= 0.0 {
738 return 0.0;
739 }
740 let n = stress_acf.len();
741 if n == 0 {
742 return 0.0;
743 }
744 let integral: f64 = (0..n - 1)
745 .map(|i| 0.5 * (stress_acf[i] + stress_acf[i + 1]) * self.dt)
746 .sum();
747 self.volume / self.temperature * integral
748 }
749
750 pub fn electrical_conductivity(&self, current_acf: &[f64]) -> f64 {
755 if self.temperature <= 0.0 || self.volume <= 0.0 {
756 return 0.0;
757 }
758 let n = current_acf.len();
759 if n == 0 {
760 return 0.0;
761 }
762 let integral: f64 = (0..n - 1)
763 .map(|i| 0.5 * (current_acf[i] + current_acf[i + 1]) * self.dt)
764 .sum();
765 self.volume / (3.0 * self.temperature) * integral
766 }
767
768 pub fn correlation_time(acf: &[f64], dt: f64) -> f64 {
770 if acf.is_empty() || acf[0].abs() < 1e-30 {
771 return 0.0;
772 }
773 let integral: f64 = (0..acf.len() - 1)
774 .map(|i| 0.5 * (acf[i] + acf[i + 1]) * dt)
775 .sum();
776 integral / acf[0]
777 }
778}
779
780#[derive(Debug, Clone)]
788pub struct EquationOfState {
789 pub r_gas: f64,
791 pub temperature: f64,
793}
794
795#[derive(Debug, Clone, Copy, PartialEq)]
797pub enum EosModel {
798 VanDerWaals,
800 PengRobinson,
802 RedlichKwong,
804}
805
806impl EquationOfState {
807 pub fn new(r_gas: f64, temperature: f64) -> Self {
811 Self { r_gas, temperature }
812 }
813
814 pub fn van_der_waals_pressure(&self, molar_volume: f64, a: f64, b: f64) -> f64 {
821 if molar_volume <= b {
822 return f64::INFINITY;
823 }
824 self.r_gas * self.temperature / (molar_volume - b) - a / (molar_volume * molar_volume)
825 }
826
827 pub fn vdw_critical_constants(a: f64, b: f64, r_gas: f64) -> (f64, f64, f64) {
831 let tc = 8.0 * a / (27.0 * r_gas * b);
832 let pc = a / (27.0 * b * b);
833 let vc = 3.0 * b;
834 (tc, pc, vc)
835 }
836
837 #[allow(clippy::too_many_arguments)]
843 pub fn peng_robinson_pressure(
844 &self,
845 molar_volume: f64,
846 a_c: f64,
847 b: f64,
848 kappa: f64,
849 tc: f64,
850 ) -> f64 {
851 if molar_volume <= b || tc <= 0.0 {
852 return f64::INFINITY;
853 }
854 let tr = self.temperature / tc;
855 let alpha = (1.0 + kappa * (1.0 - tr.sqrt())).powi(2);
856 let a_t = a_c * alpha;
857 self.r_gas * self.temperature / (molar_volume - b)
858 - a_t / (molar_volume * (molar_volume + b) + b * (molar_volume - b))
859 }
860
861 pub fn redlich_kwong_pressure(&self, molar_volume: f64, a: f64, b: f64) -> f64 {
865 if molar_volume <= b || self.temperature <= 0.0 {
866 return f64::INFINITY;
867 }
868 self.r_gas * self.temperature / (molar_volume - b)
869 - a / (self.temperature.sqrt() * molar_volume * (molar_volume + b))
870 }
871
872 pub fn compressibility_factor(&self, pressure: f64, molar_volume: f64) -> f64 {
874 if self.r_gas <= 0.0 || self.temperature <= 0.0 {
875 return 0.0;
876 }
877 pressure * molar_volume / (self.r_gas * self.temperature)
878 }
879
880 pub fn boyle_temperature_vdw(a: f64, b: f64, r_gas: f64) -> f64 {
882 a / (r_gas * b)
883 }
884}
885
886pub fn fermi_dirac(energy: f64, mu: f64, temperature: f64) -> f64 {
897 if temperature <= 0.0 {
898 return if energy < mu { 1.0 } else { 0.0 };
899 }
900 1.0 / (((energy - mu) / temperature).exp() + 1.0)
901}
902
903pub fn bose_einstein(energy: f64, mu: f64, temperature: f64) -> f64 {
907 if temperature <= 0.0 {
908 return 0.0;
909 }
910 let x = (energy - mu) / temperature;
911 if x <= 0.0 {
912 return 0.0;
913 }
914 let denom = x.exp() - 1.0;
915 if denom < 1e-30 { 0.0 } else { 1.0 / denom }
916}
917
918pub fn stefan_boltzmann_power(temperature: f64) -> f64 {
922 const SIGMA: f64 = 5.670_374_419e-8;
923 SIGMA * temperature.powi(4)
924}
925
926pub fn wien_peak_wavelength(temperature: f64) -> f64 {
930 const B: f64 = 2.897_771_955e-3;
931 if temperature <= 0.0 {
932 return f64::INFINITY;
933 }
934 B / temperature
935}
936
937pub fn planck_spectral_radiance(wavelength: f64, temperature: f64) -> f64 {
941 const H: f64 = 6.626_070_15e-34;
942 const C: f64 = 2.997_924_58e8;
943 const KB: f64 = 1.380_649e-23;
944 if wavelength <= 0.0 || temperature <= 0.0 {
945 return 0.0;
946 }
947 let exponent = H * C / (wavelength * KB * temperature);
948 if exponent > 700.0 {
949 return 0.0;
950 }
951 2.0 * H * C * C / (wavelength.powi(5) * (exponent.exp() - 1.0))
952}
953
954#[cfg(test)]
959mod tests {
960 use super::*;
961
962 #[test]
965 fn test_canonical_z_two_level() {
966 let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1.0);
968 let z = pf.canonical_z();
969 assert!((z - (1.0 + (-1.0_f64).exp())).abs() < 1e-12);
970 }
971
972 #[test]
973 fn test_canonical_z_zero_temp() {
974 let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 0.0);
975 assert_eq!(pf.canonical_z(), 0.0);
976 }
977
978 #[test]
979 fn test_free_energy_two_level() {
980 let t = 2.0;
981 let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], t);
982 let z = pf.canonical_z();
983 let f_expected = -t * z.ln();
984 assert!((pf.free_energy() - f_expected).abs() < 1e-12);
985 }
986
987 #[test]
988 fn test_average_energy_zero_temp() {
989 let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 0.0);
990 assert_eq!(pf.average_energy(), 0.0);
991 }
992
993 #[test]
994 fn test_average_energy_high_temp() {
995 let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1e6);
997 let e_avg = pf.average_energy();
998 assert!((e_avg - 0.5).abs() < 1e-3);
999 }
1000
1001 #[test]
1002 fn test_entropy_nonnegative() {
1003 let pf = PartitionFunction::canonical(vec![0.0, 1.0, 2.0], vec![1.0, 2.0, 1.0], 1.5);
1004 assert!(pf.entropy() >= 0.0);
1005 }
1006
1007 #[test]
1008 fn test_heat_capacity_positive() {
1009 let pf = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1.0);
1010 assert!(pf.heat_capacity() > 0.0);
1011 }
1012
1013 #[test]
1014 fn test_grand_canonical_z() {
1015 let pf = PartitionFunction::grand_canonical(
1016 vec![0.0, 1.0],
1017 vec![1.0, 1.0],
1018 vec![0.0, 1.0],
1019 1.0,
1020 0.5,
1021 );
1022 let z = pf.grand_canonical_z();
1023 assert!(z > 0.0);
1024 }
1025
1026 #[test]
1027 fn test_density_of_states() {
1028 let pf = PartitionFunction::microcanonical(vec![0.0, 1.0, 2.0], vec![1.0, 3.0, 1.0]);
1029 let omega = pf.density_of_states(1.0, 0.1);
1030 assert!((omega - 3.0).abs() < 1e-12);
1031 }
1032
1033 #[test]
1036 fn test_boltzmann_probabilities_sum_to_one() {
1037 let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0, 2.0, 3.0], 1.0);
1038 let sum: f64 = bd.probabilities().iter().sum();
1039 assert!((sum - 1.0).abs() < 1e-12);
1040 }
1041
1042 #[test]
1043 fn test_boltzmann_entropy_nonneg() {
1044 let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0, 2.0], 1.0);
1045 assert!(bd.entropy() >= 0.0);
1046 }
1047
1048 #[test]
1049 fn test_boltzmann_free_energy() {
1050 let bd = BoltzmannDistribution::uniform(vec![0.0, 0.5, 1.0], 0.5);
1051 let f = bd.free_energy();
1052 assert!(f.is_finite());
1053 }
1054
1055 #[test]
1056 fn test_boltzmann_mean_energy() {
1057 let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0], 1e6);
1058 assert!((bd.mean_energy() - 0.5).abs() < 1e-3);
1060 }
1061
1062 #[test]
1063 fn test_boltzmann_zero_temp() {
1064 let bd = BoltzmannDistribution::uniform(vec![0.0, 1.0], 0.0);
1065 assert_eq!(bd.partition_function(), 0.0);
1066 }
1067
1068 #[test]
1071 fn test_ising_1d_energy_all_up() {
1072 let model = IsingModel::new_1d(4, 1.0, 0.0);
1074 assert!((model.energy() - (-4.0)).abs() < 1e-12);
1075 }
1076
1077 #[test]
1078 fn test_ising_magnetisation_all_up() {
1079 let model = IsingModel::new_1d(4, 1.0, 0.0);
1080 assert!((model.magnetisation() - 1.0).abs() < 1e-12);
1081 }
1082
1083 #[test]
1084 fn test_ising_magnetisation_alternating() {
1085 let mut model = IsingModel::new_1d(4, 1.0, 0.0);
1086 for i in 0..4 {
1087 model.spins[i] = if i % 2 == 0 { 1 } else { -1 };
1088 }
1089 assert!(model.magnetisation().abs() < 1e-12);
1090 }
1091
1092 #[test]
1093 fn test_ising_2d_energy_all_up() {
1094 let model = IsingModel::new_2d(2, 2, 1.0, 0.0);
1097 assert!(model.energy() < 0.0);
1098 }
1099
1100 #[test]
1101 fn test_ising_randomise_changes_spins() {
1102 let mut model = IsingModel::new_1d(10, 1.0, 0.0);
1103 model.randomise(42);
1104 let all_up = model.spins.iter().all(|&s| s == 1);
1106 let all_dn = model.spins.iter().all(|&s| s == -1);
1107 assert!(!(all_up && all_dn));
1109 }
1110
1111 #[test]
1112 fn test_ising_metropolis_runs() {
1113 let mut model = IsingModel::new_1d(8, 1.0, 0.0);
1114 model.randomise(1);
1115 model.metropolis_sweep(10, 2.0, 99);
1117 assert!(model.magnetisation().abs() <= 1.0);
1118 }
1119
1120 #[test]
1121 fn test_ising_onsager_tc() {
1122 let model = IsingModel::new_2d(8, 8, 1.0, 0.0);
1123 let tc = model.onsager_tc();
1124 assert!((tc - 2.269).abs() < 0.01);
1126 }
1127
1128 #[test]
1129 fn test_ising_susceptibility_positive() {
1130 let mut model = IsingModel::new_1d(8, 1.0, 0.0);
1131 model.randomise(7);
1132 let chi = model.susceptibility(2.5, 20, 50, 11);
1133 assert!(chi >= 0.0);
1134 }
1135
1136 #[test]
1139 fn test_phase_transition_order_param_above_tc() {
1140 let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 3.0);
1141 assert_eq!(pt.order_parameter(), 0.0);
1142 }
1143
1144 #[test]
1145 fn test_phase_transition_order_param_below_tc() {
1146 let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 1.0);
1147 let phi = pt.order_parameter();
1148 assert!(phi > 0.0);
1149 }
1150
1151 #[test]
1152 fn test_phase_transition_free_energy_density() {
1153 let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.5, 1.0);
1154 let f0 = pt.free_energy_density(0.0);
1155 let phi_eq = pt.order_parameter();
1156 let f_eq = pt.free_energy_density(phi_eq);
1157 assert!(f_eq <= f0 + 1e-10);
1159 }
1160
1161 #[test]
1162 fn test_phase_transition_reduced_temperature() {
1163 let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 3.0);
1164 assert!((pt.reduced_temperature() - 0.5).abs() < 1e-12);
1165 }
1166
1167 #[test]
1168 fn test_phase_transition_susceptibility_diverges_near_tc() {
1169 let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 2.0 + 1e-8);
1170 assert!(pt.susceptibility() > 1e5);
1171 }
1172
1173 #[test]
1176 fn test_einstein_relation() {
1177 let fd = FluctuationDissipation::new(1.0);
1178 assert!((fd.einstein_relation(0.5) - 0.5).abs() < 1e-12);
1179 }
1180
1181 #[test]
1182 fn test_mobility_from_diffusivity() {
1183 let fd = FluctuationDissipation::new(2.0);
1184 assert!((fd.mobility_from_diffusivity(4.0) - 2.0).abs() < 1e-12);
1185 }
1186
1187 #[test]
1188 fn test_noise_power_spectral_density() {
1189 let fd = FluctuationDissipation::new(300.0);
1190 let s = fd.noise_power_spectral_density(50.0);
1192 assert!((s - 4.0 * 300.0 * 50.0).abs() < 1e-6);
1193 }
1194
1195 #[test]
1196 fn test_response_function_imaginary_at_resonance() {
1197 let fd = FluctuationDissipation::new(1.0);
1198 let chi_im = fd.response_function_imaginary(1.0, 1.0, 0.1);
1199 assert!(chi_im > 0.0);
1200 }
1201
1202 #[test]
1203 fn test_fluctuation_spectrum_zero_omega() {
1204 let fd = FluctuationDissipation::new(1.0);
1205 assert_eq!(fd.fluctuation_spectrum(0.0, 1.0, 0.1), 0.0);
1206 }
1207
1208 #[test]
1211 fn test_autocorrelation_constant_signal() {
1212 let signal = vec![1.0; 10];
1213 let acf = GreenKubo::autocorrelation(&signal);
1214 assert_eq!(acf.len(), 10);
1216 for v in &acf {
1217 assert!(v.abs() < 1e-12);
1218 }
1219 }
1220
1221 #[test]
1222 fn test_autocorrelation_lag_zero_is_one() {
1223 let signal: Vec<f64> = (0..20).map(|i| (i as f64 * 0.3).sin()).collect();
1224 let acf = GreenKubo::autocorrelation(&signal);
1225 assert!(!acf.is_empty());
1226 assert!((acf[0] - 1.0).abs() < 1e-10);
1227 }
1228
1229 #[test]
1230 fn test_diffusivity_from_vacf() {
1231 let gk = GreenKubo::new(0.01, 1.0, 1.0);
1232 let tau = 1.0;
1234 let n = 200;
1235 let vacf: Vec<f64> = (0..n).map(|i| (-(i as f64) * 0.01 / tau).exp()).collect();
1236 let d = gk.diffusivity_from_vacf(&vacf);
1237 assert!((d - tau / 3.0).abs() < 0.05);
1239 }
1240
1241 #[test]
1242 fn test_shear_viscosity_zero_temp() {
1243 let gk = GreenKubo::new(0.01, 0.0, 1.0);
1244 assert_eq!(gk.shear_viscosity(&[1.0, 0.5, 0.1]), 0.0);
1245 }
1246
1247 #[test]
1248 fn test_correlation_time() {
1249 let tau = 2.0;
1250 let dt = 0.1;
1251 let n = 100;
1252 let acf: Vec<f64> = (0..n).map(|i| (-(i as f64) * dt / tau).exp()).collect();
1253 let ct = GreenKubo::correlation_time(&acf, dt);
1254 assert!((ct - tau).abs() < 0.5);
1256 }
1257
1258 #[test]
1261 fn test_vdw_ideal_limit() {
1262 let eos = EquationOfState::new(8.314, 300.0);
1264 let v = 0.025; let p = eos.van_der_waals_pressure(v, 0.0, 0.0);
1266 assert!((p - 8.314 * 300.0 / v).abs() < 1e-6);
1267 }
1268
1269 #[test]
1270 fn test_vdw_critical_constants() {
1271 let (tc, pc, vc) = EquationOfState::vdw_critical_constants(0.364, 4.27e-5, 8.314);
1272 assert!(tc > 0.0 && pc > 0.0 && vc > 0.0);
1273 }
1274
1275 #[test]
1276 fn test_peng_robinson_pressure_finite() {
1277 let eos = EquationOfState::new(8.314, 300.0);
1278 let p = eos.peng_robinson_pressure(0.001, 0.4, 2.5e-5, 0.37, 190.0);
1279 assert!(p.is_finite());
1280 }
1281
1282 #[test]
1283 fn test_redlich_kwong_pressure_finite() {
1284 let eos = EquationOfState::new(8.314, 400.0);
1285 let p = eos.redlich_kwong_pressure(0.002, 1.0, 2.6e-5);
1286 assert!(p.is_finite() && p > 0.0);
1287 }
1288
1289 #[test]
1290 fn test_compressibility_factor_ideal_gas() {
1291 let eos = EquationOfState::new(8.314, 300.0);
1292 let v = 0.025;
1293 let p = eos.van_der_waals_pressure(v, 0.0, 0.0);
1294 let z = eos.compressibility_factor(p, v);
1295 assert!((z - 1.0).abs() < 1e-10);
1296 }
1297
1298 #[test]
1299 fn test_boyle_temperature_vdw() {
1300 let t_b = EquationOfState::boyle_temperature_vdw(0.364, 4.27e-5, 8.314);
1301 assert!(t_b > 0.0);
1302 }
1303
1304 #[test]
1307 fn test_fermi_dirac_at_mu() {
1308 assert!((fermi_dirac(1.0, 1.0, 1.0) - 0.5).abs() < 1e-12);
1310 }
1311
1312 #[test]
1313 fn test_fermi_dirac_below_mu() {
1314 assert!(fermi_dirac(0.0, 1.0, 1.0) > 0.5);
1315 }
1316
1317 #[test]
1318 fn test_fermi_dirac_zero_temp_below_mu() {
1319 assert_eq!(fermi_dirac(0.5, 1.0, 0.0), 1.0);
1320 }
1321
1322 #[test]
1323 fn test_fermi_dirac_zero_temp_above_mu() {
1324 assert_eq!(fermi_dirac(1.5, 1.0, 0.0), 0.0);
1325 }
1326
1327 #[test]
1328 fn test_bose_einstein_positive() {
1329 let n = bose_einstein(1.0, 0.5, 1.0);
1330 assert!(n > 0.0);
1331 }
1332
1333 #[test]
1334 fn test_bose_einstein_at_zero_argument() {
1335 assert_eq!(bose_einstein(1.0, 1.0, 1.0), 0.0);
1337 }
1338
1339 #[test]
1340 fn test_planck_spectral_radiance_positive() {
1341 let b = planck_spectral_radiance(500e-9, 5778.0);
1342 assert!(b > 0.0);
1343 }
1344
1345 #[test]
1346 fn test_wien_peak_wavelength() {
1347 let lam = wien_peak_wavelength(5778.0);
1349 assert!((lam - 5.02e-7).abs() < 1e-8);
1350 }
1351
1352 #[test]
1353 fn test_stefan_boltzmann() {
1354 let p = stefan_boltzmann_power(1.0);
1356 assert!((p - 5.670374419e-8).abs() < 1e-15);
1357 }
1358
1359 #[allow(clippy::too_many_arguments)]
1360 #[test]
1361 fn test_partition_function_degeneracy_scaling() {
1362 let pf1 = PartitionFunction::canonical(vec![0.0, 1.0], vec![1.0, 1.0], 1.0);
1364 let pf2 = PartitionFunction::canonical(vec![0.0, 1.0], vec![2.0, 2.0], 1.0);
1365 let diff = (pf1.average_energy() - pf2.average_energy()).abs();
1366 assert!(diff < 1e-12);
1367 }
1368
1369 #[test]
1370 fn test_ising_delta_energy_flip_all_up() {
1371 let model = IsingModel::new_1d(4, 1.0, 0.0);
1372 let de = model.delta_energy(0);
1374 assert!((de - 4.0).abs() < 1e-12);
1375 }
1376
1377 #[test]
1378 fn test_phase_transition_a_coeff() {
1379 let pt = PhaseTransition::new(1.0, 2.0, 1.0, 0.0, 3.0);
1380 assert!((pt.a_coeff() - 1.0).abs() < 1e-12);
1381 }
1382
1383 #[test]
1384 fn test_gk_autocorrelation_empty() {
1385 let acf = GreenKubo::autocorrelation(&[]);
1386 assert!(acf.is_empty());
1387 }
1388
1389 #[test]
1390 fn test_electrical_conductivity_zero_volume() {
1391 let gk = GreenKubo::new(0.01, 1.0, 0.0);
1392 assert_eq!(gk.electrical_conductivity(&[1.0, 0.5]), 0.0);
1393 }
1394
1395 #[test]
1396 fn test_vdw_pressure_below_b_returns_inf() {
1397 let eos = EquationOfState::new(8.314, 300.0);
1398 let p = eos.van_der_waals_pressure(1e-6, 0.0, 0.01);
1399 assert_eq!(p, f64::INFINITY);
1400 }
1401
1402 #[test]
1403 fn test_mean_field_tc_2d() {
1404 let model = IsingModel::new_2d(4, 4, 1.0, 0.0);
1405 assert!((model.mean_field_tc_2d() - 4.0).abs() < 1e-12);
1406 }
1407}