Skip to main content

spectral_fleet/
dynamics.rs

1//! Fleet graph dynamics: how the fleet evolves over time.
2//!
3//! Tracks edge additions/removals, agent joining/leaving,
4//! and how spectral properties change. Detects phase transitions
5//! such as the fleet becoming disconnected.
6
7use crate::fleet_graph::{CommEdge, FleetGraph};
8use crate::laplacian::spectrum;
9
10/// A snapshot of the fleet's spectral properties at a point in time.
11#[derive(Debug, Clone)]
12pub struct FleetSnapshot {
13    /// Timestamp or step number.
14    pub step: usize,
15    /// Number of agents.
16    pub agent_count: usize,
17    /// Number of edges.
18    pub edge_count: usize,
19    /// Fiedler value (algebraic connectivity).
20    pub fiedler_value: f64,
21    /// Spectral gap.
22    pub spectral_gap: f64,
23    /// Number of connected components.
24    pub components: usize,
25    /// Is the fleet connected?
26    pub is_connected: bool,
27}
28
29/// A change event in the fleet graph.
30#[derive(Debug, Clone)]
31pub enum FleetEvent {
32    AgentJoined { agent_id: String, index: usize },
33    AgentLeft { agent_id: String, index: usize },
34    EdgeAdded { from: usize, to: usize },
35    EdgeRemoved { from: usize, to: usize },
36}
37
38/// A detected phase transition in the fleet.
39#[derive(Debug, Clone)]
40pub struct PhaseTransition {
41    /// Step at which the transition occurred.
42    pub step: usize,
43    /// Description of the transition.
44    pub description: String,
45    /// Fiedler value before the transition.
46    pub fiedler_before: f64,
47    /// Fiedler value after the transition.
48    pub fiedler_after: f64,
49}
50
51/// A temporal tracker for fleet graph evolution.
52pub struct FleetDynamics {
53    /// Current fleet graph.
54    pub graph: FleetGraph,
55    /// History of snapshots.
56    pub history: Vec<FleetSnapshot>,
57    /// Events that have occurred.
58    pub events: Vec<FleetEvent>,
59    /// Detected phase transitions.
60    pub transitions: Vec<PhaseTransition>,
61    /// Current step counter.
62    pub step: usize,
63}
64
65impl FleetDynamics {
66    /// Create a new dynamics tracker starting with the given graph.
67    pub fn new(graph: FleetGraph) -> Self {
68        let snap = Self::take_snapshot(&graph, 0);
69        Self {
70            graph,
71            history: vec![snap],
72            events: Vec::new(),
73            transitions: Vec::new(),
74            step: 0,
75        }
76    }
77
78    fn take_snapshot(graph: &FleetGraph, step: usize) -> FleetSnapshot {
79        let spec = spectrum(graph);
80        let components = graph.connected_components().len();
81        FleetSnapshot {
82            step,
83            agent_count: graph.node_count(),
84            edge_count: graph.edge_count(),
85            fiedler_value: spec.fiedler_value,
86            spectral_gap: spec.spectral_gap,
87            components,
88            is_connected: components == 1,
89        }
90    }
91
92    /// Get the current snapshot.
93    pub fn current_snapshot(&self) -> &FleetSnapshot {
94        self.history.last().expect("history should never be empty")
95    }
96
97    /// Add an agent to the fleet.
98    pub fn add_agent(&mut self, id: impl Into<String>, capabilities: Vec<String>, load: f64) -> usize {
99        let idx = self.graph.add_agent(crate::fleet_graph::AgentNode::new(id, capabilities, load));
100        self.step += 1;
101        let agent_id = self.graph.agents[idx].id.clone();
102        self.events.push(FleetEvent::AgentJoined { agent_id, index: idx });
103        self.record_snapshot();
104        idx
105    }
106
107    /// Remove an agent (and all its edges) from the fleet.
108    pub fn remove_agent(&mut self, index: usize) {
109        if index >= self.graph.agents.len() {
110            return;
111        }
112        let agent_id = self.graph.agents[index].id.clone();
113        // Remove all edges involving this agent
114        self.graph.edges.retain(|e| e.from != index && e.to != index);
115        // Re-index edges (swap remove)
116        // This is tricky; for simplicity, we just remove the node and fix indices
117        // In a production system, use a proper graph data structure
118        self.graph.agents.remove(index);
119        // Fix edge indices
120        for edge in &mut self.graph.edges {
121            if edge.from > index {
122                edge.from -= 1;
123            }
124            if edge.to > index {
125                edge.to -= 1;
126            }
127        }
128        self.step += 1;
129        self.events.push(FleetEvent::AgentLeft { agent_id, index });
130        self.record_snapshot();
131    }
132
133    /// Add a communication edge.
134    pub fn add_edge(&mut self, from: usize, to: usize, bandwidth: f64, latency: f64) {
135        self.graph.add_edge(CommEdge::new(from, to, bandwidth, latency));
136        self.step += 1;
137        self.events.push(FleetEvent::EdgeAdded { from, to });
138        self.record_snapshot();
139    }
140
141    /// Remove a specific edge (by index).
142    pub fn remove_edge(&mut self, from: usize, to: usize) {
143        let before_count = self.graph.edges.len();
144        self.graph.edges.retain(|e| !(e.from == from && e.to == to));
145        if self.graph.edges.len() < before_count {
146            self.step += 1;
147            self.events.push(FleetEvent::EdgeRemoved { from, to });
148            self.record_snapshot();
149        }
150    }
151
152    fn record_snapshot(&mut self) {
153        let snap = Self::take_snapshot(&self.graph, self.step);
154
155        // Check for phase transitions
156        if let Some(prev) = self.history.last() {
157            // Connectivity change
158            if prev.is_connected != snap.is_connected {
159                self.transitions.push(PhaseTransition {
160                    step: self.step,
161                    description: if prev.is_connected && !snap.is_connected {
162                        "Fleet became disconnected".into()
163                    } else {
164                        "Fleet became connected".into()
165                    },
166                    fiedler_before: prev.fiedler_value,
167                    fiedler_after: snap.fiedler_value,
168                });
169            }
170
171            // Significant Fiedler value change (arbitrary threshold)
172            let fiedler_change = (snap.fiedler_value - prev.fiedler_value).abs();
173            if fiedler_change > 0.5 && prev.fiedler_value > 0.01 {
174                self.transitions.push(PhaseTransition {
175                    step: self.step,
176                    description: format!(
177                        "Significant connectivity change: Fiedler {:.3} → {:.3}",
178                        prev.fiedler_value, snap.fiedler_value
179                    ),
180                    fiedler_before: prev.fiedler_value,
181                    fiedler_after: snap.fiedler_value,
182                });
183            }
184        }
185
186        self.history.push(snap);
187    }
188
189    /// Compute the trajectory of Fiedler values over time.
190    pub fn fiedler_trajectory(&self) -> Vec<(usize, f64)> {
191        self.history.iter().map(|s| (s.step, s.fiedler_value)).collect()
192    }
193
194    /// Detect all phase transitions that have occurred.
195    pub fn detect_transitions(&self) -> &[PhaseTransition] {
196        &self.transitions
197    }
198
199    /// Get the full spectral history.
200    pub fn spectral_history(&self) -> &[FleetSnapshot] {
201        &self.history
202    }
203}