Skip to main content

fleet_coordinate/
graph.rs

1//! Fleet Constraint Graph — Laman Rigidity + H¹ Cohomology
2//!
3//! Laman's theorem (1867): A graph with V vertices is generically rigid in 2D
4//! iff it has exactly 2V-3 edges and every subgraph with v' vertices has
5//! at most 2v'-3 edges.
6//!
7//! Key caveat: Laman's theorem establishes the edge count condition (E=2V-3 for generic rigidity in 2D) and the subgraph condition (every subgraph has at most 2v'-3 edges), but does NOT place an upper bound on vertex degree. A Laman graph can have vertices of arbitrarily high degree.
8//!
9//! This maps directly to fleet coordination:
10//! - Vertices = agents
11//! - Edges = trust/communication links
12//! - Rigid graph = provably self-coordinating fleet (no central coordinator)
13//!
14//! H¹ dimension = number of independent cycles = number of redundant
15//! constraint paths = "emergence" in the network.
16
17#![allow(non_snake_case)]
18use serde::{Deserialize, Serialize};
19use std::collections::{HashMap, HashSet};
20
21/// One agent in the fleet
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct FleetAgent {
24    pub id: u64,
25    pub position: [f64; 2],
26    pub neighbors: Vec<u64>,
27    pub capabilities: Vec<String>,
28}
29
30/// The fleet constraint graph
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct FleetGraph {
33    agents: Vec<FleetAgent>,
34    /// O(1) agent lookup by ID — mirrors FM's tile_index pattern
35    agent_index: HashMap<u64, usize>,
36    adjacency: HashMap<u64, HashSet<u64>>,
37    edge_count: usize,
38}
39
40impl FleetGraph {
41    pub fn new() -> Self {
42        Self {
43            agents: Vec::new(),
44            agent_index: HashMap::new(),
45            adjacency: HashMap::new(),
46            edge_count: 0,
47        }
48    }
49
50    pub fn add_agent(&mut self, id: u64, position: [f64; 2], capabilities: Vec<String>) {
51        if let std::collections::hash_map::Entry::Vacant(e) = self.adjacency.entry(id) {
52            e.insert(HashSet::new());
53            let idx = self.agents.len();
54            self.agent_index.insert(id, idx);
55            self.agents.push(FleetAgent { id, position, neighbors: vec![], capabilities });
56        }
57    }
58
59    pub fn add_edge(&mut self, a: u64, b: u64) {
60        // Only count as one edge (undirected) even though we store both directions
61        let mut new_edge = false;
62        if let Some(set) = self.adjacency.get_mut(&a) {
63            if set.insert(b) {
64                new_edge = true;
65                if let Some(agent) = self.agents.iter_mut().find(|ag| ag.id == a) {
66                    if !agent.neighbors.contains(&b) {
67                        agent.neighbors.push(b);
68                    }
69                }
70            }
71        }
72        if let Some(set) = self.adjacency.get_mut(&b) {
73            if set.insert(a) {
74                if new_edge {
75                    self.edge_count += 1;
76                }
77                if let Some(agent) = self.agents.iter_mut().find(|ag| ag.id == b) {
78                    if !agent.neighbors.contains(&a) {
79                        agent.neighbors.push(a);
80                    }
81                }
82            }
83        }
84    }
85
86    pub fn V(&self) -> usize {
87        self.agents.len()
88    }
89
90    pub fn E(&self) -> usize {
91        self.edge_count
92    }
93
94    /// Check Laman rigidity condition
95    ///
96    /// A graph is generically rigid (2D) iff:
97    /// 1. E = 2V - 3 (approximately — allow 5% tolerance for boundary cases)
98    /// 2. Every subgraph with v' vertices has E' ≤ 2v' - 3
99    ///
100    /// For small graphs (V < 3), just check main condition.
101    pub fn check_laman_rigidity(&self) -> RigidityResult {
102        let V = self.V();
103        let E = self.E();
104
105        // Condition 1: E = 2V - 3 (with tolerance for small graphs)
106        let expected_E = 2 * V - 3;
107        let e_ratio = if expected_E > 0 { E as f64 / expected_E as f64 } else { 1.0 };
108
109        // For V < 3, skip subgraph check (triangle is minimum rigid structure)
110        let subgraph_check = if V < 3 {
111            true
112        } else {
113            let mut ok = true;
114            for agent in &self.agents {
115                let v_prime = agent.neighbors.len() + 1;
116                let e_prime = agent.neighbors.len();
117                if v_prime >= 2 && e_prime > 2 * v_prime - 3 {
118                    ok = false;
119                    break;
120                }
121            }
122            ok
123        };
124
125        let is_rigid = (e_ratio - 1.0).abs() < 0.05 && subgraph_check; // 5% tolerance
126        
127        // Betti number β₁ = E - V + C (number of independent cycles)
128        let H1 = if E >= V { E - V + 1 } else { 0 };
129
130        RigidityResult {
131            is_rigid,
132            V,
133            E,
134            expected_E,
135            h1_dimension: H1,
136            critical_subgraphs: vec![],
137            max_neighbors: self.agents.iter().map(|a| a.neighbors.len()).max().unwrap_or(0),
138        }
139    }
140
141    /// Maximum neighbors for any agent (Laman's theorem: 2V-3 for V≥3)
142    pub fn max_neighbors(&self) -> usize {
143        self.agents.iter().map(|a| a.neighbors.len()).max().unwrap_or(0)
144    }
145
146    /// Get agent by ID — O(1) via HashMap index
147    pub fn get_agent(&self, id: u64) -> Option<&FleetAgent> {
148        self.agent_index.get(&id).and_then(|&idx| self.agents.get(idx))
149    }
150
151    /// Get neighbors of an agent
152    pub fn get_neighbors(&self, id: u64) -> Vec<u64> {
153        self.adjacency.get(&id).map(|s| s.iter().copied().collect::<Vec<u64>>()).unwrap_or_default()
154    }
155}
156
157impl Default for FleetGraph {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163/// Result of rigidity analysis
164#[derive(Clone, Debug, Serialize, Deserialize)]
165pub struct RigidityResult {
166    /// True if graph is generically rigid (2D)
167    pub is_rigid: bool,
168    pub V: usize,
169    pub E: usize,
170    pub expected_E: usize,
171    /// H¹ dimension = number of independent cycles
172    pub h1_dimension: usize,
173    /// Agent IDs of over-constrained subgraphs
174    pub critical_subgraphs: Vec<u64>,
175    /// Maximum neighbor count across all agents
176    pub max_neighbors: usize,
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_laman_rigid_triangle() {
185        // 3 agents, 3 edges (K3) — 2*3-3 = 3 ✓
186        let mut graph = FleetGraph::new();
187        graph.add_agent(1, [0.0, 0.0], vec![]);
188        graph.add_agent(2, [1.0, 0.0], vec![]);
189        graph.add_agent(3, [0.5, 0.87], vec![]);
190        graph.add_edge(1, 2);
191        graph.add_edge(2, 3);
192        graph.add_edge(3, 1);
193
194        let result = graph.check_laman_rigidity();
195        assert!(result.is_rigid);
196        assert_eq!(result.V, 3);
197        assert_eq!(result.E, 3);
198        assert_eq!(result.max_neighbors, 2);
199    }
200
201    #[test]
202    fn test_laman_indeterminate_square() {
203        // 4 agents, 4 edges (square) — 2*4-3 = 5, we have 4 — not rigid
204        let mut graph = FleetGraph::new();
205        graph.add_agent(1, [0.0, 0.0], vec![]);
206        graph.add_agent(2, [1.0, 0.0], vec![]);
207        graph.add_agent(3, [1.0, 1.0], vec![]);
208        graph.add_agent(4, [0.0, 1.0], vec![]);
209        graph.add_edge(1, 2);
210        graph.add_edge(2, 3);
211        graph.add_edge(3, 4);
212        graph.add_edge(4, 1);
213
214        let result = graph.check_laman_rigidity();
215        assert!(!result.is_rigid);
216        assert_eq!(result.max_neighbors, 2);
217    }
218
219    #[test]
220    fn test_k12_complete_graph() {
221        // K12: 12 agents, 12*11/2 = 66 edges
222        // 2V-3 = 21, but K12 has 66 > 21 — over-constrained
223        let mut graph = FleetGraph::new();
224        for i in 0..12 {
225            graph.add_agent(i as u64, [i as f64, 0.0], vec![]);
226        }
227        for i in 0..12 {
228            for j in (i+1)..12 {
229                graph.add_edge(i as u64, j as u64);
230            }
231        }
232
233        let result = graph.check_laman_rigidity();
234        assert!(!result.is_rigid); // Over-constrained
235        assert_eq!(result.max_neighbors, 11);
236    }
237
238    #[test]
239    fn test_h1_dimension_cycle() {
240        // Triangle: 3 vertices, 3 edges → H1 = E-V+1 = 3-3+1 = 1 (one independent cycle)
241        let mut graph = FleetGraph::new();
242        graph.add_agent(0, [0.0, 0.0], vec![]);
243        graph.add_agent(1, [1.0, 0.0], vec![]);
244        graph.add_agent(2, [0.0, 1.0], vec![]);
245        graph.add_edge(0, 1);
246        graph.add_edge(1, 2);
247        graph.add_edge(2, 0);
248
249        let result = graph.check_laman_rigidity();
250        assert_eq!(result.h1_dimension, 1);
251        assert!(result.is_rigid); // K3 is Laman-rigid
252    }
253
254    #[test]
255    fn test_o1_agent_lookup() {
256        // Prove O(1) lookup via HashMap index (not iter().find())
257        let mut graph = FleetGraph::new();
258        for i in 0..100 {
259            graph.add_agent(i, [i as f64, 0.0], vec![format!("cap_{}", i)]);
260        }
261        // O(1) lookup: constant time regardless of position
262        let start = std::time::Instant::now();
263        for i in 0..100 {
264            let agent = graph.get_agent(i);
265            assert!(agent.is_some());
266            assert_eq!(agent.unwrap().id, i);
267        }
268        let elapsed = start.elapsed();
269        // Should be very fast (< 1ms for 100 lookups)
270        assert!(elapsed.as_secs_f64() < 0.001, "O(1) lookup took {}s for 100 agents", elapsed.as_secs_f64());
271    }
272
273    #[test]
274    fn test_o1_lookup_not_found() {
275        let mut graph = FleetGraph::new();
276        graph.add_agent(42, [0.0, 0.0], vec![]);
277        assert!(graph.get_agent(999).is_none());
278    }
279
280    #[test]
281    fn test_agent_index_consistency() {
282        // Ensure index stays consistent after adding many agents
283        let mut graph = FleetGraph::new();
284        for i in 0..50 {
285            graph.add_agent(i * 2, [i as f64, 0.0], vec![]);
286        }
287        for i in 0..50 {
288            let agent = graph.get_agent(i * 2).expect(&format!("agent {} should exist", i * 2));
289            assert_eq!(agent.id, i * 2);
290        }
291    }
292}