Skip to main content

renormalization_group/
fixed_point.rs

1//! # Fixed-Point Detection and Stability Analysis
2//!
3//! Linearize the RG flow around fixed points. Eigenvalues of the
4//! linearized flow determine relevant vs irrelevant directions.
5
6use crate::flow::FlowEquation;
7use crate::linalg;
8use serde::{Deserialize, Serialize};
9
10/// A fixed point of the RG flow.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FixedPoint {
13    /// Location in parameter space
14    pub location: Vec<f64>,
15    /// Eigenvalues of the stability matrix at this fixed point
16    pub eigenvalues: Vec<f64>,
17    /// Whether each direction is stable (irrelevant = true, relevant = false)
18    pub stable: Vec<bool>,
19    /// Names/labels for the parameters
20    pub param_names: Vec<String>,
21}
22
23impl FixedPoint {
24    /// Classify a direction as relevant (eigenvalue > 1), irrelevant (< 1),
25    /// or marginal (≈ 1).
26    pub fn classify_direction(&self, index: usize) -> DirectionClass {
27        if index >= self.eigenvalues.len() {
28            return DirectionClass::Marginal;
29        }
30        let ev = self.eigenvalues[index];
31        if ev > 1.0 + 1e-6 {
32            DirectionClass::Relevant
33        } else if ev < 1.0 - 1e-6 {
34            DirectionClass::Irrelevant
35        } else {
36            DirectionClass::Marginal
37        }
38    }
39
40    /// Count the number of relevant directions.
41    pub fn n_relevant(&self) -> usize {
42        self.eigenvalues.iter().filter(|&&ev| ev > 1.0 + 1e-6).count()
43    }
44
45    /// Count the number of irrelevant directions.
46    pub fn n_irrelevant(&self) -> usize {
47        self.eigenvalues.iter().filter(|&&ev| ev < 1.0 - 1e-6).count()
48    }
49
50    /// The number of relevant directions equals the codimension of the
51    /// critical surface.
52    pub fn codimension(&self) -> usize {
53        self.n_relevant()
54    }
55}
56
57/// Classification of RG flow directions.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum DirectionClass {
60    /// Eigenvalue > 1: grows under RG (destabilizes at large scale)
61    Relevant,
62    /// Eigenvalue < 1: shrinks under RG (irrelevant at large scale)
63    Irrelevant,
64    /// Eigenvalue ≈ 1: needs higher-order analysis
65    Marginal,
66}
67
68/// Find a fixed point by iterating the flow equation.
69/// Returns None if the iteration doesn't converge.
70pub fn find_fixed_point(
71    flow_eq: &dyn FlowEquation,
72    initial_guess: &[f64],
73    max_iter: usize,
74    tolerance: f64,
75) -> Option<FixedPoint> {
76    let mut current = initial_guess.to_vec();
77
78    for _ in 0..max_iter {
79        let next = flow_eq.flow_step(&current, 0);
80        let delta: f64 = next
81            .iter()
82            .zip(current.iter())
83            .map(|(n, c)| (n - c).powi(2))
84            .sum::<f64>()
85            .sqrt();
86
87        if delta < tolerance {
88            // Found fixed point; compute stability matrix
89            let jac = linalg::jacobian(
90                &|p| flow_eq.flow_step(p, 0),
91                &current,
92                1e-7,
93            );
94            // Symmetrize for eigenvalue computation
95            let n = jac.len();
96            let mut sym_jac = jac.clone();
97            for i in 0..n {
98                for j in 0..n {
99                    sym_jac[i][j] = 0.5 * (jac[i][j] + jac.get(j).map(|r| r[i]).unwrap_or(0.0));
100                    sym_jac[j][i] = sym_jac[i][j];
101                }
102            }
103            let eigenvalues = linalg::eigenvalues_symmetric(&sym_jac);
104            let stable: Vec<bool> = eigenvalues.iter().map(|&ev| ev.abs() < 1.0).collect();
105            let param_names = (0..current.len())
106                .map(|i| format!("param_{}", i))
107                .collect();
108
109            return Some(FixedPoint {
110                location: current,
111                eigenvalues,
112                stable,
113                param_names,
114            });
115        }
116        current = next;
117    }
118    None
119}
120
121/// Analyze stability around a known fixed point.
122/// Computes eigenvalues and classifies directions.
123pub fn analyze_stability(
124    flow_eq: &dyn FlowEquation,
125    fixed_point: &[f64],
126) -> FixedPoint {
127    let jac = linalg::jacobian(
128        &|p| flow_eq.flow_step(p, 0),
129        fixed_point,
130        1e-7,
131    );
132    let n = jac.len();
133    let mut sym_jac = jac.clone();
134    for i in 0..n {
135        for j in 0..n {
136            sym_jac[i][j] = 0.5 * (jac[i][j] + jac.get(j).map(|r| r[i]).unwrap_or(0.0));
137            sym_jac[j][i] = sym_jac[i][j];
138        }
139    }
140    let eigenvalues = linalg::eigenvalues_symmetric(&sym_jac);
141    let stable: Vec<bool> = eigenvalues.iter().map(|&ev| ev.abs() < 1.0).collect();
142    let param_names = (0..fixed_point.len())
143        .map(|i| format!("param_{}", i))
144        .collect();
145
146    FixedPoint {
147        location: fixed_point.to_vec(),
148        eigenvalues,
149        stable,
150        param_names,
151    }
152}
153
154/// Test if a given parameter vector is near a fixed point.
155pub fn is_near_fixed_point(
156    flow_eq: &dyn FlowEquation,
157    params: &[f64],
158    tolerance: f64,
159) -> bool {
160    let next = flow_eq.flow_step(params, 0);
161    let delta: f64 = next
162        .iter()
163        .zip(params.iter())
164        .map(|(n, c)| (n - c).powi(2))
165        .sum::<f64>()
166        .sqrt();
167    delta < tolerance
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::flow::{GaussianFlow, Ising1DFlow, RGFlow};
174
175    #[test]
176    fn test_ising_1d_zero_fixed_point() {
177        let flow_eq = Ising1DFlow::new(2.0);
178        let fp = find_fixed_point(&flow_eq, &[0.1, 0.0], 100, 1e-10);
179        assert!(fp.is_some());
180        let fp = fp.unwrap();
181        assert!(fp.location[0].abs() < 0.01);
182    }
183
184    #[test]
185    fn test_gaussian_zero_fixed_point() {
186        // The origin is trivially a fixed point (maps to itself)
187        let flow_eq = GaussianFlow::new(3, 2.0);
188        assert!(is_near_fixed_point(&flow_eq, &[0.0, 0.0], 1e-10));
189        // Also verify via direct stability analysis
190        let fp = analyze_stability(&flow_eq, &[0.0, 0.0]);
191        assert!(fp.location[0].abs() < 1e-10);
192        assert!(fp.location[1].abs() < 1e-10);
193    }
194
195    #[test]
196    fn test_gaussian_r_is_relevant() {
197        // At the Gaussian fixed point, r has eigenvalue b^2 = 4 (relevant)
198        let flow_eq = GaussianFlow::new(3, 2.0);
199        let fp = analyze_stability(&flow_eq, &[0.0, 0.0]);
200        // Both eigenvalues should be > 1 (r: b^2=4, u: b^(4-d)=b^1=2)
201        let max_eigenvalue = fp.eigenvalues.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
202        assert!(max_eigenvalue > 1.0);
203        assert!(fp.n_relevant() >= 1);
204    }
205
206    #[test]
207    fn test_uniform_fleet_trivial_fixed_point() {
208        // Uniform fleet (all identical agents) → all parameters equal → trivial fixed point
209        let flow_eq = GaussianFlow::new(3, 2.0);
210        assert!(is_near_fixed_point(&flow_eq, &[0.0, 0.0], 1e-10));
211    }
212
213    #[test]
214    fn test_direction_classification() {
215        let fp = FixedPoint {
216            location: vec![0.0, 0.0],
217            eigenvalues: vec![4.0, 0.5],
218            stable: vec![false, true],
219            param_names: vec!["r".into(), "u".into()],
220        };
221        assert_eq!(fp.classify_direction(0), DirectionClass::Relevant);
222        assert_eq!(fp.classify_direction(1), DirectionClass::Irrelevant);
223        assert_eq!(fp.n_relevant(), 1);
224        assert_eq!(fp.n_irrelevant(), 1);
225        assert_eq!(fp.codimension(), 1);
226    }
227
228    #[test]
229    fn test_relevant_operator_grows() {
230        // Start with small perturbation in relevant direction → should grow
231        let flow_eq = GaussianFlow::new(3, 2.0);
232        let mut flow = RGFlow::new(vec![0.01, 0.0]); // small r
233        flow.evolve(&flow_eq, 5);
234        // r should have grown significantly
235        assert!(flow.parameters[0] > 0.01);
236    }
237
238    #[test]
239    fn test_irrelevant_operator_shrinks() {
240        // In d=5, u is irrelevant for Gaussian flow
241        let flow_eq = GaussianFlow::new(5, 2.0);
242        let mut flow = RGFlow::new(vec![0.0, 0.1]); // small u, zero r
243        flow.evolve(&flow_eq, 5);
244        // u should have shrunk
245        assert!(flow.parameters[1].abs() < 0.1);
246    }
247
248    #[test]
249    fn test_1d_ising_no_finite_t_critical() {
250        // 1D Ising: all nonzero K flow to 0 (no finite-T critical point)
251        let flow_eq = Ising1DFlow::new(2.0);
252        let mut flow = RGFlow::new(vec![2.0, 0.0]); // Large K (low T)
253        flow.evolve(&flow_eq, 50);
254        // Should flow toward K=0
255        assert!(flow.parameters[0] < 1.0);
256    }
257}