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    pub fn diameter(&self) -> f32 {
260        let mut max_distance: f32 = 0.0;
261
262        for i in 0..self.vertices.len() {
263            for j in (i + 1)..self.vertices.len() {
264                if let Some(path) = self.shortest_path(i, j) {
265                    let mut distance = 0.0;
266                    for k in 0..(path.len() - 1) {
267                        distance += self.vertices[path[k]].distance_to(&self.vertices[path[k + 1]]);
268                    }
269                    max_distance = max_distance.max(distance);
270                }
271            }
272        }
273
274        max_distance
275    }
276}
277
278#[derive(Debug, Clone)]
279pub struct GraphAnalysis {
280    pub vertex_count: usize,
281    pub edge_count: usize,
282    pub is_connected: bool,
283    pub component_count: usize,
284    pub diameter: f32,
285    pub average_clustering: f32,
286}
287
288impl GraphAnalysis {
289    pub fn analyze(graph: &Graph) -> Self {
290        let components = graph.connected_components();
291
292        Self {
293            vertex_count: graph.vertex_count(),
294            edge_count: graph.edge_count(),
295            is_connected: graph.is_connected(),
296            component_count: components.len(),
297            diameter: graph.diameter(),
298            average_clustering: graph.average_clustering_coefficient(),
299        }
300    }
301}
302
303/// Analyze room connectivity patterns
304pub fn analyze_room_connectivity(room_centers: &[Point], connections: &[Edge]) -> GraphAnalysis {
305    let graph = Graph::new(room_centers.to_vec(), connections.to_vec());
306    GraphAnalysis::analyze(&graph)
307}