Skip to main content

uni_algo/algo/algorithms/
graph_metrics.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Graph Metrics Algorithm.
5//!
6//! Computes structural metrics:
7//! - Eccentricity for each node
8//! - Diameter (max eccentricity)
9//! - Radius (min eccentricity)
10//! - Center (nodes with min eccentricity)
11//! - Periphery (nodes with max eccentricity)
12//!
13//! Uses BFS (unweighted) or Dijkstra (weighted) from every node.
14//! Complexity: O(V * (V + E)) or O(V * (V + E) * log V).
15
16use crate::algo::GraphProjection;
17use crate::algo::algorithms::Algorithm;
18use rayon::prelude::*;
19use std::cmp::Reverse;
20use std::collections::BinaryHeap;
21use uni_common::core::id::Vid;
22
23pub struct GraphMetrics;
24
25#[derive(Debug, Clone, Default)]
26pub struct GraphMetricsConfig {}
27
28pub struct GraphMetricsResult {
29    pub diameter: f64,
30    pub radius: f64,
31    pub eccentricities: Vec<(Vid, f64)>,
32    pub center: Vec<Vid>,
33    pub periphery: Vec<Vid>,
34}
35
36impl Algorithm for GraphMetrics {
37    type Config = GraphMetricsConfig;
38    type Result = GraphMetricsResult;
39
40    fn name() -> &'static str {
41        "graph_metrics"
42    }
43
44    fn run(graph: &GraphProjection, _config: Self::Config) -> Self::Result {
45        let n = graph.vertex_count();
46        if n == 0 {
47            return GraphMetricsResult {
48                diameter: 0.0,
49                radius: 0.0,
50                eccentricities: Vec::new(),
51                center: Vec::new(),
52                periphery: Vec::new(),
53            };
54        }
55
56        // Compute eccentricity for all nodes in parallel
57        let eccentricities: Vec<f64> = (0..n)
58            .into_par_iter()
59            .map(|i| compute_eccentricity(graph, i as u32))
60            .collect();
61
62        // Filter out INFINITY (disconnected components)
63        // Usually metrics are defined for connected component or max of components.
64        // If graph is disconnected, eccentricity is technically infinity?
65        // Standard definition usually considers the connected component or returns infinity.
66        // For practical purposes, let's ignore unreachable nodes (or max reachable distance).
67        // Convention: Eccentricity is max distance to any *reachable* node? No, standard is max distance.
68        // If not connected, diameter is infinity.
69        // Let's stick to max reachable distance (or 0 if isolated) to avoid infinity messing up stats?
70        // Or better: Max distance in the component of v.
71
72        let valid_eccs: Vec<f64> = eccentricities
73            .iter()
74            .copied()
75            .filter(|&e| e < f64::INFINITY)
76            .collect();
77
78        if valid_eccs.is_empty() {
79            return GraphMetricsResult {
80                diameter: 0.0,
81                radius: 0.0,
82                eccentricities: Vec::new(),
83                center: Vec::new(),
84                periphery: Vec::new(),
85            };
86        }
87
88        let diameter = valid_eccs.iter().fold(0.0f64, |a, &b| a.max(b));
89        let radius = valid_eccs.iter().fold(f64::INFINITY, |a, &b| a.min(b));
90
91        let mut center = Vec::new();
92        let mut periphery = Vec::new();
93        let mut ecc_mapped = Vec::new();
94
95        for (i, &ecc) in eccentricities.iter().enumerate() {
96            if ecc < f64::INFINITY {
97                ecc_mapped.push((graph.to_vid(i as u32), ecc));
98                if (ecc - radius).abs() < 1e-9 {
99                    center.push(graph.to_vid(i as u32));
100                }
101                if (ecc - diameter).abs() < 1e-9 {
102                    periphery.push(graph.to_vid(i as u32));
103                }
104            }
105        }
106
107        GraphMetricsResult {
108            diameter,
109            radius,
110            eccentricities: ecc_mapped,
111            center,
112            periphery,
113        }
114    }
115}
116
117fn compute_eccentricity(graph: &GraphProjection, start: u32) -> f64 {
118    // Dijkstra (handles weighted and unweighted if weights=1.0)
119    let n = graph.vertex_count();
120    let mut dist = vec![f64::INFINITY; n];
121    let mut heap = BinaryHeap::new();
122
123    dist[start as usize] = 0.0;
124    heap.push(Reverse((0.0f64.to_bits(), start)));
125
126    let mut max_dist = 0.0;
127
128    while let Some(Reverse((d_bits, u))) = heap.pop() {
129        let d = f64::from_bits(d_bits);
130        if d > dist[u as usize] {
131            continue;
132        }
133        if d > max_dist {
134            max_dist = d;
135        }
136
137        for (i, &v) in graph.out_neighbors(u).iter().enumerate() {
138            let weight = if graph.has_weights() {
139                graph.out_weight(u, i)
140            } else {
141                1.0
142            };
143            let new_dist = d + weight;
144            if new_dist < dist[v as usize] {
145                dist[v as usize] = new_dist;
146                heap.push(Reverse((new_dist.to_bits(), v)));
147            }
148        }
149    }
150
151    // Check if we visited all? If graph is disconnected, max_dist is max of connected component.
152    // If we strictly define eccentricity as max distance to ANY node, it's infinity if disconnected.
153    // But for "Graph Metrics" in tools like Gephi/Neo4j, usually it's per component or largest component.
154    // Let's return max_dist (max reachable).
155
156    max_dist
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::algo::test_utils::build_test_graph;
163
164    #[test]
165    fn test_metrics_line() {
166        // 0-1-2-3
167        // Ecc: 0->3, 1->2, 2->2, 3->3
168        // Radius: 2 (nodes 1, 2)
169        // Diameter: 3
170        // Center: 1, 2
171        // Periphery: 0, 3
172
173        let vids = (0..4).map(Vid::from).collect();
174        // Undirected structure via bidirectional edges for testing metrics logic
175        let edges = vec![
176            (Vid::from(0), Vid::from(1)),
177            (Vid::from(1), Vid::from(0)),
178            (Vid::from(1), Vid::from(2)),
179            (Vid::from(2), Vid::from(1)),
180            (Vid::from(2), Vid::from(3)),
181            (Vid::from(3), Vid::from(2)),
182        ];
183        let graph = build_test_graph(vids, edges);
184
185        let result = GraphMetrics::run(&graph, GraphMetricsConfig::default());
186
187        assert_eq!(result.diameter, 3.0);
188        assert_eq!(result.radius, 2.0);
189
190        assert!(result.center.contains(&Vid::from(1)));
191        assert!(result.center.contains(&Vid::from(2)));
192        assert_eq!(result.center.len(), 2);
193
194        assert!(result.periphery.contains(&Vid::from(0)));
195        assert!(result.periphery.contains(&Vid::from(3)));
196        assert_eq!(result.periphery.len(), 2);
197    }
198}