Skip to main content

fleet_topology/
graph.rs

1use serde::{Deserialize, Serialize};
2use std::collections::{HashMap, HashSet, VecDeque};
3
4/// Fleet topology graph.
5/// Nodes are devices, edges are communication links.
6/// Constraints propagate along edges.
7/// Holonomy (cycle consistency) verified on every cycle.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FleetGraph {
10    nodes: Vec<Node>,
11    edges: Vec<Edge>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Node {
16    pub id: String,
17    pub device_type: DeviceType,
18    pub capability: Capability,
19    pub constraints: Vec<String>, // constraint IDs
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Edge {
24    pub from: String,
25    pub to: String,
26    pub latency_us: u64,
27    pub bandwidth_bps: u64,
28    pub constraint_flow: bool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum DeviceType {
33    RobotArm,
34    SensorArray,
35    Esp32,
36    Jetson,
37    Fpga,
38    Gateway,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub enum Capability {
43    Raw,
44    Aware,
45    Enforcing,
46    Commander,
47}
48
49impl FleetGraph {
50    pub fn new() -> Self {
51        FleetGraph {
52            nodes: Vec::new(),
53            edges: Vec::new(),
54        }
55    }
56
57    pub fn add_node(&mut self, node: Node) {
58        self.nodes.push(node);
59    }
60
61    pub fn add_edge(&mut self, edge: Edge) {
62        self.edges.push(edge);
63    }
64
65    /// Find all cycles using Johnson's algorithm (simplified DFS-based approach).
66    pub fn find_cycles(&self) -> Vec<Vec<String>> {
67        let mut cycles = Vec::new();
68        let node_ids: Vec<&str> = self.nodes.iter().map(|n| n.id.as_str()).collect();
69        let adj = self.build_adjacency();
70
71        let mut visited = HashSet::new();
72        let mut stack = Vec::new();
73        let mut in_stack = HashSet::new();
74
75        for start in &node_ids {
76            visited.clear();
77            stack.clear();
78            in_stack.clear();
79            self.dfs_cycles(
80                start,
81                start,
82                &adj,
83                &mut visited,
84                &mut stack,
85                &mut in_stack,
86                &mut cycles,
87            );
88        }
89
90        // Deduplicate cycles (normalize by rotating to smallest element)
91        let mut seen = HashSet::new();
92        let mut unique = Vec::new();
93        for cycle in cycles {
94            if cycle.len() < 2 {
95                continue;
96            }
97            let mut normalized = cycle.clone();
98            let min_pos = normalized.iter().enumerate().min_by_key(|(_, v)| *v).map(|(i, _)| i);
99            if let Some(pos) = min_pos {
100                normalized.rotate_left(pos);
101            }
102            let key = normalized.join("|");
103            if !seen.contains(&key) {
104                seen.insert(key);
105                unique.push(cycle);
106            }
107        }
108        unique
109    }
110
111    fn dfs_cycles(
112        &self,
113        current: &str,
114        start: &str,
115        adj: &HashMap<&str, Vec<&str>>,
116        visited: &mut HashSet<String>,
117        stack: &mut Vec<String>,
118        in_stack: &mut HashSet<String>,
119        cycles: &mut Vec<Vec<String>>,
120    ) {
121        visited.insert(current.to_string());
122        stack.push(current.to_string());
123        in_stack.insert(current.to_string());
124
125        if let Some(neighbors) = adj.get(&current) {
126            for &next in neighbors {
127                if next == start && stack.len() >= 2 {
128                    cycles.push(stack.clone());
129                } else if !visited.contains(next) && !in_stack.contains(next) {
130                    // Only follow edges to nodes >= start to avoid duplicate cycles
131                    if next >= start {
132                        self.dfs_cycles(next, start, adj, visited, stack, in_stack, cycles);
133                    }
134                }
135            }
136        }
137
138        stack.pop();
139        in_stack.remove(current);
140        visited.remove(current);
141    }
142
143    fn build_adjacency(&self) -> HashMap<&str, Vec<&str>> {
144        let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
145        for edge in &self.edges {
146            adj.entry(&edge.from).or_default().push(&edge.to);
147            adj.entry(&edge.to).or_default().push(&edge.from);
148        }
149        adj
150    }
151
152    /// Compute Betti number β₁ = |E| - |V| + c
153    pub fn betti1(&self) -> usize {
154        let c = self.connected_components();
155        if self.edges.len() >= self.nodes.len() {
156            self.edges.len() - self.nodes.len() + c
157        } else {
158            0
159        }
160    }
161
162    /// Find path between two nodes (BFS)
163    pub fn find_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
164        let adj = self.build_adjacency();
165        let mut queue = VecDeque::new();
166        let mut parent: HashMap<String, Option<String>> = HashMap::new();
167
168        queue.push_back(from.to_string());
169        parent.insert(from.to_string(), None);
170
171        while let Some(current) = queue.pop_front() {
172            if current == to {
173                // Reconstruct path
174                let mut path = Vec::new();
175                let mut node = Some(to.to_string());
176                while let Some(n) = node {
177                    path.push(n.clone());
178                    node = parent.get(&n).and_then(|p| p.clone());
179                }
180                path.reverse();
181                return Some(path);
182            }
183
184            if let Some(neighbors) = adj.get(current.as_str()) {
185                for neighbor in neighbors {
186                    let n = neighbor.to_string();
187                    if !parent.contains_key(&n) {
188                        parent.insert(n.clone(), Some(current.clone()));
189                        queue.push_back(n);
190                    }
191                }
192            }
193        }
194        None
195    }
196
197    /// Get neighbors of a node
198    pub fn neighbors(&self, node_id: &str) -> Vec<&Node> {
199        let neighbor_ids: HashSet<&str> = self
200            .edges
201            .iter()
202            .filter(|e| e.from == node_id || e.to == node_id)
203            .map(|e| {
204                if e.from == node_id {
205                    e.to.as_str()
206                } else {
207                    e.from.as_str()
208                }
209            })
210            .collect();
211
212        self.nodes
213            .iter()
214            .filter(|n| neighbor_ids.contains(n.id.as_str()))
215            .collect()
216    }
217
218    /// Constraint propagation: find all devices that should know about a constraint
219    pub fn constraint_reach(&self, constraint_id: &str) -> Vec<&Node> {
220        // Start from nodes that have this constraint, propagate along constraint_flow edges
221        let sources: HashSet<&str> = self
222            .nodes
223            .iter()
224            .filter(|n| n.constraints.contains(&constraint_id.to_string()))
225            .map(|n| n.id.as_str())
226            .collect();
227
228        if sources.is_empty() {
229            return Vec::new();
230        }
231
232        let constraint_adj: HashMap<&str, Vec<&str>> = self
233            .edges
234            .iter()
235            .filter(|e| e.constraint_flow)
236            .fold(HashMap::new(), |mut acc, e| {
237                acc.entry(&e.from).or_default().push(&e.to);
238                acc.entry(&e.to).or_default().push(&e.from);
239                acc
240            });
241
242        let mut reachable = HashSet::new();
243        let mut queue: VecDeque<&str> = sources.iter().copied().collect();
244        for &s in &sources {
245            reachable.insert(s);
246        }
247
248        while let Some(current) = queue.pop_front() {
249            if let Some(neighbors) = constraint_adj.get(current) {
250                for &next in neighbors {
251                    let _next_s = next.to_string();
252                    if !reachable.contains(next) {
253                        reachable.insert(next);
254                        queue.push_back(next);
255                    }
256                }
257            }
258        }
259
260        self.nodes
261            .iter()
262            .filter(|n| reachable.contains(n.id.as_str()))
263            .collect()
264    }
265
266    /// Check if graph is connected
267    pub fn is_connected(&self) -> bool {
268        if self.nodes.is_empty() {
269            return true;
270        }
271        self.connected_components() <= 1
272    }
273
274    fn connected_components(&self) -> usize {
275        if self.nodes.is_empty() {
276            return 0;
277        }
278
279        let adj = self.build_adjacency();
280        let mut visited: HashSet<&str> = HashSet::new();
281        let mut components = 0;
282
283        for node in &self.nodes {
284            if visited.contains(node.id.as_str()) {
285                continue;
286            }
287            components += 1;
288            let mut queue = VecDeque::new();
289            queue.push_back(node.id.as_str());
290            visited.insert(node.id.as_str());
291
292            while let Some(current) = queue.pop_front() {
293                if let Some(neighbors) = adj.get(current) {
294                    for &next in neighbors {
295                        if !visited.contains(next) {
296                            visited.insert(next);
297                            queue.push_back(next);
298                        }
299                    }
300                }
301            }
302        }
303        components
304    }
305
306    /// Get reference to nodes
307    pub fn nodes(&self) -> &[Node] {
308        &self.nodes
309    }
310
311    /// Get reference to edges
312    pub fn edges(&self) -> &[Edge] {
313        &self.edges
314    }
315}
316
317impl Default for FleetGraph {
318    fn default() -> Self {
319        Self::new()
320    }
321}