Skip to main content

terrain_forge/analysis/
graph.rs

1//! Graph-based analysis for room connectivity
2
3use crate::analysis::delaunay::{Edge, Point};
4use std::collections::{HashMap, HashSet, VecDeque};
5
6#[derive(Debug, Clone)]
7pub struct Graph {
8    pub vertices: Vec<Point>,
9    pub edges: Vec<Edge>,
10    adjacency: HashMap<usize, Vec<usize>>,
11}
12
13impl Graph {
14    pub fn new(vertices: Vec<Point>, edges: Vec<Edge>) -> Self {
15        let mut adjacency = HashMap::new();
16
17        for (i, _) in vertices.iter().enumerate() {
18            adjacency.insert(i, Vec::new());
19        }
20
21        for edge in &edges {
22            adjacency.entry(edge.a).or_default().push(edge.b);
23            adjacency.entry(edge.b).or_default().push(edge.a);
24        }
25
26        Self {
27            vertices,
28            edges,
29            adjacency,
30        }
31    }
32
33    pub fn from_delaunay(triangulation: &crate::analysis::delaunay::DelaunayTriangulation) -> Self {
34        Self::new(triangulation.points.clone(), triangulation.edges.clone())
35    }
36
37    pub fn vertex_count(&self) -> usize {
38        self.vertices.len()
39    }
40
41    pub fn edge_count(&self) -> usize {
42        self.edges.len()
43    }
44
45    pub fn neighbors(&self, vertex: usize) -> &[usize] {
46        self.adjacency
47            .get(&vertex)
48            .map(|v| v.as_slice())
49            .unwrap_or(&[])
50    }
51
52    pub fn is_connected(&self) -> bool {
53        if self.vertices.is_empty() {
54            return true;
55        }
56
57        let mut visited = HashSet::new();
58        let mut queue = VecDeque::new();
59        queue.push_back(0);
60        visited.insert(0);
61
62        while let Some(vertex) = queue.pop_front() {
63            for &neighbor in self.neighbors(vertex) {
64                if visited.insert(neighbor) {
65                    queue.push_back(neighbor);
66                }
67            }
68        }
69
70        visited.len() == self.vertices.len()
71    }
72
73    pub fn connected_components(&self) -> Vec<Vec<usize>> {
74        let mut visited = HashSet::new();
75        let mut components = Vec::new();
76
77        for i in 0..self.vertices.len() {
78            if !visited.contains(&i) {
79                let mut component = Vec::new();
80                let mut queue = VecDeque::new();
81                queue.push_back(i);
82                visited.insert(i);
83
84                while let Some(vertex) = queue.pop_front() {
85                    component.push(vertex);
86                    for &neighbor in self.neighbors(vertex) {
87                        if visited.insert(neighbor) {
88                            queue.push_back(neighbor);
89                        }
90                    }
91                }
92
93                components.push(component);
94            }
95        }
96
97        components
98    }
99
100    pub fn shortest_path(&self, start: usize, end: usize) -> Option<Vec<usize>> {
101        if start >= self.vertices.len() || end >= self.vertices.len() {
102            return None;
103        }
104
105        let mut distances = vec![f32::INFINITY; self.vertices.len()];
106        let mut previous = vec![None; self.vertices.len()];
107        let mut visited = HashSet::new();
108
109        #[derive(PartialEq)]
110        struct State {
111            cost: u32,
112            vertex: usize,
113        }
114
115        impl Eq for State {}
116
117        impl Ord for State {
118            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
119                other.cost.cmp(&self.cost)
120            }
121        }
122
123        impl PartialOrd for State {
124            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
125                Some(self.cmp(other))
126            }
127        }
128
129        let mut queue = std::collections::BinaryHeap::new();
130
131        distances[start] = 0.0;
132        queue.push(State {
133            cost: 0,
134            vertex: start,
135        });
136
137        while let Some(State { cost: _, vertex }) = queue.pop() {
138            if visited.contains(&vertex) {
139                continue;
140            }
141            visited.insert(vertex);
142
143            if vertex == end {
144                break;
145            }
146
147            for &neighbor in self.neighbors(vertex) {
148                if !visited.contains(&neighbor) {
149                    let edge_weight = self.vertices[vertex].distance_to(&self.vertices[neighbor]);
150                    let new_dist = distances[vertex] + edge_weight;
151
152                    if new_dist < distances[neighbor] {
153                        distances[neighbor] = new_dist;
154                        previous[neighbor] = Some(vertex);
155                        queue.push(State {
156                            cost: (new_dist * 1000.0) as u32, // Scale for integer comparison
157                            vertex: neighbor,
158                        });
159                    }
160                }
161            }
162        }
163
164        if distances[end] == f32::INFINITY {
165            return None;
166        }
167
168        let mut path = Vec::new();
169        let mut current = Some(end);
170        while let Some(vertex) = current {
171            path.push(vertex);
172            current = previous[vertex];
173        }
174        path.reverse();
175
176        Some(path)
177    }
178
179    pub fn minimum_spanning_tree(&self) -> Graph {
180        if self.vertices.is_empty() {
181            return Graph::new(Vec::new(), Vec::new());
182        }
183
184        let mut mst_edges = Vec::new();
185        let mut edges = self.edges.clone();
186        edges.sort_by(|a, b| {
187            let len_a = self.vertices[a.a].distance_to(&self.vertices[a.b]);
188            let len_b = self.vertices[b.a].distance_to(&self.vertices[b.b]);
189            len_a.partial_cmp(&len_b).unwrap()
190        });
191
192        let mut parent: Vec<usize> = (0..self.vertices.len()).collect();
193
194        fn find(parent: &mut [usize], x: usize) -> usize {
195            if parent[x] != x {
196                parent[x] = find(parent, parent[x]);
197            }
198            parent[x]
199        }
200
201        fn union(parent: &mut [usize], x: usize, y: usize) {
202            let px = find(parent, x);
203            let py = find(parent, y);
204            if px != py {
205                parent[px] = py;
206            }
207        }
208
209        for edge in edges {
210            let root_a = find(&mut parent, edge.a);
211            let root_b = find(&mut parent, edge.b);
212
213            if root_a != root_b {
214                mst_edges.push(edge);
215                union(&mut parent, edge.a, edge.b);
216
217                if mst_edges.len() == self.vertices.len() - 1 {
218                    break;
219                }
220            }
221        }
222
223        Graph::new(self.vertices.clone(), mst_edges)
224    }
225
226    pub fn clustering_coefficient(&self, vertex: usize) -> f32 {
227        let neighbors = self.neighbors(vertex);
228        if neighbors.len() < 2 {
229            return 0.0;
230        }
231
232        let mut edges_between_neighbors = 0;
233        for i in 0..neighbors.len() {
234            for j in (i + 1)..neighbors.len() {
235                let a = neighbors[i];
236                let b = neighbors[j];
237                if self.neighbors(a).contains(&b) {
238                    edges_between_neighbors += 1;
239                }
240            }
241        }
242
243        let possible_edges = neighbors.len() * (neighbors.len() - 1) / 2;
244        edges_between_neighbors as f32 / possible_edges as f32
245    }
246
247    pub fn average_clustering_coefficient(&self) -> f32 {
248        if self.vertices.is_empty() {
249            return 0.0;
250        }
251
252        let sum: f32 = (0..self.vertices.len())
253            .map(|i| self.clustering_coefficient(i))
254            .sum();
255
256        sum / self.vertices.len() as f32
257    }
258
259    /// Longest shortest-path distance between any two connected vertices.
260    ///
261    /// Runs Dijkstra from each vertex once: O(V ยท (V + E) log V).
262    pub fn diameter(&self) -> f32 {
263        let n = self.vertices.len();
264        let mut max_distance: f32 = 0.0;
265
266        for i in 0..n {
267            // Single-source Dijkstra from vertex i
268            let mut dist = vec![f32::INFINITY; n];
269            let mut visited = vec![false; n];
270            dist[i] = 0.0;
271
272            let mut queue = std::collections::BinaryHeap::new();
273            queue.push(std::cmp::Reverse((0u32, i)));
274
275            while let Some(std::cmp::Reverse((_, v))) = queue.pop() {
276                if visited[v] {
277                    continue;
278                }
279                visited[v] = true;
280
281                for &nb in self.neighbors(v) {
282                    let w = self.vertices[v].distance_to(&self.vertices[nb]);
283                    let nd = dist[v] + w;
284                    if nd < dist[nb] {
285                        dist[nb] = nd;
286                        queue.push(std::cmp::Reverse(((nd * 1000.0) as u32, nb)));
287                    }
288                }
289            }
290
291            for d in &dist {
292                if d.is_finite() {
293                    max_distance = max_distance.max(*d);
294                }
295            }
296        }
297
298        max_distance
299    }
300}
301
302#[derive(Debug, Clone)]
303pub struct GraphAnalysis {
304    pub vertex_count: usize,
305    pub edge_count: usize,
306    pub is_connected: bool,
307    pub component_count: usize,
308    pub diameter: f32,
309    pub average_clustering: f32,
310}
311
312impl GraphAnalysis {
313    pub fn analyze(graph: &Graph) -> Self {
314        let components = graph.connected_components();
315
316        Self {
317            vertex_count: graph.vertex_count(),
318            edge_count: graph.edge_count(),
319            is_connected: graph.is_connected(),
320            component_count: components.len(),
321            diameter: graph.diameter(),
322            average_clustering: graph.average_clustering_coefficient(),
323        }
324    }
325}
326
327/// Analyze room connectivity patterns
328pub fn analyze_room_connectivity(room_centers: &[Point], connections: &[Edge]) -> GraphAnalysis {
329    let graph = Graph::new(room_centers.to_vec(), connections.to_vec());
330    GraphAnalysis::analyze(&graph)
331}