1use serde::{Deserialize, Serialize};
2use std::collections::{HashMap, HashSet, VecDeque};
3
4#[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>, }
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 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 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(¤t) {
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 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 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 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 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 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 pub fn constraint_reach(&self, constraint_id: &str) -> Vec<&Node> {
220 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 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 pub fn nodes(&self) -> &[Node] {
308 &self.nodes
309 }
310
311 pub fn edges(&self) -> &[Edge] {
313 &self.edges
314 }
315}
316
317impl Default for FleetGraph {
318 fn default() -> Self {
319 Self::new()
320 }
321}