Skip to main content

oxilean_std/statistical_mechanics/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
6
7use super::functions::*;
8use super::functions::{BOLTZMANN_K, PLANCK_H};
9
10/// Discrete renormalization group iteration for 1D models.
11///
12/// Iterates the recursion relation g_{n+1} = R(g_n) for a coupling constant g.
13/// Can be used to find fixed points and analyze RG flows.
14#[allow(dead_code)]
15pub struct RenormalizationGroup {
16    /// Initial coupling constant g₀
17    pub initial_coupling: f64,
18    /// Scale factor b at each RG step (usually b = 2)
19    pub scale_factor: f64,
20}
21impl RenormalizationGroup {
22    /// Create an RG solver with initial coupling and scale factor.
23    pub fn new(initial_coupling: f64, scale_factor: f64) -> Self {
24        Self {
25            initial_coupling,
26            scale_factor,
27        }
28    }
29    /// Iterate the RG recursion R(g) for n steps.
30    ///
31    /// Returns the trajectory [g₀, g₁, ..., gₙ].
32    pub fn iterate<F>(&self, rg_map: F, n_steps: usize) -> Vec<f64>
33    where
34        F: Fn(f64) -> f64,
35    {
36        let mut trajectory = vec![self.initial_coupling];
37        let mut g = self.initial_coupling;
38        for _ in 0..n_steps {
39            g = rg_map(g);
40            trajectory.push(g);
41        }
42        trajectory
43    }
44    /// Find a fixed point using Newton's method: g* = R(g*) ⟺ R(g) - g = 0.
45    ///
46    /// Returns (g_star, converged).
47    pub fn find_fixed_point<F>(&self, rg_map: &F, tol: f64, max_iter: usize) -> (f64, bool)
48    where
49        F: Fn(f64) -> f64,
50    {
51        let mut g = self.initial_coupling;
52        for _ in 0..max_iter {
53            let g_new = rg_map(g);
54            let residual = g_new - g;
55            if residual.abs() < tol {
56                return (g_new, true);
57            }
58            g = g + 0.5 * residual;
59        }
60        let converged = (rg_map(g) - g).abs() < tol;
61        (g, converged)
62    }
63    /// Classify fixed point as stable (attractive) or unstable (repulsive).
64    ///
65    /// Computes |dR/dg| at g*. Returns Some(true) if stable, Some(false) if unstable.
66    pub fn is_stable_fixed_point<F>(&self, rg_map: &F, g_star: f64) -> Option<bool>
67    where
68        F: Fn(f64) -> f64,
69    {
70        let dg = g_star.abs() * 1e-6 + 1e-10;
71        if dg == 0.0 {
72            return None;
73        }
74        let deriv = (rg_map(g_star + dg) - rg_map(g_star - dg)) / (2.0 * dg);
75        Some(deriv.abs() < 1.0)
76    }
77    /// Compute the RG eigenvalue (scaling exponent) at a fixed point.
78    pub fn scaling_exponent<F>(&self, rg_map: &F, g_star: f64) -> f64
79    where
80        F: Fn(f64) -> f64,
81    {
82        let dg = g_star.abs() * 1e-6 + 1e-10;
83        let deriv = (rg_map(g_star + dg) - rg_map(g_star - dg)) / (2.0 * dg);
84        if self.scale_factor > 1.0 && deriv.abs() > 0.0 {
85            deriv.abs().ln() / self.scale_factor.ln()
86        } else {
87            0.0
88        }
89    }
90}
91/// Grand canonical ensemble simulation.
92///
93/// Models a system with variable particle number coupled to a reservoir
94/// at temperature T and chemical potential μ.
95#[allow(dead_code)]
96pub struct GrandCanonicalEnsemble {
97    /// Single-particle energy levels ε_k
98    pub energy_levels: Vec<f64>,
99    /// Temperature T [K]
100    pub temperature: f64,
101    /// Chemical potential μ [J]
102    pub chemical_potential: f64,
103    /// Statistics: true = Fermi-Dirac, false = Bose-Einstein
104    pub is_fermionic: bool,
105}
106impl GrandCanonicalEnsemble {
107    /// Create a grand canonical ensemble.
108    pub fn new(energy_levels: Vec<f64>, temperature: f64, mu: f64, fermionic: bool) -> Self {
109        Self {
110            energy_levels,
111            temperature,
112            chemical_potential: mu,
113            is_fermionic: fermionic,
114        }
115    }
116    /// Inverse temperature β = 1/(k_B T)
117    pub fn beta(&self) -> f64 {
118        1.0 / (BOLTZMANN_K * self.temperature)
119    }
120    /// Mean occupation number for level k:
121    /// n_k = 1/(exp(β(ε_k - μ)) ∓ 1)  where − is FD, + is BE
122    pub fn mean_occupation(&self, k: usize) -> f64 {
123        let x = self.beta() * (self.energy_levels[k] - self.chemical_potential);
124        if self.is_fermionic {
125            1.0 / (x.exp() + 1.0)
126        } else {
127            if x <= 0.0 {
128                f64::INFINITY
129            } else {
130                1.0 / (x.exp() - 1.0)
131            }
132        }
133    }
134    /// Grand potential: Ω = ∓k_BT Σ_k ln(1 ± exp(β(μ - ε_k)))
135    pub fn grand_potential(&self) -> f64 {
136        let b = self.beta();
137        let sum: f64 = self
138            .energy_levels
139            .iter()
140            .map(|&eps| {
141                let x = b * (self.chemical_potential - eps);
142                if self.is_fermionic {
143                    (1.0 + x.exp()).ln()
144                } else if x < -700.0 {
145                    0.0
146                } else {
147                    -(1.0 - x.exp()).abs().ln()
148                }
149            })
150            .sum();
151        -BOLTZMANN_K * self.temperature * sum
152    }
153    /// Mean total particle number: ⟨N⟩ = Σ_k ⟨n_k⟩
154    pub fn mean_particle_number(&self) -> f64 {
155        (0..self.energy_levels.len())
156            .filter_map(|k| {
157                let n = self.mean_occupation(k);
158                if n.is_finite() {
159                    Some(n)
160                } else {
161                    None
162                }
163            })
164            .sum()
165    }
166    /// Mean total energy: ⟨E⟩ = Σ_k ε_k ⟨n_k⟩
167    pub fn mean_energy(&self) -> f64 {
168        self.energy_levels
169            .iter()
170            .enumerate()
171            .filter_map(|(k, &eps)| {
172                let n = self.mean_occupation(k);
173                if n.is_finite() {
174                    Some(eps * n)
175                } else {
176                    None
177                }
178            })
179            .sum()
180    }
181}
182/// Computes equal-time connected correlation functions from a discrete distribution.
183#[allow(dead_code)]
184pub struct CorrelationFunction {
185    /// Sample values of the order parameter
186    pub samples: Vec<f64>,
187}
188impl CorrelationFunction {
189    /// Create from a list of samples.
190    pub fn new(samples: Vec<f64>) -> Self {
191        Self { samples }
192    }
193    /// Sample mean: ⟨φ⟩ = (1/N) Σ φ_i
194    pub fn mean(&self) -> f64 {
195        if self.samples.is_empty() {
196            return 0.0;
197        }
198        self.samples.iter().sum::<f64>() / self.samples.len() as f64
199    }
200    /// Variance: ⟨φ²⟩ - ⟨φ⟩²
201    pub fn variance(&self) -> f64 {
202        if self.samples.is_empty() {
203            return 0.0;
204        }
205        let m = self.mean();
206        let m2 = self.samples.iter().map(|&x| x * x).sum::<f64>() / self.samples.len() as f64;
207        m2 - m * m
208    }
209    /// Connected two-point correlator at lag τ (discrete): C(τ) = ⟨φ(0)φ(τ)⟩ - ⟨φ⟩²
210    pub fn connected_correlator(&self, lag: usize) -> f64 {
211        let n = self.samples.len();
212        if lag >= n {
213            return 0.0;
214        }
215        let m = self.mean();
216        let count = (n - lag) as f64;
217        let corr: f64 = (0..n - lag)
218            .map(|i| self.samples[i] * self.samples[i + lag])
219            .sum::<f64>()
220            / count;
221        corr - m * m
222    }
223    /// Autocorrelation time τ_int = ½ + Σ_{τ=1}^{∞} C(τ)/C(0)
224    pub fn integrated_autocorrelation_time(&self, max_lag: usize) -> f64 {
225        let c0 = self.connected_correlator(0);
226        if c0.abs() < 1e-300 {
227            return 0.5;
228        }
229        let sum: f64 = (1..max_lag.min(self.samples.len()))
230            .map(|tau| self.connected_correlator(tau) / c0)
231            .sum();
232        0.5 + sum
233    }
234    /// Susceptibility proportional to N * variance (for periodic systems)
235    pub fn susceptibility(&self) -> f64 {
236        self.variance() * self.samples.len() as f64
237    }
238}
239/// A statistical ensemble in the canonical ensemble
240pub struct Ensemble {
241    pub energies: Vec<f64>,
242    pub temperature: f64,
243    pub degeneracies: Vec<u32>,
244}
245impl Ensemble {
246    /// Create an ensemble with unit degeneracies
247    pub fn new(energies: Vec<f64>, temperature: f64) -> Self {
248        let n = energies.len();
249        Self {
250            energies,
251            temperature,
252            degeneracies: vec![1; n],
253        }
254    }
255    /// Create an ensemble with explicit degeneracies
256    pub fn with_degeneracies(energies: Vec<f64>, degeneracies: Vec<u32>, temperature: f64) -> Self {
257        Self {
258            energies,
259            temperature,
260            degeneracies,
261        }
262    }
263    /// Inverse temperature β = 1/(k_B T)
264    pub fn beta(&self) -> f64 {
265        1.0 / (BOLTZMANN_K * self.temperature)
266    }
267    /// Partition function Z = Σ g_i * exp(-β E_i)
268    pub fn partition_function(&self) -> f64 {
269        let beta = self.beta();
270        self.energies
271            .iter()
272            .zip(self.degeneracies.iter())
273            .map(|(&e, &g)| (g as f64) * (-beta * e).exp())
274            .sum()
275    }
276    /// Boltzmann factor for a given energy: exp(-β E)
277    pub fn boltzmann_factor(&self, energy: f64) -> f64 {
278        (-self.beta() * energy).exp()
279    }
280    /// Probability of state at index i: g_i exp(-β E_i) / Z
281    pub fn probability(&self, state_idx: usize) -> f64 {
282        let z = self.partition_function();
283        if z == 0.0 {
284            return 0.0;
285        }
286        let e = self.energies[state_idx];
287        let g = self.degeneracies[state_idx] as f64;
288        g * self.boltzmann_factor(e) / z
289    }
290    /// Mean energy ⟨E⟩ = Σ E_i p_i
291    pub fn mean_energy(&self) -> f64 {
292        self.energies
293            .iter()
294            .enumerate()
295            .map(|(i, &e)| e * self.probability(i))
296            .sum()
297    }
298    /// Gibbs entropy S = -k_B Σ p_i log p_i
299    pub fn entropy(&self) -> f64 {
300        let s: f64 = self
301            .energies
302            .iter()
303            .enumerate()
304            .filter_map(|(i, _)| {
305                let p = self.probability(i);
306                if p > 1e-300 {
307                    Some(-p * p.ln())
308                } else {
309                    None
310                }
311            })
312            .sum();
313        BOLTZMANN_K * s
314    }
315    /// Helmholtz free energy F = -k_B T log Z
316    pub fn free_energy(&self) -> f64 {
317        let z = self.partition_function();
318        if z <= 0.0 {
319            return f64::INFINITY;
320        }
321        -BOLTZMANN_K * self.temperature * z.ln()
322    }
323    /// Numerical heat capacity C_v = d⟨E⟩/dT using finite differences
324    pub fn heat_capacity(&self) -> f64 {
325        let dt = self.temperature * 1e-4;
326        let e_high = {
327            let e_high = Ensemble::with_degeneracies(
328                self.energies.clone(),
329                self.degeneracies.clone(),
330                self.temperature + dt,
331            );
332            e_high.mean_energy()
333        };
334        let e_low = {
335            let e_low = Ensemble::with_degeneracies(
336                self.energies.clone(),
337                self.degeneracies.clone(),
338                self.temperature - dt,
339            );
340            e_low.mean_energy()
341        };
342        (e_high - e_low) / (2.0 * dt)
343    }
344    /// Index of the state with the highest probability (ground state at low T)
345    pub fn max_prob_state(&self) -> usize {
346        self.energies
347            .iter()
348            .enumerate()
349            .map(|(i, _)| (i, self.probability(i)))
350            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
351            .map(|(i, _)| i)
352            .unwrap_or(0)
353    }
354}
355/// Mean-field Ising model self-consistent solver.
356///
357/// Self-consistency equation: m = tanh(β (h + z J m))
358/// where z is the coordination number (e.g., z=4 for 2D square lattice).
359pub struct MeanFieldIsing {
360    /// Exchange coupling J
361    pub j_coupling: f64,
362    /// External field h
363    pub h_field: f64,
364    /// Coordination number z
365    pub coordination: f64,
366    /// Temperature T (Kelvin)
367    pub temperature: f64,
368}
369impl MeanFieldIsing {
370    pub fn new(j: f64, h: f64, z: f64, temp: f64) -> Self {
371        Self {
372            j_coupling: j,
373            h_field: h,
374            coordination: z,
375            temperature: temp,
376        }
377    }
378    /// Mean field critical temperature T_c = z J / k_B
379    pub fn critical_temperature(&self) -> f64 {
380        self.coordination * self.j_coupling / BOLTZMANN_K
381    }
382    /// Reduced temperature t = (T - T_c) / T_c
383    pub fn reduced_temperature(&self) -> f64 {
384        let tc = self.critical_temperature();
385        (self.temperature - tc) / tc
386    }
387    /// Self-consistency equation residual: F(m) = m - tanh(β(h + zJm))
388    fn residual(&self, m: f64) -> f64 {
389        let b = 1.0 / (BOLTZMANN_K * self.temperature);
390        let arg = b * (self.h_field + self.coordination * self.j_coupling * m);
391        m - arg.tanh()
392    }
393    /// Solve for the self-consistent magnetization using simple iteration.
394    ///
395    /// Returns (m, converged) — converged is true if |F(m)| < tol.
396    pub fn solve(&self, initial_m: f64, max_iter: usize, tol: f64) -> (f64, bool) {
397        let mut m = initial_m;
398        for _ in 0..max_iter {
399            let b = 1.0 / (BOLTZMANN_K * self.temperature);
400            let arg = b * (self.h_field + self.coordination * self.j_coupling * m);
401            let m_new = arg.tanh();
402            if (m_new - m).abs() < tol {
403                return (m_new, true);
404            }
405            m = 0.8 * m + 0.2 * m_new;
406        }
407        let converged = self.residual(m).abs() < tol;
408        (m, converged)
409    }
410    /// Find all stable solutions (handles symmetry breaking below T_c)
411    ///
412    /// Returns a Vec of converged magnetization values.
413    pub fn find_all_solutions(&self) -> Vec<f64> {
414        let seeds = [-0.999, -0.5, 0.0, 0.5, 0.999];
415        let mut solutions: Vec<f64> = Vec::new();
416        for &seed in &seeds {
417            let (m, converged) = self.solve(seed, 2000, 1e-10);
418            if converged {
419                let is_new = solutions
420                    .iter()
421                    .all(|&existing| (existing - m).abs() > 1e-6);
422                if is_new {
423                    solutions.push(m);
424                }
425            }
426        }
427        solutions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
428        solutions
429    }
430    /// Mean field free energy density: f(m) = -zJm²/2 - hm + k_BT * entropy_term
431    pub fn free_energy_density(&self, m: f64) -> f64 {
432        let b = 1.0 / (BOLTZMANN_K * self.temperature);
433        let interaction = -0.5 * self.coordination * self.j_coupling * m * m;
434        let field_term = -self.h_field * m;
435        let h_eff = self.h_field + self.coordination * self.j_coupling * m;
436        let entropy_term = if (b * h_eff).abs() < 700.0 {
437            -(b * h_eff).cosh().ln() / b
438        } else {
439            -(b * h_eff).abs() / b
440        };
441        interaction + field_term + entropy_term
442    }
443}
444/// Record of critical exponents for a universality class
445#[derive(Debug, Clone)]
446pub struct CriticalExponents {
447    /// Name of the universality class
448    pub name: &'static str,
449    /// Spatial dimension
450    pub dimension: u8,
451    /// Heat capacity exponent: C ~ |t|^{-α}
452    pub alpha: f64,
453    /// Order parameter exponent: m ~ |t|^β
454    pub beta: f64,
455    /// Susceptibility exponent: χ ~ |t|^{-γ}
456    pub gamma: f64,
457    /// Critical isotherm exponent: h ~ m^δ
458    pub delta: f64,
459    /// Correlation length exponent: ξ ~ |t|^{-ν}
460    pub nu: f64,
461    /// Anomalous dimension
462    pub eta: f64,
463}
464impl CriticalExponents {
465    /// Check Widom scaling relation: γ = β(δ - 1)
466    pub fn check_widom(&self) -> bool {
467        let lhs = self.gamma;
468        let rhs = self.beta * (self.delta - 1.0);
469        (lhs - rhs).abs() < 0.01
470    }
471    /// Check Rushbrooke scaling relation: α + 2β + γ = 2
472    pub fn check_rushbrooke(&self) -> bool {
473        let sum = self.alpha + 2.0 * self.beta + self.gamma;
474        (sum - 2.0).abs() < 0.01
475    }
476    /// Check Fisher scaling relation: γ = ν(2 - η)
477    pub fn check_fisher(&self) -> bool {
478        let lhs = self.gamma;
479        let rhs = self.nu * (2.0 - self.eta);
480        (lhs - rhs).abs() < 0.05
481    }
482}
483/// Landau free energy functional for second-order phase transitions.
484///
485/// f(m, t) = a t m² + b m⁴ + c m⁶ + h m
486/// where t = (T - T_c) / T_c is the reduced temperature.
487pub struct LandauFreeEnergy {
488    /// Coefficient of m² term (typically > 0)
489    pub a: f64,
490    /// Coefficient of m⁴ term (must be > 0 for stability)
491    pub b: f64,
492    /// Coefficient of m⁶ term (optional higher-order stabilization)
493    pub c: f64,
494    /// External field coupling
495    pub h: f64,
496}
497impl LandauFreeEnergy {
498    /// Standard second-order transition: f = a t m² + b m⁴ + h m
499    pub fn new_second_order(a: f64, b: f64, h: f64) -> Self {
500        Self { a, b, c: 0.0, h }
501    }
502    /// Tricritical point model: f = a t m² + c m⁶ + h m  (b = 0)
503    pub fn new_tricritical(a: f64, c: f64, h: f64) -> Self {
504        Self { a, b: 0.0, c, h }
505    }
506    /// Free energy density at order parameter m and reduced temperature t
507    pub fn eval(&self, m: f64, t: f64) -> f64 {
508        self.a * t * m * m + self.b * m * m * m * m + self.c * m.powf(6.0) + self.h * m
509    }
510    /// Derivative ∂f/∂m
511    pub fn deriv(&self, m: f64, t: f64) -> f64 {
512        2.0 * self.a * t * m + 4.0 * self.b * m * m * m + 6.0 * self.c * m.powf(5.0) + self.h
513    }
514    /// Second derivative ∂²f/∂m²
515    pub fn deriv2(&self, m: f64, t: f64) -> f64 {
516        2.0 * self.a * t + 12.0 * self.b * m * m + 30.0 * self.c * m.powf(4.0)
517    }
518    /// Minimize free energy using Newton's method (or bisection fallback).
519    ///
520    /// Returns (m_min, f_min, converged).
521    pub fn minimize(&self, t: f64, initial_m: f64, tol: f64, max_iter: usize) -> (f64, f64, bool) {
522        let mut m = initial_m;
523        for _ in 0..max_iter {
524            let f1 = self.deriv(m, t);
525            let f2 = self.deriv2(m, t);
526            if f2.abs() < 1e-300 {
527                break;
528            }
529            let step = -f1 / f2;
530            let step = step.clamp(-0.5, 0.5);
531            m += step;
532            if step.abs() < tol {
533                return (m, self.eval(m, t), true);
534            }
535        }
536        (m, self.eval(m, t), self.deriv(m, t).abs() < tol * 100.0)
537    }
538    /// Equilibrium order parameter at reduced temperature t (h=0 assumed).
539    ///
540    /// Below T_c (t < 0): m ≈ sqrt(-at / 2b)  for b > 0 case.
541    pub fn equilibrium_order_parameter(&self, t: f64) -> f64 {
542        if t >= 0.0 || self.b <= 0.0 {
543            let (m, _, _) = self.minimize(t, 0.01, 1e-10, 1000);
544            return m;
545        }
546        let m_mf = (-self.a * t / (2.0 * self.b)).sqrt();
547        let (m_pos, f_pos, _) = self.minimize(t, m_mf, 1e-10, 1000);
548        let (m_neg, f_neg, _) = self.minimize(t, -m_mf, 1e-10, 1000);
549        if f_pos <= f_neg {
550            m_pos
551        } else {
552            m_neg
553        }
554    }
555    /// Spontaneous magnetization as a function of reduced temperature t in [-1, 0]
556    pub fn spontaneous_magnetization_curve(&self, n_points: usize) -> Vec<(f64, f64)> {
557        (0..n_points)
558            .map(|i| {
559                let t = -1.0 + (i as f64) / (n_points as f64);
560                let m = self.equilibrium_order_parameter(t);
561                (t, m.abs())
562            })
563            .collect()
564    }
565}
566/// Canonical ensemble simulation for a system with discrete energy levels.
567///
568/// Wraps `Ensemble` with additional derived quantities.
569pub struct CanonicalEnsemble {
570    inner: Ensemble,
571}
572impl CanonicalEnsemble {
573    /// Create from energy levels and temperature (in Kelvin)
574    pub fn new(energies: Vec<f64>, temperature: f64) -> Self {
575        Self {
576            inner: Ensemble::new(energies, temperature),
577        }
578    }
579    /// Create with explicit degeneracies
580    pub fn with_degeneracies(energies: Vec<f64>, degeneracies: Vec<u32>, temperature: f64) -> Self {
581        Self {
582            inner: Ensemble::with_degeneracies(energies, degeneracies, temperature),
583        }
584    }
585    /// Partition function Z(β)
586    pub fn partition_function(&self) -> f64 {
587        self.inner.partition_function()
588    }
589    /// Helmholtz free energy F = -k_B T ln Z
590    pub fn free_energy(&self) -> f64 {
591        self.inner.free_energy()
592    }
593    /// Mean energy ⟨E⟩
594    pub fn mean_energy(&self) -> f64 {
595        self.inner.mean_energy()
596    }
597    /// Entropy S = (⟨E⟩ - F) / T
598    pub fn entropy(&self) -> f64 {
599        (self.inner.mean_energy() - self.inner.free_energy()) / self.inner.temperature
600    }
601    /// Heat capacity C_v = d⟨E⟩/dT
602    pub fn heat_capacity(&self) -> f64 {
603        self.inner.heat_capacity()
604    }
605    /// Variance of energy: Var(E) = ⟨E²⟩ - ⟨E⟩²
606    pub fn energy_variance(&self) -> f64 {
607        let mean_e = self.inner.mean_energy();
608        let mean_e2: f64 = self
609            .inner
610            .energies
611            .iter()
612            .enumerate()
613            .map(|(i, &e)| e * e * self.inner.probability(i))
614            .sum();
615        mean_e2 - mean_e * mean_e
616    }
617    /// Probability distribution over energy levels
618    pub fn probabilities(&self) -> Vec<f64> {
619        (0..self.inner.energies.len())
620            .map(|i| self.inner.probability(i))
621            .collect()
622    }
623    /// Relative entropy (KL divergence) from uniform distribution: D(p||q)
624    pub fn kl_from_uniform(&self) -> f64 {
625        let n = self.inner.energies.len() as f64;
626        if n == 0.0 {
627            return 0.0;
628        }
629        (0..self.inner.energies.len())
630            .filter_map(|i| {
631                let p = self.inner.probability(i);
632                if p > 1e-300 {
633                    Some(p * (p * n).ln())
634                } else {
635                    None
636                }
637            })
638            .sum()
639    }
640}
641/// Van der Waals gas: (P + a n²/V²)(V - nb) = nRT.
642///
643/// Uses SI units with k_B (per-particle form):
644///   (P + a/v²)(v - b) = k_B T   where v = V/N is volume per particle.
645#[allow(dead_code)]
646pub struct VanDerWaalsGas {
647    /// Attractive interaction parameter a [J·m³]
648    pub a: f64,
649    /// Excluded volume parameter b [m³]
650    pub b: f64,
651    /// Temperature T [K]
652    pub temperature: f64,
653}
654impl VanDerWaalsGas {
655    /// Create a van der Waals gas.
656    pub fn new(a: f64, b: f64, temperature: f64) -> Self {
657        Self { a, b, temperature }
658    }
659    /// Pressure at specific volume v = V/N per particle.
660    ///
661    /// P = k_B T / (v - b) - a / v²
662    pub fn pressure(&self, v: f64) -> f64 {
663        if v <= self.b {
664            return f64::INFINITY;
665        }
666        BOLTZMANN_K * self.temperature / (v - self.b) - self.a / (v * v)
667    }
668    /// Critical temperature: T_c = 8a / (27 k_B b)
669    pub fn critical_temperature(&self) -> f64 {
670        8.0 * self.a / (27.0 * BOLTZMANN_K * self.b)
671    }
672    /// Critical pressure: P_c = a / (27 b²)
673    pub fn critical_pressure(&self) -> f64 {
674        self.a / (27.0 * self.b * self.b)
675    }
676    /// Critical volume per particle: v_c = 3b
677    pub fn critical_volume(&self) -> f64 {
678        3.0 * self.b
679    }
680    /// Reduced temperature: T_r = T / T_c
681    pub fn reduced_temperature(&self) -> f64 {
682        self.temperature / self.critical_temperature()
683    }
684    /// Check if the system is above the critical temperature
685    pub fn is_supercritical(&self) -> bool {
686        self.temperature >= self.critical_temperature()
687    }
688    /// Compressibility factor at the critical point: Z_c = P_c v_c / (k_B T_c) = 3/8
689    pub fn critical_compressibility() -> f64 {
690        3.0 / 8.0
691    }
692}
693/// Equation of state using the virial expansion up to third virial coefficient.
694///
695/// P/(ρk_BT) = 1 + B₂(T) ρ + B₃(T) ρ² + ...
696#[allow(dead_code)]
697pub struct VirialGas {
698    /// Second virial coefficient B₂(T) [m³]
699    pub b2: f64,
700    /// Third virial coefficient B₃(T) [m⁶]
701    pub b3: f64,
702    /// Temperature T [K]
703    pub temperature: f64,
704}
705impl VirialGas {
706    /// Create a virial gas model with explicit B₂ and B₃.
707    pub fn new(b2: f64, b3: f64, temperature: f64) -> Self {
708        Self {
709            b2,
710            b3,
711            temperature,
712        }
713    }
714    /// Pressure from virial expansion: P = ρ k_B T (1 + B₂ρ + B₃ρ²)
715    pub fn pressure(&self, density: f64) -> f64 {
716        BOLTZMANN_K
717            * self.temperature
718            * density
719            * (1.0 + self.b2 * density + self.b3 * density * density)
720    }
721    /// Compressibility factor Z = Pv/(k_BT) = 1 + B₂/v + B₃/v² (v = 1/ρ)
722    pub fn compressibility_factor(&self, density: f64) -> f64 {
723        1.0 + self.b2 * density + self.b3 * density * density
724    }
725    /// Hard-sphere second virial coefficient: B₂ = (2/3)π σ³
726    pub fn hard_sphere_b2(sigma: f64) -> f64 {
727        (2.0 / 3.0) * std::f64::consts::PI * sigma * sigma * sigma
728    }
729    /// Boyle temperature: temperature where B₂(T) = 0
730    /// For Lennard-Jones gas this is approximately T_B ≈ 3.4 ε/k_B
731    pub fn is_above_boyle_temperature(&self) -> bool {
732        self.b2 >= 0.0
733    }
734}
735/// 2D Ising model on an n×n grid with periodic boundary conditions
736pub struct IsingModel {
737    pub spins: Vec<Vec<i8>>,
738    pub j_coupling: f64,
739    pub temperature: f64,
740}
741impl IsingModel {
742    /// Create an n×n Ising model with a deterministic initial spin pattern
743    pub fn new(n: usize, j: f64, temp: f64) -> Self {
744        let mut rng_state: u64 = 12345;
745        let spins = (0..n)
746            .map(|_| {
747                (0..n)
748                    .map(|_| {
749                        let r = Self::lcg_next(&mut rng_state);
750                        if r < 0.5 {
751                            1i8
752                        } else {
753                            -1i8
754                        }
755                    })
756                    .collect()
757            })
758            .collect();
759        Self {
760            spins,
761            j_coupling: j,
762            temperature: temp,
763        }
764    }
765    /// Total energy E = -J Σ s_i s_j (nearest neighbors, periodic BC)
766    pub fn energy(&self) -> f64 {
767        let n = self.spins.len();
768        let mut e = 0.0;
769        for i in 0..n {
770            for j in 0..n {
771                let s = self.spins[i][j] as f64;
772                let s_right = self.spins[i][(j + 1) % n] as f64;
773                let s_down = self.spins[(i + 1) % n][j] as f64;
774                e -= self.j_coupling * s * (s_right + s_down);
775            }
776        }
777        e
778    }
779    /// Magnetization per site |Σ s_i| / N²
780    pub fn magnetization(&self) -> f64 {
781        let n = self.spins.len();
782        let total: i64 = self
783            .spins
784            .iter()
785            .flat_map(|row| row.iter())
786            .map(|&s| s as i64)
787            .sum();
788        (total.abs() as f64) / ((n * n) as f64)
789    }
790    /// One Metropolis-Hastings step: propose flipping a random spin
791    pub fn metropolis_step(&mut self, rng: &mut u64) {
792        let n = self.spins.len();
793        let r1 = Self::lcg_next(rng);
794        let r2 = Self::lcg_next(rng);
795        let r3 = Self::lcg_next(rng);
796        let i = (r1 * n as f64) as usize % n;
797        let j = (r2 * n as f64) as usize % n;
798        let s = self.spins[i][j] as f64;
799        let neighbors: f64 = [
800            self.spins[(i + n - 1) % n][j] as f64,
801            self.spins[(i + 1) % n][j] as f64,
802            self.spins[i][(j + n - 1) % n] as f64,
803            self.spins[i][(j + 1) % n] as f64,
804        ]
805        .iter()
806        .sum();
807        let delta_e = 2.0 * self.j_coupling * s * neighbors;
808        let accept = if delta_e <= 0.0 {
809            true
810        } else {
811            let prob = (-delta_e / (BOLTZMANN_K * self.temperature)).exp();
812            r3 < prob
813        };
814        if accept {
815            self.spins[i][j] = -self.spins[i][j];
816        }
817    }
818    /// Linear congruential generator returning a float in [0, 1)
819    fn lcg_next(state: &mut u64) -> f64 {
820        *state = state
821            .wrapping_mul(6364136223846793005)
822            .wrapping_add(1442695040888963407);
823        (*state >> 33) as f64 / (u32::MAX as f64 + 1.0)
824    }
825}
826/// Table of critical exponents for common universality classes
827pub struct CriticalExponentTable {
828    pub entries: Vec<CriticalExponents>,
829}
830impl CriticalExponentTable {
831    /// Build the standard table of universality classes
832    pub fn standard() -> Self {
833        Self {
834            entries: vec![
835                CriticalExponents {
836                    name: "2D Ising",
837                    dimension: 2,
838                    alpha: 0.0,
839                    beta: 0.125,
840                    gamma: 1.75,
841                    delta: 15.0,
842                    nu: 1.0,
843                    eta: 0.25,
844                },
845                CriticalExponents {
846                    name: "3D Ising",
847                    dimension: 3,
848                    alpha: 0.110,
849                    beta: 0.326,
850                    gamma: 1.237,
851                    delta: 4.789,
852                    nu: 0.630,
853                    eta: 0.036,
854                },
855                CriticalExponents {
856                    name: "3D XY",
857                    dimension: 3,
858                    alpha: -0.013,
859                    beta: 0.346,
860                    gamma: 1.316,
861                    delta: 4.780,
862                    nu: 0.671,
863                    eta: 0.038,
864                },
865                CriticalExponents {
866                    name: "3D Heisenberg",
867                    dimension: 3,
868                    alpha: -0.122,
869                    beta: 0.365,
870                    gamma: 1.386,
871                    delta: 4.803,
872                    nu: 0.707,
873                    eta: 0.033,
874                },
875                CriticalExponents {
876                    name: "Mean Field",
877                    dimension: 4,
878                    alpha: 0.0,
879                    beta: 0.5,
880                    gamma: 1.0,
881                    delta: 3.0,
882                    nu: 0.5,
883                    eta: 0.0,
884                },
885                CriticalExponents {
886                    name: "2D Potts q=3",
887                    dimension: 2,
888                    alpha: 0.333,
889                    beta: 0.111,
890                    gamma: 1.444,
891                    delta: 14.0,
892                    nu: 0.833,
893                    eta: 0.148,
894                },
895            ],
896        }
897    }
898    /// Find a universality class by name
899    pub fn find(&self, name: &str) -> Option<&CriticalExponents> {
900        self.entries.iter().find(|e| e.name == name)
901    }
902    /// Validate all scaling relations in the table
903    pub fn validate_scaling_relations(&self) -> Vec<(&'static str, bool, bool, bool)> {
904        self.entries
905            .iter()
906            .map(|e| {
907                (
908                    e.name,
909                    e.check_widom(),
910                    e.check_rushbrooke(),
911                    e.check_fisher(),
912                )
913            })
914            .collect()
915    }
916}
917/// Classical ideal gas in a box
918pub struct IdealGas {
919    pub n_particles: u64,
920    pub temperature: f64,
921    pub volume: f64,
922}
923impl IdealGas {
924    pub fn new(n: u64, t: f64, v: f64) -> Self {
925        Self {
926            n_particles: n,
927            temperature: t,
928            volume: v,
929        }
930    }
931    /// Pressure P = N k_B T / V
932    pub fn pressure(&self) -> f64 {
933        (self.n_particles as f64) * BOLTZMANN_K * self.temperature / self.volume
934    }
935    /// Mean kinetic energy per particle ⟨E⟩ = (3/2) k_B T
936    pub fn mean_kinetic_energy(&self) -> f64 {
937        1.5 * BOLTZMANN_K * self.temperature
938    }
939    /// RMS speed sqrt(3 k_B T / m) for particles of mass m (kg)
940    pub fn rms_speed(&self, mass: f64) -> f64 {
941        (3.0 * BOLTZMANN_K * self.temperature / mass).sqrt()
942    }
943    /// Sackur-Tetrode entropy approximation (monatomic ideal gas)
944    ///
945    /// S/N k_B ≈ ln(V/N * (4π m ⟨E⟩ / (3 N h²))^(3/2)) + 5/2
946    /// Uses m = proton mass (1.67e-27 kg) as default
947    pub fn entropy(&self) -> f64 {
948        let n = self.n_particles as f64;
949        let m = 1.6726e-27_f64;
950        let mean_e = self.mean_kinetic_energy();
951        let lambda_arg = 4.0 * std::f64::consts::PI * m * mean_e / (3.0 * n * PLANCK_H * PLANCK_H);
952        if lambda_arg <= 0.0 {
953            return 0.0;
954        }
955        BOLTZMANN_K * n * ((self.volume / n) * lambda_arg.powf(1.5) + 2.5)
956    }
957}
958/// 1D Ising model: exact solution via transfer matrix
959///
960/// Hamiltonian: H = -J Σ s_i s_{i+1} - h Σ s_i  (periodic BC)
961pub struct IsingModel1D {
962    /// Number of sites
963    pub n_sites: usize,
964    /// Exchange coupling J (in units of energy)
965    pub j_coupling: f64,
966    /// External magnetic field h (in units of energy)
967    pub h_field: f64,
968    /// Temperature T (in Kelvin; uses dimensionless β = 1/(k_B T))
969    pub temperature: f64,
970}
971impl IsingModel1D {
972    /// Create a 1D Ising model
973    pub fn new(n: usize, j: f64, h: f64, temp: f64) -> Self {
974        Self {
975            n_sites: n,
976            j_coupling: j,
977            h_field: h,
978            temperature: temp,
979        }
980    }
981    /// Inverse temperature β = 1/(k_B T)
982    pub fn beta(&self) -> f64 {
983        1.0 / (BOLTZMANN_K * self.temperature)
984    }
985    /// Transfer matrix eigenvalues λ± for the 1D Ising model.
986    ///
987    /// The 2×2 transfer matrix T with elements T_{s,s'} = exp(βJ s s' + βh(s+s')/2)
988    /// has eigenvalues:
989    ///   λ± = exp(βJ) [cosh(βh) ± sqrt(sinh²(βh) + exp(-4βJ))]
990    pub fn eigenvalues(&self) -> (f64, f64) {
991        let b = self.beta();
992        let bj = b * self.j_coupling;
993        let bh = b * self.h_field;
994        let exp_bj = bj.exp();
995        let cosh_bh = bh.cosh();
996        let sinh_bh = bh.sinh();
997        let disc = sinh_bh * sinh_bh + (-4.0 * bj).exp();
998        let sqrt_disc = disc.sqrt();
999        let lambda_plus = exp_bj * (cosh_bh + sqrt_disc);
1000        let lambda_minus = exp_bj * (cosh_bh - sqrt_disc);
1001        (lambda_plus, lambda_minus)
1002    }
1003    /// Exact partition function Z = λ+^N + λ-^N  (periodic BC)
1004    pub fn partition_function(&self) -> f64 {
1005        let (lp, lm) = self.eigenvalues();
1006        let n = self.n_sites as f64;
1007        lp.powf(n) + lm.powf(n)
1008    }
1009    /// Free energy per site f = -k_B T / N * ln Z
1010    pub fn free_energy_per_site(&self) -> f64 {
1011        let z = self.partition_function();
1012        if z <= 0.0 {
1013            return f64::INFINITY;
1014        }
1015        -BOLTZMANN_K * self.temperature * z.ln() / (self.n_sites as f64)
1016    }
1017    /// Zero-field (h=0) partition function: Z = (2 cosh(βJ))^N
1018    pub fn zero_field_partition_function(&self) -> f64 {
1019        let b = self.beta();
1020        let bj = b * self.j_coupling;
1021        (2.0 * bj.cosh()).powf(self.n_sites as f64)
1022    }
1023    /// Magnetization per site ⟨m⟩ = (1/N β) ∂ln Z/∂h  (numerical)
1024    pub fn magnetization_per_site(&self) -> f64 {
1025        let dh = self.j_coupling.abs() * 1e-6 + 1e-30;
1026        let z_plus = IsingModel1D::new(
1027            self.n_sites,
1028            self.j_coupling,
1029            self.h_field + dh,
1030            self.temperature,
1031        )
1032        .partition_function();
1033        let z_minus = IsingModel1D::new(
1034            self.n_sites,
1035            self.j_coupling,
1036            self.h_field - dh,
1037            self.temperature,
1038        )
1039        .partition_function();
1040        let z = self.partition_function();
1041        if z <= 0.0 {
1042            return 0.0;
1043        }
1044        (z_plus - z_minus) / (2.0 * dh * self.beta() * z)
1045    }
1046    /// Susceptibility per site χ = ∂⟨m⟩/∂h  (numerical second derivative)
1047    pub fn susceptibility_per_site(&self) -> f64 {
1048        let dh = self.j_coupling.abs() * 1e-4 + 1e-30;
1049        let m_plus = IsingModel1D::new(
1050            self.n_sites,
1051            self.j_coupling,
1052            self.h_field + dh,
1053            self.temperature,
1054        )
1055        .magnetization_per_site();
1056        let m_minus = IsingModel1D::new(
1057            self.n_sites,
1058            self.j_coupling,
1059            self.h_field - dh,
1060            self.temperature,
1061        )
1062        .magnetization_per_site();
1063        (m_plus - m_minus) / (2.0 * dh)
1064    }
1065}