1use crate::types::CsrGraph;
10use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
11use std::collections::VecDeque;
12
13#[derive(Debug, Clone)]
15pub struct GraphMetricsResult {
16 pub density: f64,
18 pub average_path_length: f64,
20 pub diameter: u64,
22 pub average_clustering_coefficient: f64,
24 pub num_components: usize,
26 pub largest_component_size: usize,
28 pub is_connected: bool,
30}
31
32#[derive(Debug, Clone)]
40pub struct GraphDensity {
41 metadata: KernelMetadata,
42}
43
44impl GraphDensity {
45 #[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 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#[derive(Debug, Clone)]
82pub struct AveragePathLength {
83 metadata: KernelMetadata,
84}
85
86impl AveragePathLength {
87 #[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 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 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#[derive(Debug, Clone)]
177pub struct ClusteringCoefficient {
178 metadata: KernelMetadata,
179}
180
181impl ClusteringCoefficient {
182 #[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 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 triangles /= 2;
216
217 let possible = (degree * (degree - 1)) / 2;
218 triangles as f64 / possible as f64
219 }
220
221 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 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 wedges += (degree * (degree - 1)) as u64 / 2;
249
250 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#[derive(Debug, Clone)]
289pub struct ConnectedComponents {
290 metadata: KernelMetadata,
291}
292
293impl ConnectedComponents {
294 #[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 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 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 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 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#[derive(Debug, Clone)]
379pub struct FullGraphMetrics {
380 metadata: KernelMetadata,
381}
382
383impl FullGraphMetrics {
384 #[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 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 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 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 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 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 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 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 assert_eq!(diameter, 3);
508
509 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 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 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}