Skip to main content

renormalization_group/
lib.rs

1//! # Renormalization Group for Multi-Scale Fleet Analysis
2//!
3//! This library applies the renormalization group (RG) from statistical physics
4//! to multi-scale fleet analysis. Agents are coarse-grained into block variables
5//! at increasing scale; the library tracks how constraint parameters flow under
6//! this transformation.
7//!
8//! ## Modules
9//!
10//! - [`block_spin`] — Coarse-graining transformations
11//! - [`flow`] — RG flow equations for coupling constants
12//! - [`fixed_point`] — Fixed-point detection and stability analysis
13//! - [`critical_surface`] — Critical manifolds separating fleet phases
14//! - [`universality`] — Universality classes of fleet behavior
15//! - [`scaling`] — Finite-size scaling and scaling relations
16
17pub mod block_spin;
18pub mod critical_surface;
19pub mod fixed_point;
20pub mod flow;
21pub mod scaling;
22pub mod universality;
23
24pub use block_spin::*;
25pub use critical_surface::*;
26pub use fixed_point::*;
27pub use flow::*;
28pub use scaling::{
29    scaling_collapse, finite_size_order_parameter, finite_size_susceptibility,
30    finite_size_correlation_length, binder_cumulant, verify_hyperscaling,
31    find_tc_from_crossing, correlation_function, extract_correlation_length,
32    ScalingResult,
33};
34pub use universality::*;
35
36/// Gaussian elimination solver for dense linear algebra.
37/// Implemented from scratch with no external math dependencies.
38pub mod linalg {
39    /// Solve a linear system Ax = b via Gaussian elimination with partial pivoting.
40    /// Returns `None` if the system is singular.
41    pub fn solve(a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
42        let n = b.len();
43        if a.len() != n {
44            return None;
45        }
46        for row in a {
47            if row.len() != n {
48                return None;
49            }
50        }
51
52        // Build augmented matrix [A|b]
53        let mut aug: Vec<Vec<f64>> = Vec::with_capacity(n);
54        for i in 0..n {
55            let mut row = a[i].clone();
56            row.push(b[i]);
57            aug.push(row);
58        }
59
60        // Forward elimination with partial pivoting
61        for col in 0..n {
62            // Find pivot
63            let mut max_row = col;
64            let mut max_val = aug[col][col].abs();
65            for row in (col + 1)..n {
66                let val = aug[row][col].abs();
67                if val > max_val {
68                    max_val = val;
69                    max_row = row;
70                }
71            }
72            if max_val < 1e-14 {
73                return None; // singular
74            }
75            // Swap rows
76            aug.swap(col, max_row);
77
78            // Eliminate below
79            let pivot = aug[col][col];
80            for row in (col + 1)..n {
81                let factor = aug[row][col] / pivot;
82                for j in col..=(n) {
83                    aug[row][j] -= factor * aug[col][j];
84                }
85            }
86        }
87
88        // Back substitution
89        let mut x = vec![0.0; n];
90        for i in (0..n).rev() {
91            if aug[i][i].abs() < 1e-14 {
92                return None;
93            }
94            let mut sum = aug[i][n];
95            for j in (i + 1)..n {
96                sum -= aug[i][j] * x[j];
97            }
98            x[i] = sum / aug[i][i];
99        }
100        Some(x)
101    }
102
103    /// Compute eigenvalues of a real symmetric matrix using the QR algorithm
104    /// with Wilkinson shifts. Returns eigenvalues sorted by absolute value descending.
105    pub fn eigenvalues_symmetric(mat: &[Vec<f64>]) -> Vec<f64> {
106        let n = mat.len();
107        if n == 0 {
108            return vec![];
109        }
110        if n == 1 {
111            return vec![mat[0][0]];
112        }
113
114        // Use Jacobi eigenvalue algorithm for robustness
115        let mut a: Vec<Vec<f64>> = mat.to_vec();
116        let max_iter = 100 * n * n;
117        let tol = 1e-12;
118
119        for _ in 0..max_iter {
120            // Find largest off-diagonal element
121            let mut max_val = 0.0;
122            let mut p = 0;
123            let mut q = 1;
124            for i in 0..n {
125                for j in (i + 1)..n {
126                    if a[i][j].abs() > max_val {
127                        max_val = a[i][j].abs();
128                        p = i;
129                        q = j;
130                    }
131                }
132            }
133            if max_val < tol {
134                break;
135            }
136
137            // Compute rotation angle
138            let app = a[p][p];
139            let aqq = a[q][q];
140            let apq = a[p][q];
141
142            let theta = if (app - aqq).abs() < 1e-15 {
143                std::f64::consts::FRAC_PI_4
144            } else {
145                0.5 * (2.0 * apq / (app - aqq)).atan()
146            };
147            let c = theta.cos();
148            let s = theta.sin();
149
150            // Apply Jacobi rotation
151            // Update rows/cols p and q
152            let mut new_ap = vec![0.0; n];
153            let mut new_aq = vec![0.0; n];
154            for i in 0..n {
155                if i != p && i != q {
156                    new_ap[i] = c * a[i][p] + s * a[i][q];
157                    new_aq[i] = -s * a[i][p] + c * a[i][q];
158                }
159            }
160            new_ap[p] = c * c * app + 2.0 * s * c * apq + s * s * aqq;
161            new_aq[q] = s * s * app - 2.0 * s * c * apq + c * c * aqq;
162            new_ap[q] = 0.0;
163            new_aq[p] = 0.0;
164
165            for i in 0..n {
166                a[i][p] = new_ap[i];
167                a[p][i] = new_ap[i];
168                a[i][q] = new_aq[i];
169                a[q][i] = new_aq[i];
170            }
171            a[p][q] = 0.0;
172            a[q][p] = 0.0;
173        }
174
175        let mut eigenvals: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
176        eigenvals.sort_by(|a, b| b.abs().partial_cmp(&a.abs()).unwrap_or(std::cmp::Ordering::Equal));
177        eigenvals
178    }
179
180    /// Compute the Jacobian of a vector function numerically.
181    /// `f` takes a parameter vector and returns a vector of the same dimension.
182    /// Uses central differences with step size `h`.
183    pub fn jacobian(
184        f: &dyn Fn(&[f64]) -> Vec<f64>,
185        x: &[f64],
186        h: f64,
187    ) -> Vec<Vec<f64>> {
188        let n = x.len();
189        let fx = f(x);
190        let m = fx.len();
191        let mut jac = vec![vec![0.0; n]; m];
192
193        for j in 0..n {
194            let mut xp = x.to_vec();
195            let mut xm = x.to_vec();
196            xp[j] += h;
197            xm[j] -= h;
198            let fp = f(&xp);
199            let fm = f(&xm);
200            for i in 0..m {
201                jac[i][j] = (fp[i] - fm[i]) / (2.0 * h);
202            }
203        }
204        jac
205    }
206
207    /// Matrix-vector multiplication
208    pub fn mat_vec_mul(a: &[Vec<f64>], x: &[f64]) -> Vec<f64> {
209        a.iter()
210            .map(|row| row.iter().zip(x.iter()).map(|(a, b)| a * b).sum())
211            .collect()
212    }
213
214    /// Transpose a matrix
215    pub fn transpose(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
216        if a.is_empty() {
217            return vec![];
218        }
219        let m = a.len();
220        let n = a[0].len();
221        (0..n)
222            .map(|j| (0..m).map(|i| a[i][j]).collect())
223            .collect()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::linalg::*;
230
231    #[test]
232    fn test_gaussian_solve_identity() {
233        let a = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
234        let b = vec![3.0, 4.0];
235        let x = solve(&a, &b).unwrap();
236        assert!((x[0] - 3.0).abs() < 1e-10);
237        assert!((x[1] - 4.0).abs() < 1e-10);
238    }
239
240    #[test]
241    fn test_gaussian_solve_3x3() {
242        let a = vec![vec![2.0, 1.0, -1.0], vec![-3.0, -1.0, 2.0], vec![-2.0, 1.0, 2.0]];
243        let b = vec![8.0, -11.0, -3.0];
244        let x = solve(&a, &b).unwrap();
245        assert!((x[0] - 2.0).abs() < 1e-10);
246        assert!((x[1] - 3.0).abs() < 1e-10);
247        assert!((x[2] - (-1.0)).abs() < 1e-10);
248    }
249
250    #[test]
251    fn test_gaussian_singular() {
252        let a = vec![vec![1.0, 2.0], vec![2.0, 4.0]];
253        let b = vec![1.0, 2.0];
254        assert!(solve(&a, &b).is_none());
255    }
256
257    #[test]
258    fn test_eigenvalues_2x2() {
259        let mat = vec![vec![4.0, 1.0], vec![1.0, 3.0]];
260        let eigs = eigenvalues_symmetric(&mat);
261        // Eigenvalues of [[4,1],[1,3]] are (7 ± sqrt(5))/2 ≈ 4.618, 2.382
262        let e1 = (7.0 + 5.0_f64.sqrt()) / 2.0;
263        let e2 = (7.0 - 5.0_f64.sqrt()) / 2.0;
264        assert!((eigs[0] - e1).abs() < 0.01 || (eigs[0] - e2).abs() < 0.01);
265        assert!((eigs[1] - e1).abs() < 0.01 || (eigs[1] - e2).abs() < 0.01);
266    }
267}