rustkernel_graph/
metrics.rs

1//! Graph-level metrics kernels.
2//!
3//! This module provides graph-wide metrics:
4//! - Graph density
5//! - Average path length
6//! - Diameter
7//! - Average clustering coefficient
8
9use crate::types::CsrGraph;
10use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
11use std::collections::VecDeque;
12
13/// Result of graph metrics computation.
14#[derive(Debug, Clone)]
15pub struct GraphMetricsResult {
16    /// Graph density: 2E / (V * (V-1))
17    pub density: f64,
18    /// Average shortest path length.
19    pub average_path_length: f64,
20    /// Graph diameter (longest shortest path).
21    pub diameter: u64,
22    /// Average clustering coefficient.
23    pub average_clustering_coefficient: f64,
24    /// Number of connected components.
25    pub num_components: usize,
26    /// Size of largest component.
27    pub largest_component_size: usize,
28    /// Is the graph connected?
29    pub is_connected: bool,
30}
31
32// ============================================================================
33// Graph Density Kernel
34// ============================================================================
35
36/// Graph density kernel.
37///
38/// Computes graph density: 2E / (V * (V-1)) for undirected graphs.
39#[derive(Debug, Clone)]
40pub struct GraphDensity {
41    metadata: KernelMetadata,
42}
43
44impl GraphDensity {
45    /// Create a new graph density kernel.
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            metadata: KernelMetadata::batch("graph/density", Domain::GraphAnalytics)
50                .with_description("Graph density calculation")
51                .with_throughput(1_000_000)
52                .with_latency_us(1.0),
53        }
54    }
55
56    /// Compute graph density.
57    pub fn compute(graph: &CsrGraph) -> f64 {
58        graph.density()
59    }
60}
61
62impl Default for GraphDensity {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl GpuKernel for GraphDensity {
69    fn metadata(&self) -> &KernelMetadata {
70        &self.metadata
71    }
72}
73
74// ============================================================================
75// Average Path Length Kernel
76// ============================================================================
77
78/// Average path length kernel.
79///
80/// Computes the average shortest path length using BFS from each node.
81#[derive(Debug, Clone)]
82pub struct AveragePathLength {
83    metadata: KernelMetadata,
84}
85
86impl AveragePathLength {
87    /// Create a new average path length kernel.
88    #[must_use]
89    pub fn new() -> Self {
90        Self {
91            metadata: KernelMetadata::batch("graph/average-path-length", Domain::GraphAnalytics)
92                .with_description("Average shortest path length (BFS)")
93                .with_throughput(10_000)
94                .with_latency_us(100.0),
95        }
96    }
97
98    /// Compute average path length and diameter.
99    ///
100    /// Returns (average_path_length, diameter).
101    pub fn compute(graph: &CsrGraph) -> (f64, u64) {
102        let n = graph.num_nodes;
103        if n <= 1 {
104            return (0.0, 0);
105        }
106
107        let mut total_distance = 0u64;
108        let mut num_pairs = 0u64;
109        let mut diameter = 0u64;
110
111        for source in 0..n {
112            let distances = Self::bfs_distances(graph, source);
113
114            for (target, &dist) in distances.iter().enumerate() {
115                if target != source && dist > 0 {
116                    total_distance += dist as u64;
117                    num_pairs += 1;
118                    if dist as u64 > diameter {
119                        diameter = dist as u64;
120                    }
121                }
122            }
123        }
124
125        let average = if num_pairs > 0 {
126            total_distance as f64 / num_pairs as f64
127        } else {
128            0.0
129        };
130
131        (average, diameter)
132    }
133
134    /// BFS to compute distances from source to all other nodes.
135    fn bfs_distances(graph: &CsrGraph, source: usize) -> Vec<i64> {
136        let n = graph.num_nodes;
137        let mut distances = vec![-1i64; n];
138        distances[source] = 0;
139
140        let mut queue = VecDeque::new();
141        queue.push_back(source);
142
143        while let Some(v) = queue.pop_front() {
144            for &w in graph.neighbors(v as u64) {
145                let w = w as usize;
146                if distances[w] < 0 {
147                    distances[w] = distances[v] + 1;
148                    queue.push_back(w);
149                }
150            }
151        }
152
153        distances
154    }
155}
156
157impl Default for AveragePathLength {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163impl GpuKernel for AveragePathLength {
164    fn metadata(&self) -> &KernelMetadata {
165        &self.metadata
166    }
167}
168
169// ============================================================================
170// Clustering Coefficient Kernel
171// ============================================================================
172
173/// Clustering coefficient kernel.
174///
175/// Computes local and global clustering coefficients.
176#[derive(Debug, Clone)]
177pub struct ClusteringCoefficient {
178    metadata: KernelMetadata,
179}
180
181impl ClusteringCoefficient {
182    /// Create a new clustering coefficient kernel.
183    #[must_use]
184    pub fn new() -> Self {
185        Self {
186            metadata: KernelMetadata::batch("graph/clustering-coefficient", Domain::GraphAnalytics)
187                .with_description("Clustering coefficient (local and global)")
188                .with_throughput(50_000)
189                .with_latency_us(20.0),
190        }
191    }
192
193    /// Compute local clustering coefficient for a single node.
194    ///
195    /// CC(v) = 2 * triangles(v) / (degree(v) * (degree(v) - 1))
196    pub fn compute_local(graph: &CsrGraph, node: u64) -> f64 {
197        let degree = graph.out_degree(node) as usize;
198        if degree < 2 {
199            return 0.0;
200        }
201
202        let neighbors: std::collections::HashSet<u64> =
203            graph.neighbors(node).iter().copied().collect();
204
205        let mut triangles = 0u64;
206        for &v in graph.neighbors(node) {
207            for &w in graph.neighbors(v) {
208                if w != node && neighbors.contains(&w) {
209                    triangles += 1;
210                }
211            }
212        }
213
214        // Each triangle is counted twice
215        triangles /= 2;
216
217        let possible = (degree * (degree - 1)) / 2;
218        triangles as f64 / possible as f64
219    }
220
221    /// Compute average clustering coefficient for the entire graph.
222    pub fn compute_average(graph: &CsrGraph) -> f64 {
223        let n = graph.num_nodes;
224        if n == 0 {
225            return 0.0;
226        }
227
228        let sum: f64 = (0..n).map(|i| Self::compute_local(graph, i as u64)).sum();
229
230        sum / n as f64
231    }
232
233    /// Compute global clustering coefficient.
234    ///
235    /// 3 * triangles / wedges
236    pub fn compute_global(graph: &CsrGraph) -> f64 {
237        let n = graph.num_nodes;
238        let mut triangles = 0u64;
239        let mut wedges = 0u64;
240
241        for u in 0..n {
242            let neighbors: std::collections::HashSet<u64> =
243                graph.neighbors(u as u64).iter().copied().collect();
244            let degree = neighbors.len();
245
246            if degree >= 2 {
247                // Count wedges centered at u
248                wedges += (degree * (degree - 1)) as u64 / 2;
249
250                // Count triangles
251                for &v in graph.neighbors(u as u64) {
252                    for &w in graph.neighbors(v) {
253                        if w != u as u64 && neighbors.contains(&w) && v < w {
254                            triangles += 1;
255                        }
256                    }
257                }
258            }
259        }
260
261        if wedges == 0 {
262            0.0
263        } else {
264            triangles as f64 / wedges as f64
265        }
266    }
267}
268
269impl Default for ClusteringCoefficient {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275impl GpuKernel for ClusteringCoefficient {
276    fn metadata(&self) -> &KernelMetadata {
277        &self.metadata
278    }
279}
280
281// ============================================================================
282// Connected Components Kernel
283// ============================================================================
284
285/// Connected components kernel.
286///
287/// Finds connected components using BFS.
288#[derive(Debug, Clone)]
289pub struct ConnectedComponents {
290    metadata: KernelMetadata,
291}
292
293impl ConnectedComponents {
294    /// Create a new connected components kernel.
295    #[must_use]
296    pub fn new() -> Self {
297        Self {
298            metadata: KernelMetadata::batch("graph/connected-components", Domain::GraphAnalytics)
299                .with_description("Connected components (BFS)")
300                .with_throughput(100_000)
301                .with_latency_us(10.0),
302        }
303    }
304
305    /// Find connected components.
306    ///
307    /// Returns component labels for each node.
308    pub fn compute(graph: &CsrGraph) -> Vec<u64> {
309        let n = graph.num_nodes;
310        let mut labels = vec![u64::MAX; n];
311        let mut current_label = 0u64;
312
313        for start in 0..n {
314            if labels[start] != u64::MAX {
315                continue;
316            }
317
318            // BFS from this node
319            let mut queue = VecDeque::new();
320            queue.push_back(start);
321            labels[start] = current_label;
322
323            while let Some(v) = queue.pop_front() {
324                for &w in graph.neighbors(v as u64) {
325                    let w = w as usize;
326                    if labels[w] == u64::MAX {
327                        labels[w] = current_label;
328                        queue.push_back(w);
329                    }
330                }
331            }
332
333            current_label += 1;
334        }
335
336        labels
337    }
338
339    /// Count the number of connected components.
340    pub fn count_components(graph: &CsrGraph) -> usize {
341        let labels = Self::compute(graph);
342        labels.iter().copied().max().map_or(0, |m| m as usize + 1)
343    }
344
345    /// Get component sizes.
346    pub fn component_sizes(graph: &CsrGraph) -> Vec<usize> {
347        let labels = Self::compute(graph);
348        let num_components = labels.iter().copied().max().map_or(0, |m| m as usize + 1);
349
350        let mut sizes = vec![0usize; num_components];
351        for label in labels {
352            sizes[label as usize] += 1;
353        }
354
355        sizes
356    }
357}
358
359impl Default for ConnectedComponents {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365impl GpuKernel for ConnectedComponents {
366    fn metadata(&self) -> &KernelMetadata {
367        &self.metadata
368    }
369}
370
371// ============================================================================
372// Full Graph Metrics Kernel
373// ============================================================================
374
375/// Full graph metrics kernel.
376///
377/// Computes all graph-level metrics at once.
378#[derive(Debug, Clone)]
379pub struct FullGraphMetrics {
380    metadata: KernelMetadata,
381}
382
383impl FullGraphMetrics {
384    /// Create a new full graph metrics kernel.
385    #[must_use]
386    pub fn new() -> Self {
387        Self {
388            metadata: KernelMetadata::batch("graph/full-metrics", Domain::GraphAnalytics)
389                .with_description("Complete graph metrics")
390                .with_throughput(1_000)
391                .with_latency_us(1000.0),
392        }
393    }
394
395    /// Compute all graph metrics.
396    pub fn compute(graph: &CsrGraph) -> GraphMetricsResult {
397        let density = GraphDensity::compute(graph);
398        let (average_path_length, diameter) = AveragePathLength::compute(graph);
399        let average_clustering_coefficient = ClusteringCoefficient::compute_average(graph);
400
401        let component_sizes = ConnectedComponents::component_sizes(graph);
402        let num_components = component_sizes.len();
403        let largest_component_size = component_sizes.iter().copied().max().unwrap_or(0);
404        let is_connected = num_components <= 1;
405
406        GraphMetricsResult {
407            density,
408            average_path_length,
409            diameter,
410            average_clustering_coefficient,
411            num_components,
412            largest_component_size,
413            is_connected,
414        }
415    }
416}
417
418impl Default for FullGraphMetrics {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424impl GpuKernel for FullGraphMetrics {
425    fn metadata(&self) -> &KernelMetadata {
426        &self.metadata
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    fn create_complete_graph() -> CsrGraph {
435        // Complete graph K4
436        CsrGraph::from_edges(
437            4,
438            &[
439                (0, 1),
440                (0, 2),
441                (0, 3),
442                (1, 0),
443                (1, 2),
444                (1, 3),
445                (2, 0),
446                (2, 1),
447                (2, 3),
448                (3, 0),
449                (3, 1),
450                (3, 2),
451            ],
452        )
453    }
454
455    fn create_line_graph() -> CsrGraph {
456        // Line: 0 - 1 - 2 - 3
457        CsrGraph::from_edges(4, &[(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)])
458    }
459
460    fn create_disconnected_graph() -> CsrGraph {
461        // Two disconnected components: 0-1 and 2-3
462        CsrGraph::from_edges(4, &[(0, 1), (1, 0), (2, 3), (3, 2)])
463    }
464
465    #[test]
466    fn test_graph_density_complete() {
467        let graph = create_complete_graph();
468        let density = GraphDensity::compute(&graph);
469
470        // Complete graph should have density 1.0
471        assert!(
472            (density - 1.0).abs() < 0.01,
473            "Expected 1.0, got {}",
474            density
475        );
476    }
477
478    #[test]
479    fn test_graph_density_line() {
480        let graph = create_line_graph();
481        let density = GraphDensity::compute(&graph);
482
483        // Line graph: 3 edges out of 6 possible = 0.5
484        assert!(
485            (density - 0.5).abs() < 0.01,
486            "Expected 0.5, got {}",
487            density
488        );
489    }
490
491    #[test]
492    fn test_average_path_length_complete() {
493        let graph = create_complete_graph();
494        let (avg, diameter) = AveragePathLength::compute(&graph);
495
496        // In complete graph, all paths are length 1
497        assert!((avg - 1.0).abs() < 0.01, "Expected 1.0, got {}", avg);
498        assert_eq!(diameter, 1);
499    }
500
501    #[test]
502    fn test_average_path_length_line() {
503        let graph = create_line_graph();
504        let (avg, diameter) = AveragePathLength::compute(&graph);
505
506        // Diameter of line 0-1-2-3 is 3
507        assert_eq!(diameter, 3);
508
509        // Average: (1+2+3+1+1+2+2+1+1+3+2+1) / 12 = 20/12 = 1.67
510        assert!(avg > 1.0 && avg < 3.0);
511    }
512
513    #[test]
514    fn test_clustering_coefficient_complete() {
515        let graph = create_complete_graph();
516        let cc = ClusteringCoefficient::compute_average(&graph);
517
518        // Complete graph has CC = 1.0
519        assert!((cc - 1.0).abs() < 0.01, "Expected 1.0, got {}", cc);
520    }
521
522    #[test]
523    fn test_clustering_coefficient_line() {
524        let graph = create_line_graph();
525        let cc = ClusteringCoefficient::compute_average(&graph);
526
527        // Line graph has no triangles, CC = 0
528        assert!((cc).abs() < 0.01, "Expected 0.0, got {}", cc);
529    }
530
531    #[test]
532    fn test_connected_components() {
533        let graph = create_disconnected_graph();
534        let num = ConnectedComponents::count_components(&graph);
535
536        assert_eq!(num, 2);
537    }
538
539    #[test]
540    fn test_connected_graph() {
541        let graph = create_line_graph();
542        let num = ConnectedComponents::count_components(&graph);
543
544        assert_eq!(num, 1);
545    }
546
547    #[test]
548    fn test_full_metrics() {
549        let graph = create_complete_graph();
550        let metrics = FullGraphMetrics::compute(&graph);
551
552        assert!((metrics.density - 1.0).abs() < 0.01);
553        assert!(metrics.is_connected);
554        assert_eq!(metrics.num_components, 1);
555    }
556}