use crate::fleet_graph::{CommEdge, FleetGraph};
use crate::laplacian::spectrum;
#[derive(Debug, Clone)]
pub struct FleetSnapshot {
pub step: usize,
pub agent_count: usize,
pub edge_count: usize,
pub fiedler_value: f64,
pub spectral_gap: f64,
pub components: usize,
pub is_connected: bool,
}
#[derive(Debug, Clone)]
pub enum FleetEvent {
AgentJoined { agent_id: String, index: usize },
AgentLeft { agent_id: String, index: usize },
EdgeAdded { from: usize, to: usize },
EdgeRemoved { from: usize, to: usize },
}
#[derive(Debug, Clone)]
pub struct PhaseTransition {
pub step: usize,
pub description: String,
pub fiedler_before: f64,
pub fiedler_after: f64,
}
pub struct FleetDynamics {
pub graph: FleetGraph,
pub history: Vec<FleetSnapshot>,
pub events: Vec<FleetEvent>,
pub transitions: Vec<PhaseTransition>,
pub step: usize,
}
impl FleetDynamics {
pub fn new(graph: FleetGraph) -> Self {
let snap = Self::take_snapshot(&graph, 0);
Self {
graph,
history: vec![snap],
events: Vec::new(),
transitions: Vec::new(),
step: 0,
}
}
fn take_snapshot(graph: &FleetGraph, step: usize) -> FleetSnapshot {
let spec = spectrum(graph);
let components = graph.connected_components().len();
FleetSnapshot {
step,
agent_count: graph.node_count(),
edge_count: graph.edge_count(),
fiedler_value: spec.fiedler_value,
spectral_gap: spec.spectral_gap,
components,
is_connected: components == 1,
}
}
pub fn current_snapshot(&self) -> &FleetSnapshot {
self.history.last().expect("history should never be empty")
}
pub fn add_agent(&mut self, id: impl Into<String>, capabilities: Vec<String>, load: f64) -> usize {
let idx = self.graph.add_agent(crate::fleet_graph::AgentNode::new(id, capabilities, load));
self.step += 1;
let agent_id = self.graph.agents[idx].id.clone();
self.events.push(FleetEvent::AgentJoined { agent_id, index: idx });
self.record_snapshot();
idx
}
pub fn remove_agent(&mut self, index: usize) {
if index >= self.graph.agents.len() {
return;
}
let agent_id = self.graph.agents[index].id.clone();
self.graph.edges.retain(|e| e.from != index && e.to != index);
self.graph.agents.remove(index);
for edge in &mut self.graph.edges {
if edge.from > index {
edge.from -= 1;
}
if edge.to > index {
edge.to -= 1;
}
}
self.step += 1;
self.events.push(FleetEvent::AgentLeft { agent_id, index });
self.record_snapshot();
}
pub fn add_edge(&mut self, from: usize, to: usize, bandwidth: f64, latency: f64) {
self.graph.add_edge(CommEdge::new(from, to, bandwidth, latency));
self.step += 1;
self.events.push(FleetEvent::EdgeAdded { from, to });
self.record_snapshot();
}
pub fn remove_edge(&mut self, from: usize, to: usize) {
let before_count = self.graph.edges.len();
self.graph.edges.retain(|e| !(e.from == from && e.to == to));
if self.graph.edges.len() < before_count {
self.step += 1;
self.events.push(FleetEvent::EdgeRemoved { from, to });
self.record_snapshot();
}
}
fn record_snapshot(&mut self) {
let snap = Self::take_snapshot(&self.graph, self.step);
if let Some(prev) = self.history.last() {
if prev.is_connected != snap.is_connected {
self.transitions.push(PhaseTransition {
step: self.step,
description: if prev.is_connected && !snap.is_connected {
"Fleet became disconnected".into()
} else {
"Fleet became connected".into()
},
fiedler_before: prev.fiedler_value,
fiedler_after: snap.fiedler_value,
});
}
let fiedler_change = (snap.fiedler_value - prev.fiedler_value).abs();
if fiedler_change > 0.5 && prev.fiedler_value > 0.01 {
self.transitions.push(PhaseTransition {
step: self.step,
description: format!(
"Significant connectivity change: Fiedler {:.3} → {:.3}",
prev.fiedler_value, snap.fiedler_value
),
fiedler_before: prev.fiedler_value,
fiedler_after: snap.fiedler_value,
});
}
}
self.history.push(snap);
}
pub fn fiedler_trajectory(&self) -> Vec<(usize, f64)> {
self.history.iter().map(|s| (s.step, s.fiedler_value)).collect()
}
pub fn detect_transitions(&self) -> &[PhaseTransition] {
&self.transitions
}
pub fn spectral_history(&self) -> &[FleetSnapshot] {
&self.history
}
}