1use crate::graph::Graph;
2use crate::sheaf::SheafConfig;
3use crate::flow::FlowFn;
4use crate::spectrum::compute_spectrum;
5
6#[derive(Debug, Clone, Copy)]
8pub struct GapPoint {
9 pub t: f64,
11 pub gap: f64,
13 pub gap_rate: f64,
15 pub phase_transition: bool,
17}
18
19#[derive(Debug, Clone)]
21pub struct GapTrajectory {
22 pub points: Vec<GapPoint>,
23 pub min_gap: f64,
25 pub max_gap: f64,
27 pub total_change: f64,
29 pub n_transitions: usize,
31}
32
33impl GapTrajectory {
34 pub fn n_points(&self) -> usize {
35 self.points.len()
36 }
37}
38
39#[derive(Debug, Clone)]
45pub struct SpectralGapTracker {
46 pub g: Graph,
47 pub cfg: SheafConfig,
48 pub flow: FlowFn,
49}
50
51impl SpectralGapTracker {
52 pub fn new(g: Graph, cfg: SheafConfig, flow: FlowFn) -> Self {
53 SpectralGapTracker { g, cfg, flow }
54 }
55
56 pub fn track(&self, t0: f64, t1: f64, n_steps: usize) -> GapTrajectory {
58 let n_points = n_steps + 1;
59 let dt = if n_steps > 0 { (t1 - t0) / n_steps as f64 } else { 0.0 };
60
61 let mut points = Vec::with_capacity(n_points);
62 let mut min_gap = f64::MAX;
63 let mut max_gap = 0.0_f64;
64 let mut prev_gap = -1.0;
65 let mut prev_rate = 0.0;
66 let mut n_transitions = 0;
67
68 for i in 0..n_points {
69 let t = t0 + i as f64 * dt;
70 let sp = compute_spectrum(&self.g, &self.cfg, self.flow, t);
71 let gap = sp.lambda1;
72
73 if gap < min_gap { min_gap = gap; }
74 if gap > max_gap { max_gap = gap; }
75
76 let gap_rate = if i > 0 {
77 (gap - prev_gap) / dt
78 } else {
79 0.0
80 };
81
82 let phase_transition = if i > 1 {
83 prev_rate * gap_rate < -1e-12
84 } else {
85 false
86 };
87
88 if phase_transition {
89 n_transitions += 1;
90 }
91
92 points.push(GapPoint {
93 t,
94 gap,
95 gap_rate,
96 phase_transition,
97 });
98
99 prev_gap = gap;
100 if i > 0 {
101 prev_rate = gap_rate;
102 }
103 }
104
105 let total_change = (max_gap - min_gap).abs();
106
107 GapTrajectory {
108 points,
109 min_gap,
110 max_gap,
111 total_change,
112 n_transitions,
113 }
114 }
115
116 pub fn relative_change(&self, t0: f64, t1: f64, n_steps: usize) -> f64 {
119 let traj = self.track(t0, t1, n_steps);
120 if traj.max_gap > 0.0 {
121 traj.total_change / traj.max_gap
122 } else {
123 0.0
124 }
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct PhaseTransition {
131 pub t: f64,
133 pub gap: f64,
135 pub rate_before: f64,
137 pub rate_after: f64,
139}
140
141pub fn detect_phase_transitions(traj: &GapTrajectory) -> Vec<PhaseTransition> {
143 let mut transitions = Vec::new();
144 for i in 1..traj.points.len() - 1 {
145 let before = traj.points[i - 1].gap_rate;
146 let after = traj.points[i].gap_rate;
147 if before * after < -1e-12 {
148 transitions.push(PhaseTransition {
149 t: traj.points[i].t,
150 gap: traj.points[i].gap,
151 rate_before: before,
152 rate_after: after,
153 });
154 }
155 }
156 transitions
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::graph::Graph;
163 use crate::flow::*;
164
165 #[test]
166 fn test_static_no_transitions() {
167 let g = Graph::cycle(4);
168 let cfg = SheafConfig::static_sheaf(1.0);
169 let tracker = SpectralGapTracker::new(g, cfg, flow_constant);
170 let traj = tracker.track(0.0, 10.0, 100);
171 assert_eq!(traj.n_transitions, 0, "static: no phase transitions");
172 }
173
174 #[test]
175 fn test_linear_variation() {
176 let g = Graph::cycle(4);
177 let cfg = SheafConfig::linear(1.0, 0.5);
178 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
179 let traj = tracker.track(0.0, 20.0, 100);
180
181 assert!(traj.total_change > 0.01, "gap varies over time");
182 assert_eq!(traj.n_points(), 101);
183 }
184
185 #[test]
186 fn test_linear_min_max() {
187 let g = Graph::cycle(4);
188 let cfg = SheafConfig::linear(1.0, 0.5);
189 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
190 let traj = tracker.track(0.0, 20.0, 200);
191
192 assert!(traj.min_gap > 0.0, "min gap > 0");
193 assert!(traj.max_gap > traj.min_gap, "max > min");
194 }
195
196 #[test]
197 fn test_linear_larger_alpha_more_change() {
198 let g = Graph::cycle(4);
199 let t1 = SpectralGapTracker::new(g.clone(), SheafConfig::linear(1.0, 0.1), flow_sinusoidal);
200 let t2 = SpectralGapTracker::new(g, SheafConfig::linear(1.0, 1.0), flow_sinusoidal);
201
202 let traj1 = t1.track(0.0, 10.0, 100);
203 let traj2 = t2.track(0.0, 10.0, 100);
204
205 assert!(traj2.total_change > traj1.total_change,
206 "larger α → more total change: {} vs {}", traj2.total_change, traj1.total_change);
207 }
208
209 #[test]
210 fn test_pulse_flow_variation() {
211 let g = Graph::cycle(4);
212 let cfg = SheafConfig::linear(1.0, 0.5);
213 let tracker = SpectralGapTracker::new(g, cfg, flow_pulse);
214 let traj = tracker.track(0.0, 20.0, 200);
215 assert_eq!(traj.n_points(), 201);
216 assert!(traj.total_change > 0.0, "pulse causes gap variation");
217 }
218
219 #[test]
220 fn test_sinusoidal_phase_transitions() {
221 let g = Graph::cycle(4);
222 let cfg = SheafConfig::linear(1.0, 1.0);
223 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
224 let traj = tracker.track(0.0, 30.0, 500);
225 assert!(traj.n_transitions >= 1, "sinusoidal causes ≥1 phase transition");
226 }
227
228 #[test]
229 fn test_first_point_rate_zero() {
230 let g = Graph::cycle(4);
231 let cfg = SheafConfig::linear(1.0, 0.5);
232 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
233 let traj = tracker.track(0.0, 10.0, 100);
234 assert!((traj.points[0].gap_rate).abs() < 1e-12, "first point rate = 0");
235 }
236
237 #[test]
238 fn test_some_nonzero_rates() {
239 let g = Graph::cycle(4);
240 let cfg = SheafConfig::linear(1.0, 0.5);
241 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
242 let traj = tracker.track(0.0, 10.0, 100);
243 let any_rate = traj.points[1..].iter().any(|p| p.gap_rate.abs() > 1e-6);
244 assert!(any_rate, "some nonzero gap rates");
245 }
246
247 #[test]
248 fn test_pulse_both_rate_signs() {
249 let g = Graph::cycle(4);
250 let cfg = SheafConfig::linear(1.0, 1.5);
251 let tracker = SpectralGapTracker::new(g, cfg, flow_pulse);
252 let traj = tracker.track(0.0, 20.0, 400);
253
254 let pos = traj.points.iter().filter(|p| p.gap_rate > 0.01).count();
255 let neg = traj.points.iter().filter(|p| p.gap_rate < -0.01).count();
256 assert!(pos > 0 && neg > 0, "gap rate has both positive/negative phases");
257 }
258
259 #[test]
260 fn test_high_alpha_large_change() {
261 let g = Graph::cycle(4);
262 let cfg = SheafConfig::linear(1.0, 10.0);
263 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
264 let traj = tracker.track(0.0, 10.0, 100);
265 assert!(traj.total_change > 1.0, "high α causes large variation");
266 assert!(traj.min_gap > 0.0, "gap stays positive even at high α");
267 }
268
269 #[test]
270 fn test_smooth_variation() {
271 let g = Graph::cycle(4);
272 let cfg = SheafConfig::linear(1.0, 0.5);
273 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
274 let traj = tracker.track(0.0, 10.0, 1000);
275
276 let max_jump = traj.points.windows(2)
277 .map(|w| (w[1].gap - w[0].gap).abs())
278 .fold(0.0_f64, f64::max);
279 assert!(max_jump < 0.5, "gap changes smoothly: max_jump={}", max_jump);
280 }
281
282 #[test]
283 fn test_nonlinear_tanh_trajectory() {
284 let g = Graph::cycle(4);
285 let cfg = SheafConfig::nonlinear(2.0, crate::sheaf::NonlinearFn::Tanh, 1.0);
286 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
287 let traj = tracker.track(0.0, 10.0, 100);
288 assert_eq!(traj.n_points(), 101);
289 assert!(traj.total_change > 0.01, "tanh causes variation");
290 }
291
292 #[test]
293 fn test_nonlinear_expdecay_trajectory() {
294 let g = Graph::cycle(5);
295 let cfg = SheafConfig::nonlinear(2.0, crate::sheaf::NonlinearFn::ExpDecay, 0.5);
296 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
297 let traj = tracker.track(0.0, 10.0, 100);
298 assert!(traj.min_gap > 0.0, "expdecay min gap > 0");
299 }
300
301 #[test]
302 fn test_nonlinear_differs_from_linear() {
303 let g = Graph::cycle(4);
304 let lcfg = SheafConfig::linear(1.0, 0.5);
305 let ncfg = SheafConfig::nonlinear(1.0, crate::sheaf::NonlinearFn::Sigmoid, 2.0);
306
307 let lt = SpectralGapTracker::new(g.clone(), lcfg, flow_sinusoidal)
308 .track(0.0, 10.0, 100);
309 let nt = SpectralGapTracker::new(g, ncfg, flow_sinusoidal)
310 .track(0.0, 10.0, 100);
311
312 let any_diff = lt.points.iter().zip(nt.points.iter())
313 .any(|(lp, np)| (lp.gap - np.gap).abs() > 0.05);
314 assert!(any_diff, "nonlinear dynamics differ from linear");
315 }
316
317 #[test]
318 fn test_expander_robustness() {
319 let cycle = Graph::cycle(10);
320 let expander = Graph::expander(10);
321 let cfg = SheafConfig::linear(1.0, 1.0);
322
323 let ct = SpectralGapTracker::new(cycle, cfg.clone(), flow_sinusoidal)
324 .track(0.0, 10.0, 200);
325 let et = SpectralGapTracker::new(expander, cfg, flow_sinusoidal)
326 .track(0.0, 10.0, 200);
327
328 let cycle_rel = ct.total_change / ct.max_gap;
331 let exp_rel = et.total_change / et.max_gap;
332 assert!(et.min_gap > 0.0, "expander gap stays positive");
333 assert!(ct.min_gap > 0.0, "cycle gap stays positive");
334 assert!(exp_rel <= cycle_rel + 0.1,
336 "expander resists gap erosion: exp_rel={:.4}, cycle_rel={:.4}",
337 exp_rel, cycle_rel);
338 }
339
340 #[test]
341 fn test_phase_transition_detection() {
342 let g = Graph::cycle(4);
343 let cfg = SheafConfig::linear(1.0, 1.0);
344 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
345 let traj = tracker.track(0.0, 30.0, 500);
346
347 let transitions = detect_phase_transitions(&traj);
348 assert!(!transitions.is_empty(), "should find phase transitions");
349 for pt in &transitions {
350 assert!(pt.rate_before * pt.rate_after < 0.0, "sign must change");
351 }
352 }
353
354 #[test]
355 fn test_static_no_phase_transitions() {
356 let g = Graph::cycle(4);
357 let cfg = SheafConfig::static_sheaf(1.0);
358 let tracker = SpectralGapTracker::new(g, cfg, flow_constant);
359 let traj = tracker.track(0.0, 10.0, 100);
360 let transitions = detect_phase_transitions(&traj);
361 assert!(transitions.is_empty(), "static: no phase transitions detected");
362 }
363
364 #[test]
365 fn test_pulse_large_variation() {
366 let g = Graph::cycle(4);
367 let cfg = SheafConfig::linear(1.0, 1.5);
368 let tracker = SpectralGapTracker::new(g, cfg, flow_pulse);
369 let traj = tracker.track(0.0, 20.0, 400);
370 assert!(traj.total_change > 1.0, "pulse causes large gap variation");
371 }
372
373 #[test]
374 fn test_trajectory_continuity() {
375 let g = Graph::cycle(4);
376 let cfg = SheafConfig::linear(1.0, 0.5);
377 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
378 let traj = tracker.track(0.0, 10.0, 100);
379 for i in 1..traj.n_points() {
381 assert!(traj.points[i].t > traj.points[i - 1].t, "times increasing");
382 }
383 }
384
385 #[test]
386 fn test_relative_change_computation() {
387 let g = Graph::cycle(4);
388 let cfg = SheafConfig::linear(1.0, 0.5);
389 let tracker = SpectralGapTracker::new(g, cfg, flow_sinusoidal);
390 let rel = tracker.relative_change(0.0, 10.0, 100);
391 assert!(rel >= 0.0, "relative change ≥ 0");
392 assert!(rel <= 1.0, "relative change ≤ 1.0");
393 }
394}