Skip to main content

renormalization_group/
scaling.rs

1//! # Finite-Size Scaling and Scaling Relations
2//!
3//! Critical exponents, scaling collapse, and finite-size corrections
4//! for bounded fleets.
5
6use serde::{Deserialize, Serialize};
7
8/// A critical exponent.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CriticalExponent {
11    /// Name of the exponent
12    pub name: String,
13    /// Numerical value
14    pub value: f64,
15}
16
17impl CriticalExponent {
18    pub fn new(name: &str, value: f64) -> Self {
19        Self {
20            name: name.into(),
21            value,
22        }
23    }
24}
25
26/// Scaling function result.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ScalingResult {
29    /// Scaled x values
30    pub x_scaled: Vec<f64>,
31    /// Scaled y values
32    pub y_scaled: Vec<f64>,
33    /// Exponents used for scaling
34    pub exponents_used: Vec<CriticalExponent>,
35}
36
37/// Scaling collapse: given data at different system sizes, collapse
38/// onto a universal curve using finite-size scaling.
39///
40/// For an observable O(t, L) where t = (T - Tc)/Tc and L is system size:
41///   O(t, L) = L^(γ/ν) * f(t * L^(1/ν))
42///
43/// This function computes the scaled coordinates.
44pub fn scaling_collapse(
45    temperatures: &[f64],
46    observables: &[f64],
47    system_sizes: &[usize],
48    tc: f64,
49    nu: f64,
50    gamma_over_nu: f64,
51) -> ScalingResult {
52    let mut x_scaled = Vec::new();
53    let mut y_scaled = Vec::new();
54
55    for (i, &t) in temperatures.iter().enumerate() {
56        if i >= observables.len() || i >= system_sizes.len() {
57            break;
58        }
59        let l = system_sizes[i] as f64;
60        let reduced_t = (t - tc) / tc;
61
62        // x = t * L^(1/ν)
63        let x = reduced_t * l.powf(1.0 / nu);
64        // y = O / L^(γ/ν)
65        let y = observables[i] / l.powf(gamma_over_nu);
66
67        x_scaled.push(x);
68        y_scaled.push(y);
69    }
70
71    ScalingResult {
72        x_scaled,
73        y_scaled,
74        exponents_used: vec![
75            CriticalExponent::new("nu", nu),
76            CriticalExponent::new("gamma/nu", gamma_over_nu),
77        ],
78    }
79}
80
81/// Compute the finite-size scaling form of the order parameter.
82/// M(L) ~ L^(-β/ν) at criticality.
83pub fn finite_size_order_parameter(
84    system_size: usize,
85    beta: f64,
86    nu: f64,
87) -> f64 {
88    let l = system_size as f64;
89    l.powf(-beta / nu)
90}
91
92/// Compute the finite-size scaling form of the susceptibility.
93/// χ(L) ~ L^(γ/ν) at criticality.
94pub fn finite_size_susceptibility(
95    system_size: usize,
96    gamma: f64,
97    nu: f64,
98) -> f64 {
99    let l = system_size as f64;
100    l.powf(gamma / nu)
101}
102
103/// Compute the finite-size scaling form of the correlation length.
104/// ξ(L) ~ L at criticality (trivially bounded by system size).
105pub fn finite_size_correlation_length(system_size: usize) -> f64 {
106    system_size as f64
107}
108
109/// Compute the Binder cumulant for finite-size analysis.
110/// U_L = 1 - <m^4> / (3 * <m^2>^2)
111/// At criticality, U_L approaches a universal value independent of L.
112pub fn binder_cumulant(m2: f64, m4: f64) -> f64 {
113    if m2.abs() < 1e-15 {
114        return 0.0;
115    }
116    1.0 - m4 / (3.0 * m2 * m2)
117}
118
119/// Verify hyperscaling: 2 - α = dν (valid for d < 4).
120pub fn verify_hyperscaling(d: usize, alpha: f64, nu: f64) -> bool {
121    ((2.0 - alpha) - d as f64 * nu).abs() < 0.05
122}
123
124/// Extract critical temperature from finite-size data using Binder cumulant crossing.
125/// The Binder cumulant curves for different L cross at Tc.
126pub fn find_tc_from_crossing(
127    temperatures: &[f64],
128    binder_l1: &[f64],
129    binder_l2: &[f64],
130) -> Option<f64> {
131    if temperatures.len() < 2 || binder_l1.len() != temperatures.len() || binder_l2.len() != temperatures.len() {
132        return None;
133    }
134
135    // Find where binder_l1 and binder_l2 cross
136    for i in 0..temperatures.len() - 1 {
137        let diff1 = binder_l1[i] - binder_l2[i];
138        let diff2 = binder_l1[i + 1] - binder_l2[i + 1];
139
140        if diff1 * diff2 < 0.0 {
141            // Linear interpolation
142            let t = diff1.abs() / (diff1.abs() + diff2.abs());
143            let tc = temperatures[i] + t * (temperatures[i + 1] - temperatures[i]);
144            return Some(tc);
145        }
146    }
147    None
148}
149
150/// Compute the correlation function for a 1D system.
151/// C(r) = <s_i s_{i+r}> - <s_i>^2
152pub fn correlation_function(spins: &[f64], max_r: usize) -> Vec<f64> {
153    let n = spins.len();
154    let mean: f64 = spins.iter().sum::<f64>() / n as f64;
155    let mut corr = Vec::with_capacity(max_r);
156
157    for r in 0..max_r {
158        if r >= n {
159            corr.push(0.0);
160            continue;
161        }
162        let mut sum = 0.0;
163        let mut count = 0;
164        for i in 0..(n - r) {
165            sum += spins[i] * spins[i + r];
166            count += 1;
167        }
168        let c = if count > 0 { sum / count as f64 - mean * mean } else { 0.0 };
169        corr.push(c);
170    }
171    corr
172}
173
174/// Extract correlation length from a correlation function.
175/// Fits C(r) ~ exp(-r/ξ) and returns ξ.
176pub fn extract_correlation_length(corr: &[f64]) -> f64 {
177    if corr.len() < 2 {
178        return 0.0;
179    }
180    // Use first two nonzero points
181    let mut r0 = 0;
182    while r0 < corr.len() && corr[r0].abs() < 1e-15 {
183        r0 += 1;
184    }
185    if r0 >= corr.len() - 1 {
186        return 0.0;
187    }
188    let c0 = corr[r0].abs();
189    let r1 = r0 + 1;
190    let c1 = corr[r1].abs();
191
192    if c0 < 1e-15 || c1 < 1e-15 {
193        return 1.0; // Very short correlation
194    }
195    // ln(C(r+1)/C(r)) = -1/ξ
196    let ratio = c1 / c0;
197    if ratio <= 0.0 {
198        return 1.0;
199    }
200    let xi = -1.0 / ratio.ln();
201    if xi.is_nan() || xi.is_infinite() || xi < 0.0 {
202        return f64::MAX;
203    }
204    xi
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_scaling_collapse_mean_field() {
213        // Mean field: ν=0.5, γ=1.0, γ/ν=2.0
214        let temps = vec![0.9, 0.95, 1.05, 1.1];
215        let obs = vec![100.0, 400.0, 400.0, 100.0];
216        let sizes = vec![10, 20, 20, 10];
217        let result = scaling_collapse(&temps, &obs, &sizes, 1.0, 0.5, 2.0);
218        assert_eq!(result.x_scaled.len(), 4);
219        assert_eq!(result.y_scaled.len(), 4);
220    }
221
222    #[test]
223    fn test_finite_size_order_parameter_2d_ising() {
224        // 2D Ising: β = 1/8, ν = 1
225        // M(L) ~ L^(-1/8)
226        let m10 = finite_size_order_parameter(10, 1.0 / 8.0, 1.0);
227        let m100 = finite_size_order_parameter(100, 1.0 / 8.0, 1.0);
228        // Ratio should be (100/10)^(-1/8) = 10^(-1/8) ≈ 0.7499
229        let ratio = m100 / m10;
230        let expected = 10.0_f64.powf(-1.0 / 8.0);
231        assert!((ratio - expected).abs() < 0.01);
232    }
233
234    #[test]
235    fn test_finite_size_susceptibility_mean_field() {
236        // Mean field: γ=1, ν=0.5, γ/ν=2
237        let chi10 = finite_size_susceptibility(10, 1.0, 0.5);
238        let chi100 = finite_size_susceptibility(100, 1.0, 0.5);
239        // Ratio: (100/10)^2 = 100
240        let ratio = chi100 / chi10;
241        assert!((ratio - 100.0).abs() < 0.1);
242    }
243
244    #[test]
245    fn test_binder_cumulant_uniform() {
246        // All spins aligned: m = ±1, m^2 = 1, m^4 = 1
247        let u = binder_cumulant(1.0, 1.0);
248        assert!((u - (1.0 - 1.0 / 3.0)).abs() < 1e-10);
249    }
250
251    #[test]
252    fn test_binder_cumulant_gaussian() {
253        // Gaussian distribution: U = 0 for Gaussian
254        // <m^4> = 3<m^2>^2 for Gaussian → U = 1 - 3<m^2>^2/(3<m^2>^2) = 0
255        let u = binder_cumulant(1.0, 3.0);
256        assert!(u.abs() < 1e-10);
257    }
258
259    #[test]
260    fn test_hyperscaling_2d_ising() {
261        // d=2, α=0, ν=1: 2-0=2, 2*1=2 ✓
262        assert!(verify_hyperscaling(2, 0.0, 1.0));
263    }
264
265    #[test]
266    fn test_hyperscaling_mean_field() {
267        // d=4, α=0, ν=0.5: 2-0=2, 4*0.5=2 ✓
268        assert!(verify_hyperscaling(4, 0.0, 0.5));
269    }
270
271    #[test]
272    fn test_correlation_function_constant() {
273        let spins = vec![1.0; 100];
274        let corr = correlation_function(&spins, 10);
275        // C(0) should be <s^2> - <s>^2 = 1 - 1 = 0
276        assert!(corr[0].abs() < 1e-10);
277    }
278
279    #[test]
280    fn test_correlation_function_alternating() {
281        let spins: Vec<f64> = (0..100).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect();
282        let corr = correlation_function(&spins, 5);
283        // C(1) should be negative (anticorrelated)
284        assert!(corr[1] < 0.0);
285        // C(2) should be positive
286        assert!(corr[2] > 0.0);
287    }
288
289    #[test]
290    fn test_find_tc_from_crossing() {
291        // binder_l1 increases with T, binder_l2 decreases with T → they cross
292        let temps = vec![0.8, 0.9, 1.0, 1.1, 1.2];
293        let binder_l1 = vec![0.3, 0.45, 0.55, 0.65, 0.75]; // increasing
294        let binder_l2 = vec![0.7, 0.55, 0.45, 0.35, 0.25]; // decreasing
295        // They cross between T=0.9 and T=1.0
296        let tc = find_tc_from_crossing(&temps, &binder_l1, &binder_l2);
297        assert!(tc.is_some());
298        let tc = tc.unwrap();
299        assert!((tc - 0.95).abs() < 0.1);
300    }
301
302    #[test]
303    fn test_extract_correlation_length() {
304        // Exponentially decaying correlation with ξ = 2.0
305        let corr: Vec<f64> = (0..20).map(|r| (-r as f64 / 2.0).exp()).collect();
306        let xi = extract_correlation_length(&corr);
307        assert!((xi - 2.0).abs() < 0.5);
308    }
309
310    #[test]
311    fn test_finite_size_correlation_length() {
312        assert_eq!(finite_size_correlation_length(100), 100.0);
313    }
314}