oximedia_graph/
metrics_graph.rs1use std::collections::{HashMap, HashSet, VecDeque};
4
5#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct MetricsGraph {
9 adjacency: HashMap<usize, HashSet<usize>>,
11 directed: bool,
13}
14
15impl MetricsGraph {
16 #[allow(dead_code)]
18 pub fn new_directed() -> Self {
19 Self {
20 adjacency: HashMap::new(),
21 directed: true,
22 }
23 }
24
25 #[allow(dead_code)]
27 pub fn new_undirected() -> Self {
28 Self {
29 adjacency: HashMap::new(),
30 directed: false,
31 }
32 }
33
34 #[allow(dead_code)]
36 pub fn add_node(&mut self, id: usize) {
37 self.adjacency.entry(id).or_default();
38 }
39
40 #[allow(dead_code)]
42 pub fn add_edge(&mut self, from: usize, to: usize) {
43 self.adjacency.entry(from).or_default().insert(to);
44 self.adjacency.entry(to).or_default(); if !self.directed {
46 self.adjacency.entry(to).or_default().insert(from);
47 }
48 }
49
50 #[allow(dead_code)]
52 pub fn node_count(&self) -> usize {
53 self.adjacency.len()
54 }
55
56 #[allow(dead_code)]
58 pub fn edge_count(&self) -> usize {
59 let total: usize = self.adjacency.values().map(|n| n.len()).sum();
60 if self.directed {
61 total
62 } else {
63 total / 2
64 }
65 }
66
67 #[allow(dead_code)]
69 fn bfs_distances(&self, source: usize) -> HashMap<usize, usize> {
70 let mut dist = HashMap::new();
71 let mut queue = VecDeque::new();
72 dist.insert(source, 0usize);
73 queue.push_back(source);
74
75 while let Some(u) = queue.pop_front() {
76 let d = dist[&u];
77 if let Some(neighbors) = self.adjacency.get(&u) {
78 for &v in neighbors {
79 if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(v) {
80 e.insert(d + 1);
81 queue.push_back(v);
82 }
83 }
84 }
85 }
86 dist
87 }
88
89 #[allow(dead_code)]
92 pub fn diameter(&self) -> Option<usize> {
93 if self.adjacency.is_empty() {
94 return None;
95 }
96
97 let nodes: Vec<usize> = self.adjacency.keys().copied().collect();
98 let mut max_dist = 0usize;
99 let mut found_any = false;
100
101 for &source in &nodes {
102 let distances = self.bfs_distances(source);
103 for (&target, &d) in &distances {
104 if target != source {
105 found_any = true;
106 if d > max_dist {
107 max_dist = d;
108 }
109 }
110 }
111 }
112
113 if found_any {
114 Some(max_dist)
115 } else {
116 None
117 }
118 }
119
120 #[allow(dead_code)]
125 pub fn local_clustering_coefficient(&self, node: usize) -> f64 {
126 let neighbors = match self.adjacency.get(&node) {
127 Some(n) => n,
128 None => return 0.0,
129 };
130
131 let k = neighbors.len();
132 if k < 2 {
133 return 0.0;
134 }
135
136 let neighbors_vec: Vec<usize> = neighbors.iter().copied().collect();
137 let mut triangle_edges = 0usize;
138
139 for i in 0..k {
140 for j in (i + 1)..k {
141 let u = neighbors_vec[i];
142 let v = neighbors_vec[j];
143 if let Some(u_neighbors) = self.adjacency.get(&u) {
145 if u_neighbors.contains(&v) {
146 triangle_edges += 1;
147 }
148 }
149 }
150 }
151
152 2.0 * triangle_edges as f64 / (k * (k - 1)) as f64
153 }
154
155 #[allow(dead_code)]
157 pub fn average_clustering_coefficient(&self) -> f64 {
158 if self.adjacency.is_empty() {
159 return 0.0;
160 }
161 let nodes: Vec<usize> = self.adjacency.keys().copied().collect();
162 let sum: f64 = nodes
163 .iter()
164 .map(|&n| self.local_clustering_coefficient(n))
165 .sum();
166 sum / nodes.len() as f64
167 }
168
169 #[allow(dead_code)]
173 pub fn betweenness_centrality(&self) -> HashMap<usize, f64> {
174 let nodes: Vec<usize> = self.adjacency.keys().copied().collect();
175 let mut centrality: HashMap<usize, f64> = nodes.iter().map(|&n| (n, 0.0)).collect();
176
177 for &source in &nodes {
178 let mut stack: Vec<usize> = Vec::new();
180 let mut pred: HashMap<usize, Vec<usize>> =
181 nodes.iter().map(|&n| (n, Vec::new())).collect();
182 let mut sigma: HashMap<usize, f64> = nodes.iter().map(|&n| (n, 0.0)).collect();
183 let mut dist: HashMap<usize, i64> = nodes.iter().map(|&n| (n, -1)).collect();
184
185 sigma.insert(source, 1.0);
186 dist.insert(source, 0);
187
188 let mut queue = VecDeque::new();
189 queue.push_back(source);
190
191 while let Some(v) = queue.pop_front() {
192 stack.push(v);
193 let dv = dist[&v];
194
195 if let Some(neighbors) = self.adjacency.get(&v) {
196 for &w in neighbors {
197 let dw = dist[&w];
198 if dw < 0 {
199 queue.push_back(w);
200 dist.insert(w, dv + 1);
201 }
202 if dist[&w] == dv + 1 {
203 let sw = sigma[&v];
204 if let Some(sw_entry) = sigma.get_mut(&w) {
206 *sw_entry += sw;
207 }
208 if let Some(pred_entry) = pred.get_mut(&w) {
209 pred_entry.push(v);
210 }
211 }
212 }
213 }
214 }
215
216 let mut delta: HashMap<usize, f64> = nodes.iter().map(|&n| (n, 0.0)).collect();
218 while let Some(w) = stack.pop() {
219 let sw = sigma[&w];
220 let dw = delta[&w];
221 for &v in &pred[&w] {
222 let sv = sigma[&v];
223 let contribution = (sv / sw) * (1.0 + dw);
224 if let Some(dv_entry) = delta.get_mut(&v) {
226 *dv_entry += contribution;
227 }
228 }
229 if w != source {
230 let dw_val = delta[&w];
231 if let Some(c) = centrality.get_mut(&w) {
233 *c += dw_val;
234 }
235 }
236 }
237 }
238
239 centrality
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn make_path_graph(n: usize) -> MetricsGraph {
248 let mut g = MetricsGraph::new_undirected();
249 for i in 0..n {
250 g.add_node(i);
251 }
252 for i in 0..n.saturating_sub(1) {
253 g.add_edge(i, i + 1);
254 }
255 g
256 }
257
258 fn make_complete_graph(n: usize) -> MetricsGraph {
259 let mut g = MetricsGraph::new_undirected();
260 for i in 0..n {
261 g.add_node(i);
262 }
263 for i in 0..n {
264 for j in (i + 1)..n {
265 g.add_edge(i, j);
266 }
267 }
268 g
269 }
270
271 #[test]
272 fn test_node_count() {
273 let g = make_path_graph(4);
274 assert_eq!(g.node_count(), 4);
275 }
276
277 #[test]
278 fn test_edge_count_undirected() {
279 let g = make_path_graph(4);
280 assert_eq!(g.edge_count(), 3);
281 }
282
283 #[test]
284 fn test_diameter_path_graph() {
285 let g = make_path_graph(5);
286 assert_eq!(g.diameter(), Some(4));
287 }
288
289 #[test]
290 fn test_diameter_single_node() {
291 let mut g = MetricsGraph::new_undirected();
292 g.add_node(0);
293 assert_eq!(g.diameter(), None);
294 }
295
296 #[test]
297 fn test_diameter_empty() {
298 let g = MetricsGraph::new_undirected();
299 assert_eq!(g.diameter(), None);
300 }
301
302 #[test]
303 fn test_diameter_complete_graph() {
304 let g = make_complete_graph(4);
305 assert_eq!(g.diameter(), Some(1));
306 }
307
308 #[test]
309 fn test_clustering_coefficient_complete_graph() {
310 let g = make_complete_graph(4);
311 for i in 0..4 {
313 let cc = g.local_clustering_coefficient(i);
314 assert!((cc - 1.0).abs() < 1e-10, "node {i}: cc={cc}");
315 }
316 }
317
318 #[test]
319 fn test_clustering_coefficient_path_graph() {
320 let g = make_path_graph(5);
322 let cc = g.local_clustering_coefficient(2);
323 assert_eq!(cc, 0.0);
324 }
325
326 #[test]
327 fn test_clustering_coefficient_low_degree() {
328 let g = make_path_graph(4);
329 let cc = g.local_clustering_coefficient(0);
331 assert_eq!(cc, 0.0);
332 }
333
334 #[test]
335 fn test_average_clustering_complete() {
336 let g = make_complete_graph(4);
337 let avg = g.average_clustering_coefficient();
338 assert!((avg - 1.0).abs() < 1e-10);
339 }
340
341 #[test]
342 fn test_average_clustering_empty() {
343 let g = MetricsGraph::new_undirected();
344 assert_eq!(g.average_clustering_coefficient(), 0.0);
345 }
346
347 #[test]
348 fn test_betweenness_centrality_path() {
349 let g = make_path_graph(5);
351 let bc = g.betweenness_centrality();
352 let mid = bc[&2];
353 let end = bc[&0];
354 assert!(
355 mid > end,
356 "middle node should have higher betweenness than endpoint"
357 );
358 }
359
360 #[test]
361 fn test_betweenness_centrality_complete_symmetric() {
362 let g = make_complete_graph(4);
363 let bc = g.betweenness_centrality();
364 let vals: Vec<f64> = bc.values().copied().collect();
366 let first = vals[0];
367 for v in &vals {
368 assert!(
369 (v - first).abs() < 1e-9,
370 "values should be equal: {first} vs {v}"
371 );
372 }
373 }
374
375 #[test]
376 fn test_directed_graph_edge_count() {
377 let mut g = MetricsGraph::new_directed();
378 g.add_edge(0, 1);
379 g.add_edge(1, 2);
380 assert_eq!(g.edge_count(), 2);
381 }
382}