Skip to main content

renormalization_group/
flow.rs

1//! # RG Flow Equations
2//!
3//! Track how coupling parameters evolve under repeated coarse-graining.
4//! The RG flow is the trajectory in parameter space as scale increases.
5
6use crate::linalg;
7use serde::{Deserialize, Serialize};
8
9/// State of the RG flow at a particular scale.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RGFlow {
12    /// Current parameter values (coupling constants, fields, etc.)
13    pub parameters: Vec<f64>,
14    /// Current scale level
15    pub scale: usize,
16}
17
18/// A single step in the RG flow trajectory.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct FlowStep {
21    /// Scale at this step
22    pub scale: usize,
23    /// Parameter values
24    pub parameters: Vec<f64>,
25    /// Change in parameters from previous step
26    pub delta: Vec<f64>,
27}
28
29/// The RG flow equation: maps parameters at scale ℓ to scale ℓ+1.
30/// This is the beta function in RG language.
31pub trait FlowEquation: Send + Sync {
32    /// Compute parameters at the next scale given current parameters.
33    fn flow_step(&self, params: &[f64], scale: usize) -> Vec<f64>;
34
35    /// Dimensionality of parameter space
36    fn dim(&self) -> usize;
37}
38
39/// 1D Ising RG flow (decimation).
40/// Parameters: [K = J/kT, h = H/kT]
41/// Under decimation by factor b=2:
42///   K' = atanh(tanh(K)^2) (simplified form)
43///   h' = 0 (simplification for zero field)
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Ising1DFlow {
46    /// Decimation factor
47    pub b: f64,
48}
49
50impl Ising1DFlow {
51    pub fn new(b: f64) -> Self {
52        Self { b }
53    }
54}
55
56impl FlowEquation for Ising1DFlow {
57    fn flow_step(&self, params: &[f64], _scale: usize) -> Vec<f64> {
58        let k = params[0];
59        // K' = atanh(tanh(K)^2) for b=2 decimation
60        let tanh_k = k.tanh();
61        let new_k = tanh_k * tanh_k;
62        let k_prime = new_k.atanh();
63        // h remains 0 for simplicity (add field evolution if needed)
64        let h = if params.len() > 1 { params[1] } else { 0.0 };
65        let h_prime = if h.abs() < 1e-15 {
66            0.0
67        } else {
68            // Field renormalization for 1D Ising
69            2.0 * h // Simplified: field doubles under decimation
70        };
71        vec![k_prime, h_prime]
72    }
73
74    fn dim(&self) -> usize {
75        2
76    }
77}
78
79/// Gaussian (free) RG flow.
80/// Parameters: [r (mass), u (coupling)]
81/// r' = b^2 * r  (relevant, eigenvalue b^2)
82/// u' = b^(4-d) * u  (marginal in d=4)
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct GaussianFlow {
85    /// Spatial dimension
86    pub d: usize,
87    /// Scale factor
88    pub b: f64,
89}
90
91impl GaussianFlow {
92    pub fn new(d: usize, b: f64) -> Self {
93        Self { d, b }
94    }
95}
96
97impl FlowEquation for GaussianFlow {
98    fn flow_step(&self, params: &[f64], _scale: usize) -> Vec<f64> {
99        let r = params[0];
100        let u = if params.len() > 1 { params[1] } else { 0.0 };
101        let r_prime = self.b * self.b * r;
102        let u_prime = self.b.powi(4 - self.d as i32) * u;
103        vec![r_prime, u_prime]
104    }
105
106    fn dim(&self) -> usize {
107        2
108    }
109}
110
111/// Wilson-Fisher flow near d=4.
112/// Beta functions: dr/dl = 2r + (n+2)u/(6π²)
113///                 du/dl = ε*u - (n+8)u²/(6π²)
114/// where ε = 4-d
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct WilsonFisherFlow {
117    /// Spatial dimension
118    pub d: f64,
119    /// Number of field components (n=1 for Ising)
120    pub n: usize,
121    /// Step size for discrete flow
122    pub dl: f64,
123}
124
125impl WilsonFisherFlow {
126    pub fn new(d: f64, n: usize, dl: f64) -> Self {
127        Self { d, n, dl }
128    }
129}
130
131impl FlowEquation for WilsonFisherFlow {
132    fn flow_step(&self, params: &[f64], _scale: usize) -> Vec<f64> {
133        let r = params[0];
134        let u = params[1];
135        let epsilon = 4.0 - self.d;
136        let pi2 = std::f64::consts::PI * std::f64::consts::PI;
137
138        let dr = 2.0 * r + (self.n as f64 + 2.0) * u / (6.0 * pi2);
139        let du = epsilon * u - (self.n as f64 + 8.0) * u * u / (6.0 * pi2);
140
141        vec![r + self.dl * dr, u + self.dl * du]
142    }
143
144    fn dim(&self) -> usize {
145        2
146    }
147}
148
149impl RGFlow {
150    /// Create a new RG flow state.
151    pub fn new(parameters: Vec<f64>) -> Self {
152        Self {
153            parameters,
154            scale: 0,
155        }
156    }
157
158    /// Evolve the flow for `n_steps` steps.
159    pub fn evolve(&mut self, flow_eq: &dyn FlowEquation, n_steps: usize) -> Vec<FlowStep> {
160        let mut trajectory = Vec::with_capacity(n_steps);
161        for _ in 0..n_steps {
162            let old_params = self.parameters.clone();
163            let new_params = flow_eq.flow_step(&self.parameters, self.scale);
164            let delta: Vec<f64> = new_params
165                .iter()
166                .zip(old_params.iter())
167                .map(|(n, o)| n - o)
168                .collect();
169            self.scale += 1;
170            self.parameters = new_params;
171            trajectory.push(FlowStep {
172                scale: self.scale,
173                parameters: self.parameters.clone(),
174                delta,
175            });
176        }
177        trajectory
178    }
179
180    /// Compute the stability matrix (Jacobian of the flow equation)
181    /// at the current parameter values.
182    pub fn stability_matrix(
183        &self,
184        flow_eq: &dyn FlowEquation,
185    ) -> Vec<Vec<f64>> {
186        linalg::jacobian(
187            &|p| flow_eq.flow_step(p, self.scale),
188            &self.parameters,
189            1e-7,
190        )
191    }
192}
193
194/// Compute the flow length (total parameter displacement) of a trajectory.
195pub fn flow_length(trajectory: &[FlowStep]) -> f64 {
196    trajectory
197        .iter()
198        .map(|step| step.delta.iter().map(|d| d * d).sum::<f64>().sqrt())
199        .sum()
200}
201
202/// Check if a trajectory has converged (delta < tolerance for last steps).
203pub fn has_converged(trajectory: &[FlowStep], tolerance: f64, n_check: usize) -> bool {
204    let n = trajectory.len();
205    if n < n_check {
206        return false;
207    }
208    trajectory[(n - n_check)..]
209        .iter()
210        .all(|step| step.delta.iter().map(|d| d * d).sum::<f64>().sqrt() < tolerance)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_ising_1d_flow_toward_zero() {
219        // 1D Ising has no phase transition at finite T;
220        // K = J/kT should flow to 0 (high-T fixed point)
221        let flow_eq = Ising1DFlow::new(2.0);
222        let mut flow = RGFlow::new(vec![0.5, 0.0]);
223        let _traj = flow.evolve(&flow_eq, 20);
224        // K should decrease toward 0
225        assert!(flow.parameters[0] < 0.5);
226        // After many steps, K should be very small
227        assert!(flow.parameters[0].abs() < 0.1);
228    }
229
230    #[test]
231    fn test_ising_1d_zero_k_fixed_point() {
232        // K=0 is a fixed point of the 1D Ising flow
233        let flow_eq = Ising1DFlow::new(2.0);
234        let result = flow_eq.flow_step(&[0.0, 0.0], 0);
235        assert!(result[0].abs() < 1e-10);
236    }
237
238    #[test]
239    fn test_gaussian_r_grows_relevant() {
240        // r is a relevant operator: it grows under RG flow
241        let flow_eq = GaussianFlow::new(3, 2.0); // d=3, b=2
242        let mut flow = RGFlow::new(vec![0.1, 0.0]);
243        let _traj = flow.evolve(&flow_eq, 5);
244        // r should have grown: r' = 4*r each step
245        let expected = 0.1 * 4.0_f64.powi(5);
246        assert!((flow.parameters[0] - expected).abs() < 1e-10);
247    }
248
249    #[test]
250    fn test_gaussian_u_irrelevant_d3() {
251        // In d=3, u is irrelevant: u' = b^(4-3)*u = b*u... wait b^(4-d)
252        // b=2, d=3: u' = 2^1 * u = 2u (relevant in d=3!)
253        // Let's use d=5: u' = b^(4-5)*u = b^(-1)*u = u/2 (irrelevant)
254        let flow_eq = GaussianFlow::new(5, 2.0);
255        let mut flow = RGFlow::new(vec![0.0, 1.0]);
256        let _traj = flow.evolve(&flow_eq, 3);
257        // u should shrink: u' = 2^(-1)*u each step = u/2 each step
258        let expected = 1.0 * 0.5_f64.powi(3);
259        assert!((flow.parameters[1] - expected).abs() < 1e-10);
260    }
261
262    #[test]
263    fn test_gaussian_zero_is_fixed_point() {
264        let flow_eq = GaussianFlow::new(3, 2.0);
265        let result = flow_eq.flow_step(&[0.0, 0.0], 0);
266        assert!(result[0].abs() < 1e-15);
267        assert!(result[1].abs() < 1e-15);
268    }
269
270    #[test]
271    fn test_wilson_fisher_fixed_point() {
272        // Wilson-Fisher fixed point at u* = 6π²ε/(n+8), r* from beta_r=0
273        let flow_eq = WilsonFisherFlow::new(3.99, 1, 0.01); // ε=0.01, n=1
274        let epsilon = 0.01;
275        let n = 1.0;
276        let pi2 = std::f64::consts::PI * std::f64::consts::PI;
277        let u_star = 6.0 * pi2 * epsilon / (n + 8.0);
278
279        // Start near the Wilson-Fisher fixed point
280        let mut flow = RGFlow::new(vec![0.0, u_star]);
281        let _traj = flow.evolve(&flow_eq, 500);
282        // Should stay near fixed point (not diverge wildly)
283        assert!(flow.parameters[1] > 0.0);
284        assert!(flow.parameters[1] < u_star * 10.0);
285    }
286
287    #[test]
288    fn test_flow_convergence_detection() {
289        let flow_eq = GaussianFlow::new(3, 2.0);
290        let mut flow = RGFlow::new(vec![0.0, 0.0]); // At fixed point
291        let traj = flow.evolve(&flow_eq, 5);
292        assert!(has_converged(&traj, 1e-10, 3));
293    }
294
295    #[test]
296    fn test_flow_length_positive() {
297        let flow_eq = GaussianFlow::new(3, 2.0);
298        let mut flow = RGFlow::new(vec![1.0, 0.0]);
299        let traj = flow.evolve(&flow_eq, 3);
300        assert!(flow_length(&traj) > 0.0);
301    }
302}