Skip to main content

noether_guard/
renormalize.rs

1/// Renormalization-group-style analysis: coarse-grain the time series at
2/// different scales to find where symmetry breaking first appears.
3#[derive(Debug, Clone)]
4pub struct RenormalizationGroup<'a> {
5    data: &'a [(f64, f64)],
6}
7
8impl<'a> RenormalizationGroup<'a> {
9    /// Create an RG analyser for a drift time series `(time, drift)`.
10    pub fn new(data: &'a [(f64, f64)]) -> Self {
11        Self { data }
12    }
13
14    /// Coarse-grain the data by averaging over blocks of `block_size`.
15    pub fn coarse_grain(&self, block_size: usize) -> Vec<(f64, f64)> {
16        if block_size == 0 || self.data.is_empty() {
17            return Vec::new();
18        }
19        self.data
20            .chunks(block_size)
21            .map(|chunk| {
22                let n = chunk.len() as f64;
23                let avg_t: f64 = chunk.iter().map(|(t, _)| t).sum::<f64>() / n;
24                let avg_d: f64 = chunk.iter().map(|(_, d)| d).sum::<f64>() / n;
25                (avg_t, avg_d)
26            })
27            .collect()
28    }
29
30    /// Find the coarse-graining scale (as block size) where drift becomes
31    /// monotonically increasing, indicating symmetry breaking.
32    pub fn find_breaking_scale(&self) -> Option<f64> {
33        if self.data.len() < 4 {
34            return None;
35        }
36
37        // Try increasingly coarse scales
38        for block_log in 0.. {
39            let block_size = 1usize << block_log;
40            if block_size >= self.data.len() {
41                break;
42            }
43            let coarse = self.coarse_grain(block_size);
44            if coarse.len() < 3 {
45                continue;
46            }
47            // Check if drift is monotonically increasing
48            let monotonic = coarse
49                .windows(2)
50                .all(|w| w[1].1 >= w[0].1 - f64::EPSILON);
51            let exceeds = coarse.iter().any(|(_, d)| *d > 0.0);
52
53            if monotonic && exceeds {
54                // Return the time scale (average time span per block)
55                if let (Some(first), Some(last)) = (coarse.first(), coarse.last()) {
56                    let span = last.0 - first.0;
57                    return Some(span / coarse.len() as f64);
58                }
59            }
60        }
61        None
62    }
63
64    /// Compute the "beta function" — how the drift coupling changes with scale.
65    pub fn beta_function(&self, scales: &[usize]) -> Vec<(usize, f64)> {
66        scales
67            .iter()
68            .map(|&block_size| {
69                let coarse = self.coarse_grain(block_size);
70                if coarse.len() < 2 {
71                    return (block_size, 0.0);
72                }
73                // Rate of change of average drift between consecutive scales
74                let total_drift: f64 = coarse.iter().map(|(_, d)| d).sum();
75                let avg_drift = total_drift / coarse.len() as f64;
76                (block_size, avg_drift)
77            })
78            .collect()
79    }
80}