spectral_fleet/
dynamics.rs1use crate::fleet_graph::{CommEdge, FleetGraph};
8use crate::laplacian::spectrum;
9
10#[derive(Debug, Clone)]
12pub struct FleetSnapshot {
13 pub step: usize,
15 pub agent_count: usize,
17 pub edge_count: usize,
19 pub fiedler_value: f64,
21 pub spectral_gap: f64,
23 pub components: usize,
25 pub is_connected: bool,
27}
28
29#[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#[derive(Debug, Clone)]
40pub struct PhaseTransition {
41 pub step: usize,
43 pub description: String,
45 pub fiedler_before: f64,
47 pub fiedler_after: f64,
49}
50
51pub struct FleetDynamics {
53 pub graph: FleetGraph,
55 pub history: Vec<FleetSnapshot>,
57 pub events: Vec<FleetEvent>,
59 pub transitions: Vec<PhaseTransition>,
61 pub step: usize,
63}
64
65impl FleetDynamics {
66 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 pub fn current_snapshot(&self) -> &FleetSnapshot {
94 self.history.last().expect("history should never be empty")
95 }
96
97 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 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 self.graph.edges.retain(|e| e.from != index && e.to != index);
115 self.graph.agents.remove(index);
119 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 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 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 if let Some(prev) = self.history.last() {
157 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 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 pub fn fiedler_trajectory(&self) -> Vec<(usize, f64)> {
191 self.history.iter().map(|s| (s.step, s.fiedler_value)).collect()
192 }
193
194 pub fn detect_transitions(&self) -> &[PhaseTransition] {
196 &self.transitions
197 }
198
199 pub fn spectral_history(&self) -> &[FleetSnapshot] {
201 &self.history
202 }
203}