Skip to main content

spectral_fleet/
fleet_graph.rs

1//! Fleet graph representation: agents as nodes, communication channels as edges.
2//!
3//! A fleet is modeled as a directed, weighted graph where each node is an AI agent
4//! and each edge represents a communication or dependency channel with associated
5//! bandwidth, latency, and reliability metrics.
6
7use serde::{Deserialize, Serialize};
8
9/// An agent (node) in the fleet graph.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AgentNode {
12    pub id: String,
13    pub capabilities: Vec<String>,
14    /// Current load in [0, 1]. 0 = idle, 1 = fully loaded.
15    pub load: f64,
16}
17
18impl AgentNode {
19    pub fn new(id: impl Into<String>, capabilities: Vec<String>, load: f64) -> Self {
20        Self {
21            id: id.into(),
22            capabilities,
23            load,
24        }
25    }
26}
27
28/// A directed communication channel (edge) between two agents.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct CommEdge {
31    pub from: usize,
32    pub to: usize,
33    /// Communication bandwidth (higher = faster).
34    pub bandwidth: f64,
35    /// Latency in milliseconds (lower = faster).
36    pub latency: f64,
37}
38
39impl CommEdge {
40    pub fn new(from: usize, to: usize, bandwidth: f64, latency: f64) -> Self {
41        Self {
42            from,
43            to,
44            bandwidth,
45            latency,
46        }
47    }
48}
49
50/// The fleet graph: a directed weighted graph of agents and communication channels.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct FleetGraph {
53    pub agents: Vec<AgentNode>,
54    pub edges: Vec<CommEdge>,
55}
56
57impl FleetGraph {
58    /// Create an empty fleet graph.
59    pub fn new() -> Self {
60        Self {
61            agents: Vec::new(),
62            edges: Vec::new(),
63        }
64    }
65
66    /// Add an agent and return its index.
67    pub fn add_agent(&mut self, agent: AgentNode) -> usize {
68        let idx = self.agents.len();
69        self.agents.push(agent);
70        idx
71    }
72
73    /// Add a directed communication edge.
74    pub fn add_edge(&mut self, edge: CommEdge) {
75        assert!(edge.from < self.agents.len(), "edge.from out of bounds");
76        assert!(edge.to < self.agents.len(), "edge.to out of bounds");
77        self.edges.push(edge);
78    }
79
80    /// Number of agents (nodes).
81    pub fn node_count(&self) -> usize {
82        self.agents.len()
83    }
84
85    /// Number of edges.
86    pub fn edge_count(&self) -> usize {
87        self.edges.len()
88    }
89
90    /// Get the number of agents.
91    pub fn len(&self) -> usize {
92        self.agents.len()
93    }
94
95    /// Check if the graph is empty.
96    pub fn is_empty(&self) -> bool {
97        self.agents.is_empty()
98    }
99
100    /// Build the adjacency matrix (n×n). Entry [i][j] = total bandwidth from i→j.
101    /// For undirected analysis, use `undirected_adjacency`.
102    pub fn adjacency_matrix(&self) -> Vec<Vec<f64>> {
103        let n = self.node_count();
104        let mut adj = vec![vec![0.0; n]; n];
105        for edge in &self.edges {
106            adj[edge.from][edge.to] += edge.bandwidth;
107        }
108        adj
109    }
110
111    /// Build the undirected adjacency matrix: A[i][j] = A[j][i] = max bandwidth in either direction.
112    pub fn undirected_adjacency(&self) -> Vec<Vec<f64>> {
113        let n = self.node_count();
114        let mut adj = vec![vec![0.0; n]; n];
115        for edge in &self.edges {
116            let w = edge.bandwidth;
117            adj[edge.from][edge.to] = f64::max(adj[edge.from][edge.to], w);
118            adj[edge.to][edge.from] = f64::max(adj[edge.to][edge.from], w);
119        }
120        adj
121    }
122
123    /// Degree matrix (diagonal): D[i][i] = sum of weights incident to node i.
124    /// Uses undirected version of the graph.
125    pub fn degree_matrix(&self) -> Vec<Vec<f64>> {
126        let adj = self.undirected_adjacency();
127        let n = self.node_count();
128        let mut deg = vec![vec![0.0; n]; n];
129        for i in 0..n {
130            let mut d = 0.0;
131            for j in 0..n {
132                d += adj[i][j];
133            }
134            deg[i][i] = d;
135        }
136        deg
137    }
138
139    /// Neighbors of node i (undirected).
140    pub fn neighbors(&self, i: usize) -> Vec<usize> {
141        let mut neighbors = Vec::new();
142        for edge in &self.edges {
143            if edge.from == i {
144                neighbors.push(edge.to);
145            }
146            if edge.to == i {
147                neighbors.push(edge.from);
148            }
149        }
150        neighbors.sort();
151        neighbors.dedup();
152        neighbors
153    }
154
155    /// Check if the graph is connected (undirected BFS).
156    pub fn is_connected(&self) -> bool {
157        if self.node_count() == 0 {
158            return true;
159        }
160        let n = self.node_count();
161        let adj = self.undirected_adjacency();
162        let mut visited = vec![false; n];
163        let mut stack = vec![0usize];
164        visited[0] = true;
165        while let Some(node) = stack.pop() {
166            for j in 0..n {
167                if adj[node][j] > 0.0 && !visited[j] {
168                    visited[j] = true;
169                    stack.push(j);
170                }
171            }
172        }
173        visited.iter().all(|&v| v)
174    }
175
176    /// Number of connected components.
177    pub fn connected_components(&self) -> Vec<Vec<usize>> {
178        let n = self.node_count();
179        if n == 0 {
180            return Vec::new();
181        }
182        let adj = self.undirected_adjacency();
183        let mut visited = vec![false; n];
184        let mut components = Vec::new();
185        for start in 0..n {
186            if visited[start] {
187                continue;
188            }
189            let mut component = Vec::new();
190            let mut stack = vec![start];
191            visited[start] = true;
192            while let Some(node) = stack.pop() {
193                component.push(node);
194                for j in 0..n {
195                    if adj[node][j] > 0.0 && !visited[j] {
196                        visited[j] = true;
197                        stack.push(j);
198                    }
199                }
200            }
201            components.push(component);
202        }
203        components
204    }
205
206    /// Build from GitHub org data: repos as nodes, dependencies as edges.
207    /// Each repo has a name (agent id) and list of capabilities (languages, topics).
208    /// Dependencies are edges weighted by coupling strength.
209    pub fn from_github_org(
210        repos: Vec<(String, Vec<String>)>,
211        dependencies: Vec<(String, String, f64)>,
212    ) -> Self {
213        let mut graph = Self::new();
214        // Map repo names to indices.
215        let mut name_to_idx = std::collections::HashMap::new();
216        for (name, caps) in &repos {
217            let idx = graph.add_agent(AgentNode::new(name.as_str(), caps.clone(), 0.0));
218            name_to_idx.insert(name.clone(), idx);
219        }
220        for (from_name, to_name, weight) in dependencies {
221            if let (Some(&from), Some(&to)) =
222                (name_to_idx.get(&from_name), name_to_idx.get(&to_name))
223            {
224                graph.add_edge(CommEdge::new(from, to, weight, 1.0));
225            }
226        }
227        graph
228    }
229
230    /// Create a complete graph on n nodes (all pairs connected).
231    pub fn complete(n: usize) -> Self {
232        let mut graph = Self::new();
233        for i in 0..n {
234            graph.add_agent(AgentNode::new(
235                format!("agent-{i}"),
236                vec!["general".into()],
237                0.0,
238            ));
239        }
240        for i in 0..n {
241            for j in 0..n {
242                if i != j {
243                    graph.add_edge(CommEdge::new(i, j, 1.0, 1.0));
244                }
245            }
246        }
247        graph
248    }
249
250    /// Create a path graph: 0-1-2-...-(n-1).
251    pub fn path(n: usize) -> Self {
252        let mut graph = Self::new();
253        for i in 0..n {
254            graph.add_agent(AgentNode::new(
255                format!("agent-{i}"),
256                vec!["general".into()],
257                0.0,
258            ));
259        }
260        for i in 0..n.saturating_sub(1) {
261            graph.add_edge(CommEdge::new(i, i + 1, 1.0, 1.0));
262            graph.add_edge(CommEdge::new(i + 1, i, 1.0, 1.0));
263        }
264        graph
265    }
266
267    /// Create a cycle graph: 0-1-2-...-(n-1)-0.
268    pub fn cycle(n: usize) -> Self {
269        let mut graph = Self::path(n);
270        if n > 2 {
271            graph.add_edge(CommEdge::new(n - 1, 0, 1.0, 1.0));
272            graph.add_edge(CommEdge::new(0, n - 1, 1.0, 1.0));
273        }
274        graph
275    }
276
277    /// Create a star graph: center node 0 connected to all others.
278    pub fn star(n: usize) -> Self {
279        let mut graph = Self::new();
280        for i in 0..n {
281            graph.add_agent(AgentNode::new(
282                format!("agent-{i}"),
283                vec!["general".into()],
284                0.0,
285            ));
286        }
287        for i in 1..n {
288            graph.add_edge(CommEdge::new(0, i, 1.0, 1.0));
289            graph.add_edge(CommEdge::new(i, 0, 1.0, 1.0));
290        }
291        graph
292    }
293
294    /// Create a barbell graph: two cliques of size k joined by a single bridge edge.
295    pub fn barbell(k: usize) -> Self {
296        let n = 2 * k;
297        let mut graph = Self::new();
298        for i in 0..n {
299            graph.add_agent(AgentNode::new(
300                format!("agent-{i}"),
301                vec!["general".into()],
302                0.0,
303            ));
304        }
305        // Left clique
306        for i in 0..k {
307            for j in 0..k {
308                if i != j {
309                    graph.add_edge(CommEdge::new(i, j, 1.0, 1.0));
310                }
311            }
312        }
313        // Right clique
314        for i in k..n {
315            for j in k..n {
316                if i != j {
317                    graph.add_edge(CommEdge::new(i, j, 1.0, 1.0));
318                }
319            }
320        }
321        // Bridge
322        graph.add_edge(CommEdge::new(k - 1, k, 1.0, 1.0));
323        graph.add_edge(CommEdge::new(k, k - 1, 1.0, 1.0));
324        graph
325    }
326
327    /// Create two disconnected cliques of size k each.
328    pub fn two_cliques(k: usize) -> Self {
329        let n = 2 * k;
330        let mut graph = Self::new();
331        for i in 0..n {
332            graph.add_agent(AgentNode::new(
333                format!("agent-{i}"),
334                vec!["general".into()],
335                0.0,
336            ));
337        }
338        // Left clique
339        for i in 0..k {
340            for j in 0..k {
341                if i != j {
342                    graph.add_edge(CommEdge::new(i, j, 1.0, 1.0));
343                }
344            }
345        }
346        // Right clique
347        for i in k..n {
348            for j in k..n {
349                if i != j {
350                    graph.add_edge(CommEdge::new(i, j, 1.0, 1.0));
351                }
352            }
353        }
354        graph
355    }
356
357    /// Create an Erdős–Rényi random graph with n nodes and edge probability p.
358    pub fn random(n: usize, p: f64, seed: u64) -> Self {
359        let mut graph = Self::new();
360        for i in 0..n {
361            graph.add_agent(AgentNode::new(
362                format!("agent-{i}"),
363                vec!["general".into()],
364                0.0,
365            ));
366        }
367        // Simple LCG PRNG
368        let mut rng = seed;
369        let next_rand = |rng: &mut u64| {
370            *rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
371            (*rng >> 33) as f64 / (1u64 << 31) as f64
372        };
373        for i in 0..n {
374            for j in 0..n {
375                if i != j && next_rand(&mut rng) < p {
376                    graph.add_edge(CommEdge::new(i, j, 1.0, 1.0));
377                    graph.add_edge(CommEdge::new(j, i, 1.0, 1.0));
378                }
379            }
380        }
381        graph
382    }
383}
384
385impl Default for FleetGraph {
386    fn default() -> Self {
387        Self::new()
388    }
389}