Skip to main content

evolving_sheaf/
stability.rs

1use crate::graph::Graph;
2use crate::sheaf::SheafConfig;
3use crate::flow::FlowFn;
4use crate::spectrum::compute_spectrum;
5
6/// A 2D stability map of spectral gap over (coupling_alpha, time) grid.
7///
8/// Rows correspond to alpha values, columns to time points.
9#[derive(Debug, Clone)]
10pub struct StabilityMap {
11    /// Alpha values (rows).
12    pub alphas: Vec<f64>,
13    /// Time values (columns).
14    pub times: Vec<f64>,
15    /// Gap values stored row-major: gap_values[i * n_times + j].
16    pub gap_values: Vec<f64>,
17}
18
19impl StabilityMap {
20    pub fn n_alphas(&self) -> usize {
21        self.alphas.len()
22    }
23
24    pub fn n_times(&self) -> usize {
25        self.times.len()
26    }
27
28    /// Get the gap value at (alpha_idx, time_idx).
29    pub fn get(&self, alpha_idx: usize, time_idx: usize) -> f64 {
30        self.gap_values[alpha_idx * self.n_times() + time_idx]
31    }
32
33    /// Compute the total variation for each alpha row.
34    pub fn row_variation(&self) -> Vec<f64> {
35        let n_t = self.n_times();
36        self.alphas.iter().enumerate().map(|(i, _)| {
37            let base = self.get(i, 0);
38            (1..n_t).map(|j| (self.get(i, j) - base).abs()).sum()
39        }).collect()
40    }
41}
42
43/// Compute a stability map: spectral gap over (alpha, time) grid.
44///
45/// Parameters:
46/// - `g`: graph topology
47/// - `base_cfg`: base sheaf config (alpha will be overridden per row)
48/// - `flow`: flow energy function
49/// - `alphas`: array of alpha values to scan
50/// - `times`: array of time points to scan
51///
52/// Returns a `StabilityMap` with gap_values stored row-major.
53pub fn stability_map(
54    g: &Graph,
55    base_cfg: &SheafConfig,
56    flow: FlowFn,
57    alphas: &[f64],
58    times: &[f64],
59) -> StabilityMap {
60    let n_alphas = alphas.len();
61    let n_times = times.len();
62    let mut gap_values = vec![0.0; n_alphas * n_times];
63
64    for (i, &alpha) in alphas.iter().enumerate() {
65        let mut cfg = *base_cfg;
66        cfg.alpha = alpha;
67        for (j, &t) in times.iter().enumerate() {
68            let sp = compute_spectrum(g, &cfg, flow, t);
69            gap_values[i * n_times + j] = sp.lambda1;
70        }
71    }
72
73    StabilityMap {
74        alphas: alphas.to_vec(),
75        times: times.to_vec(),
76        gap_values,
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::graph::Graph;
84    use crate::flow::*;
85    use crate::sheaf::SheafConfig;
86
87    #[test]
88    fn test_stability_map_basic() {
89        let g = Graph::cycle(4);
90        let cfg = SheafConfig::linear(1.0, 0.0);
91
92        let alphas = vec![0.0, 0.5, 1.0];
93        let times = vec![0.0, 5.0, 10.0];
94
95        let sm = stability_map(&g, &cfg, flow_sinusoidal, &alphas, &times);
96
97        assert_eq!(sm.n_alphas(), 3);
98        assert_eq!(sm.n_times(), 3);
99        assert_eq!(sm.gap_values.len(), 9);
100
101        // α=0 should be constant across time
102        assert!((sm.get(0, 0) - sm.get(0, 1)).abs() < 0.01);
103        assert!((sm.get(0, 0) - sm.get(0, 2)).abs() < 0.01);
104
105        // Larger α should differ from α=0
106        let any_diff = (0..3).any(|j| (sm.get(2, j) - sm.get(0, 0)).abs() > 0.01);
107        assert!(any_diff, "larger α changes gap");
108    }
109
110    #[test]
111    fn test_stability_map_increasing_alpha() {
112        let g = Graph::cycle(4);
113        let cfg = SheafConfig::linear(1.0, 0.0);
114
115        let alphas = vec![0.0, 2.0];
116        let times = vec![0.0, 3.0, 6.0, 9.0];
117
118        let sm = stability_map(&g, &cfg, flow_sinusoidal, &alphas, &times);
119
120        let var0 = (1..4).map(|j| (sm.get(0, j) - sm.get(0, 0)).abs()).sum::<f64>();
121        let var1 = (1..4).map(|j| (sm.get(1, j) - sm.get(1, 0)).abs()).sum::<f64>();
122
123        assert!(var0 < 0.01, "α=0: no variation");
124        assert!(var1 > var0, "α=2: more variation than α=0");
125    }
126
127    #[test]
128    fn test_stability_map_grid_dims() {
129        let g = Graph::cycle(4);
130        let cfg = SheafConfig::static_sheaf(1.0);
131
132        let alphas: Vec<f64> = (0..5).map(|i| i as f64 * 0.5).collect();
133        let times: Vec<f64> = (0..10).map(|i| i as f64).collect();
134
135        let sm = stability_map(&g, &cfg, flow_sinusoidal, &alphas, &times);
136        assert_eq!(sm.n_alphas(), 5);
137        assert_eq!(sm.n_times(), 10);
138        assert_eq!(sm.gap_values.len(), 50);
139    }
140
141    #[test]
142    fn test_stability_row_variation() {
143        let g = Graph::cycle(4);
144        let cfg = SheafConfig::linear(1.0, 0.0);
145        let alphas = vec![0.0, 1.0];
146        let times = vec![0.0, 2.0, 4.0, 6.0, 8.0];
147        let sm = stability_map(&g, &cfg, flow_sinusoidal, &alphas, &times);
148
149        let vars = sm.row_variation();
150        assert_eq!(vars.len(), 2);
151        assert!(vars[1] > vars[0], "larger alpha → more row variation");
152    }
153
154    #[test]
155    fn test_stability_map_with_constant_flow() {
156        let g = Graph::cycle(4);
157        let cfg = SheafConfig::linear(1.0, 0.5);
158        let alphas = vec![0.0, 1.0];
159        let times = vec![0.0, 5.0, 10.0];
160        let sm = stability_map(&g, &cfg, flow_constant, &alphas, &times);
161
162        // With constant flow, gap should be same across time at each alpha
163        assert!((sm.get(0, 0) - sm.get(0, 1)).abs() < 0.001);
164        assert!((sm.get(0, 0) - sm.get(0, 2)).abs() < 0.001);
165        assert!((sm.get(1, 0) - sm.get(1, 1)).abs() < 0.001);
166        assert!((sm.get(1, 0) - sm.get(1, 2)).abs() < 0.001);
167    }
168
169    #[test]
170    fn test_stability_map_all_positive() {
171        let g = Graph::cycle(5);
172        let cfg = SheafConfig::linear(1.0, 0.0);
173        let alphas = vec![0.0, 0.5, 1.0, 2.0];
174        let times = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
175        let sm = stability_map(&g, &cfg, flow_sinusoidal, &alphas, &times);
176
177        // All gap values should be positive
178        for &val in &sm.gap_values {
179            assert!(val > 0.0, "all gaps positive: got {}", val);
180        }
181    }
182}