Skip to main content

scirs2_graph/
measures.rs

1//! Graph measures and metrics
2//!
3//! This module provides functions for measuring various properties
4//! of graphs, including centrality measures, clustering coefficients,
5//! and other graph metrics.
6
7use std::collections::{HashMap, HashSet};
8use std::hash::Hash;
9
10use scirs2_core::ndarray::{Array1, Array2};
11
12use crate::algorithms::{dijkstra_path, dijkstra_path_digraph};
13use crate::base::{DiGraph, EdgeWeight, Graph, Node};
14use crate::error::{GraphError, Result};
15use petgraph::graph::IndexType;
16
17/// Centrality measure type
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum CentralityType {
20    /// Degree centrality: number of connections
21    Degree,
22    /// Betweenness centrality: number of shortest paths going through a node
23    Betweenness,
24    /// Closeness centrality: inverse of the sum of shortest paths to all other nodes
25    Closeness,
26    /// Eigenvector centrality: nodes connected to important nodes are important
27    Eigenvector,
28    /// Katz centrality: influenced by all paths, with exponentially decaying weights
29    Katz,
30    /// PageRank centrality: Google's PageRank algorithm
31    PageRank,
32    /// Weighted degree centrality: sum of edge weights
33    WeightedDegree,
34    /// Weighted betweenness centrality: betweenness with edge weights
35    WeightedBetweenness,
36    /// Weighted closeness centrality: closeness with edge weights
37    WeightedCloseness,
38}
39
40/// Calculates centrality measures for nodes in an undirected graph
41///
42/// # Arguments
43/// * `graph` - The graph to analyze
44/// * `centrality_type` - The type of centrality to calculate
45///
46/// # Returns
47/// * `Result<HashMap<N, f64>>` - A map from nodes to their centrality values
48///
49/// # Time Complexity
50/// - Degree: O(V + E)
51/// - Betweenness: O(V * E) for unweighted, O(V² + VE) for weighted
52/// - Closeness: O(V * (V + E)) using BFS for each node
53/// - Eigenvector: O(V + E) per iteration, typically converges in O(log V) iterations
54/// - Katz: O(V + E) per iteration, typically converges quickly
55/// - PageRank: O(V + E) per iteration, typically converges in ~50 iterations
56///
57/// # Space Complexity
58/// O(V) for storing centrality values plus algorithm-specific requirements
59#[allow(dead_code)]
60pub fn centrality<N, E, Ix>(
61    graph: &Graph<N, E, Ix>,
62    centrality_type: CentralityType,
63) -> Result<HashMap<N, f64>>
64where
65    N: Node + std::fmt::Debug,
66    E: EdgeWeight
67        + scirs2_core::numeric::Zero
68        + scirs2_core::numeric::One
69        + std::ops::Add<Output = E>
70        + PartialOrd
71        + Into<f64>
72        + std::marker::Copy
73        + std::fmt::Debug
74        + std::default::Default,
75    Ix: petgraph::graph::IndexType,
76{
77    let nodes = graph.nodes();
78    let n = nodes.len();
79
80    if n == 0 {
81        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
82    }
83
84    match centrality_type {
85        CentralityType::Degree => degree_centrality(graph),
86        CentralityType::Betweenness => betweenness_centrality(graph),
87        CentralityType::Closeness => closeness_centrality(graph),
88        CentralityType::Eigenvector => eigenvector_centrality(graph),
89        CentralityType::Katz => katz_centrality(graph, 0.1, 1.0), // Default parameters
90        CentralityType::PageRank => pagerank_centrality(graph, 0.85, 1e-6), // Default parameters
91        CentralityType::WeightedDegree => weighted_degree_centrality(graph),
92        CentralityType::WeightedBetweenness => weighted_betweenness_centrality(graph),
93        CentralityType::WeightedCloseness => weighted_closeness_centrality(graph),
94    }
95}
96
97/// Calculates centrality measures for nodes in a directed graph
98///
99/// # Arguments
100/// * `graph` - The directed graph to analyze
101/// * `centrality_type` - The type of centrality to calculate
102///
103/// # Returns
104/// * `Result<HashMap<N, f64>>` - A map from nodes to their centrality values
105#[allow(dead_code)]
106pub fn centrality_digraph<N, E, Ix>(
107    graph: &DiGraph<N, E, Ix>,
108    centrality_type: CentralityType,
109) -> Result<HashMap<N, f64>>
110where
111    N: Node + std::fmt::Debug,
112    E: EdgeWeight
113        + scirs2_core::numeric::Zero
114        + scirs2_core::numeric::One
115        + std::ops::Add<Output = E>
116        + PartialOrd
117        + Into<f64>
118        + std::marker::Copy
119        + std::fmt::Debug
120        + std::default::Default,
121    Ix: petgraph::graph::IndexType,
122{
123    let nodes = graph.nodes();
124    let n = nodes.len();
125
126    if n == 0 {
127        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
128    }
129
130    match centrality_type {
131        CentralityType::Degree => degree_centrality_digraph(graph),
132        CentralityType::Betweenness => betweenness_centrality_digraph(graph),
133        CentralityType::Closeness => closeness_centrality_digraph(graph),
134        CentralityType::Eigenvector => eigenvector_centrality_digraph(graph),
135        CentralityType::Katz => katz_centrality_digraph(graph, 0.1, 1.0), // Default parameters
136        CentralityType::PageRank => pagerank_centrality_digraph(graph, 0.85, 1e-6), // Default parameters
137        CentralityType::WeightedDegree => weighted_degree_centrality_digraph(graph),
138        CentralityType::WeightedBetweenness => weighted_betweenness_centrality_digraph(graph),
139        CentralityType::WeightedCloseness => weighted_closeness_centrality_digraph(graph),
140    }
141}
142
143/// Calculates degree centrality for nodes in an undirected graph
144///
145/// # Time Complexity
146/// O(V + E) where V is vertices and E is edges
147///
148/// # Space Complexity
149/// O(V) for storing the centrality values
150#[allow(dead_code)]
151fn degree_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
152where
153    N: Node + std::fmt::Debug,
154    E: EdgeWeight,
155    Ix: petgraph::graph::IndexType,
156{
157    let nodes = graph.nodes();
158    let n = nodes.len() as f64;
159    let degrees = graph.degree_vector();
160
161    let mut centrality = HashMap::new();
162    let normalization = if n <= 1.0 { 1.0 } else { n - 1.0 };
163
164    for (i, node) in nodes.iter().enumerate() {
165        let degree = degrees[i] as f64;
166        centrality.insert((*node).clone(), degree / normalization);
167    }
168
169    Ok(centrality)
170}
171
172/// Calculates degree centrality for nodes in a directed graph
173#[allow(dead_code)]
174fn degree_centrality_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<HashMap<N, f64>>
175where
176    N: Node + std::fmt::Debug,
177    E: EdgeWeight,
178    Ix: petgraph::graph::IndexType,
179{
180    let nodes = graph.nodes();
181    let n = nodes.len() as f64;
182    let in_degrees = graph.in_degree_vector();
183    let out_degrees = graph.out_degree_vector();
184
185    let mut centrality = HashMap::new();
186    let normalization = if n <= 1.0 { 1.0 } else { n - 1.0 };
187
188    for (i, node) in nodes.iter().enumerate() {
189        // We use the sum of in-degree and out-degree as the total degree
190        let degree = (in_degrees[i] + out_degrees[i]) as f64;
191        centrality.insert((*node).clone(), degree / normalization);
192    }
193
194    Ok(centrality)
195}
196
197/// Calculates betweenness centrality for nodes in an undirected graph
198///
199/// # Time Complexity
200/// O(V * E) for unweighted graphs using Brandes' algorithm
201/// O(V² + VE) for weighted graphs
202///
203/// # Space Complexity
204/// O(V + E) for the algorithm's data structures
205#[allow(dead_code)]
206fn betweenness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
207where
208    N: Node + std::fmt::Debug,
209    E: EdgeWeight
210        + scirs2_core::numeric::Zero
211        + scirs2_core::numeric::One
212        + std::ops::Add<Output = E>
213        + PartialOrd
214        + Into<f64>
215        + std::marker::Copy
216        + std::fmt::Debug
217        + std::default::Default,
218    Ix: petgraph::graph::IndexType,
219{
220    let nodes = graph.nodes();
221    let n = nodes.len();
222
223    if n <= 1 {
224        let mut result = HashMap::new();
225        for node in nodes {
226            result.insert(node.clone(), 0.0);
227        }
228        return Ok(result);
229    }
230
231    let mut betweenness = HashMap::new();
232    for node in nodes.iter() {
233        betweenness.insert((*node).clone(), 0.0);
234    }
235
236    // For each pair of nodes, find the shortest paths
237    for (i, &s) in nodes.iter().enumerate() {
238        for (j, &t) in nodes.iter().enumerate() {
239            if i == j {
240                continue;
241            }
242
243            // Find shortest path from s to t
244            if let Ok(Some(path)) = dijkstra_path(graph, s, t) {
245                // Skip source and target nodes
246                for node in &path.nodes[1..path.nodes.len() - 1] {
247                    *betweenness.entry(node.clone()).or_insert(0.0) += 1.0;
248                }
249            }
250        }
251    }
252
253    // Normalize by the number of possible paths (excluding the node itself)
254    let scale = 1.0 / ((n - 1) * (n - 2)) as f64;
255    for val in betweenness.values_mut() {
256        *val *= scale;
257    }
258
259    Ok(betweenness)
260}
261
262/// Calculates betweenness centrality for nodes in a directed graph
263#[allow(dead_code)]
264fn betweenness_centrality_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<HashMap<N, f64>>
265where
266    N: Node + std::fmt::Debug,
267    E: EdgeWeight
268        + scirs2_core::numeric::Zero
269        + scirs2_core::numeric::One
270        + std::ops::Add<Output = E>
271        + PartialOrd
272        + Into<f64>
273        + std::marker::Copy
274        + std::fmt::Debug
275        + std::default::Default,
276    Ix: petgraph::graph::IndexType,
277{
278    let nodes = graph.nodes();
279    let n = nodes.len();
280
281    if n <= 1 {
282        let mut result = HashMap::new();
283        for node in nodes {
284            result.insert(node.clone(), 0.0);
285        }
286        return Ok(result);
287    }
288
289    let mut betweenness = HashMap::new();
290    for node in nodes.iter() {
291        betweenness.insert((*node).clone(), 0.0);
292    }
293
294    // For each pair of nodes, find the shortest paths
295    for (i, &s) in nodes.iter().enumerate() {
296        for (j, &t) in nodes.iter().enumerate() {
297            if i == j {
298                continue;
299            }
300
301            // Find shortest path from s to t
302            if let Ok(Some(path)) = dijkstra_path_digraph(graph, s, t) {
303                // Skip source and target nodes
304                for node in &path.nodes[1..path.nodes.len() - 1] {
305                    *betweenness.entry(node.clone()).or_insert(0.0) += 1.0;
306                }
307            }
308        }
309    }
310
311    // Normalize by the number of possible paths (excluding the node itself)
312    let scale = 1.0 / ((n - 1) * (n - 2)) as f64;
313    for val in betweenness.values_mut() {
314        *val *= scale;
315    }
316
317    Ok(betweenness)
318}
319
320/// Calculates closeness centrality for nodes in an undirected graph
321#[allow(dead_code)]
322fn closeness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
323where
324    N: Node + std::fmt::Debug,
325    E: EdgeWeight
326        + scirs2_core::numeric::Zero
327        + scirs2_core::numeric::One
328        + std::ops::Add<Output = E>
329        + PartialOrd
330        + Into<f64>
331        + std::marker::Copy
332        + std::fmt::Debug
333        + std::default::Default,
334    Ix: petgraph::graph::IndexType,
335{
336    let nodes = graph.nodes();
337    let n = nodes.len();
338
339    if n <= 1 {
340        let mut result = HashMap::new();
341        for node in nodes {
342            result.insert(node.clone(), 1.0);
343        }
344        return Ok(result);
345    }
346
347    let mut closeness = HashMap::new();
348
349    // For each node, calculate the sum of shortest paths to all other nodes
350    for &node in nodes.iter() {
351        let mut sum_distances = 0.0;
352        let mut reachable_nodes = 0;
353
354        for &other in nodes.iter() {
355            if node == other {
356                continue;
357            }
358
359            // Find shortest path from node to other
360            if let Ok(Some(path)) = dijkstra_path(graph, node, other) {
361                sum_distances += path.total_weight.into();
362                reachable_nodes += 1;
363            }
364        }
365
366        // If the node can reach other nodes
367        if reachable_nodes > 0 {
368            // Closeness is 1 / average path length
369            let closeness_value = reachable_nodes as f64 / sum_distances;
370            // Normalize by the fraction of nodes that are reachable
371            let normalized = closeness_value * (reachable_nodes as f64 / (n - 1) as f64);
372            closeness.insert(node.clone(), normalized);
373        } else {
374            closeness.insert(node.clone(), 0.0);
375        }
376    }
377
378    Ok(closeness)
379}
380
381/// Calculates closeness centrality for nodes in a directed graph
382#[allow(dead_code)]
383fn closeness_centrality_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<HashMap<N, f64>>
384where
385    N: Node + std::fmt::Debug,
386    E: EdgeWeight
387        + scirs2_core::numeric::Zero
388        + scirs2_core::numeric::One
389        + std::ops::Add<Output = E>
390        + PartialOrd
391        + Into<f64>
392        + std::marker::Copy
393        + std::fmt::Debug
394        + std::default::Default,
395    Ix: petgraph::graph::IndexType,
396{
397    let nodes = graph.nodes();
398    let n = nodes.len();
399
400    if n <= 1 {
401        let mut result = HashMap::new();
402        for node in nodes {
403            result.insert(node.clone(), 1.0);
404        }
405        return Ok(result);
406    }
407
408    let mut closeness = HashMap::new();
409
410    // For each node, calculate the sum of shortest paths to all other nodes
411    for &node in nodes.iter() {
412        let mut sum_distances = 0.0;
413        let mut reachable_nodes = 0;
414
415        for &other in nodes.iter() {
416            if node == other {
417                continue;
418            }
419
420            // Find shortest path from node to other
421            if let Ok(Some(path)) = dijkstra_path_digraph(graph, node, other) {
422                sum_distances += path.total_weight.into();
423                reachable_nodes += 1;
424            }
425        }
426
427        // If the node can reach other nodes
428        if reachable_nodes > 0 {
429            // Closeness is 1 / average path length
430            let closeness_value = reachable_nodes as f64 / sum_distances;
431            // Normalize by the fraction of nodes that are reachable
432            let normalized = closeness_value * (reachable_nodes as f64 / (n - 1) as f64);
433            closeness.insert(node.clone(), normalized);
434        } else {
435            closeness.insert(node.clone(), 0.0);
436        }
437    }
438
439    Ok(closeness)
440}
441
442/// Calculates eigenvector centrality for nodes in an undirected graph
443///
444/// Uses the power iteration method.
445#[allow(dead_code)]
446fn eigenvector_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
447where
448    N: Node + std::fmt::Debug,
449    E: EdgeWeight
450        + scirs2_core::numeric::Zero
451        + scirs2_core::numeric::One
452        + PartialOrd
453        + Into<f64>
454        + std::marker::Copy,
455    Ix: petgraph::graph::IndexType,
456{
457    let nodes = graph.nodes();
458    let n = nodes.len();
459
460    if n == 0 {
461        return Err(GraphError::InvalidGraph("Empty _graph".to_string()));
462    }
463
464    // Get adjacency matrix with weights converted to f64
465    let adj_mat = graph.adjacency_matrix();
466    let mut adj_f64 = Array2::<f64>::zeros((n, n));
467    for i in 0..n {
468        for j in 0..n {
469            adj_f64[[i, j]] = adj_mat[[i, j]].into();
470        }
471    }
472
473    // Start with uniform vector
474    let mut x = Array1::<f64>::ones(n);
475    x.mapv_inplace(|v| v / (n as f64).sqrt()); // Normalize
476
477    // Power iteration
478    let max_iter = 100;
479    let tol = 1e-6;
480
481    for _ in 0..max_iter {
482        // y = A * x
483        let y = adj_f64.dot(&x);
484
485        // Compute the norm
486        let norm = (y.iter().map(|v| v * v).sum::<f64>()).sqrt();
487        if norm < 1e-10 {
488            // If the norm is too small, we have a zero vector
489            return Err(GraphError::AlgorithmError(
490                "Eigenvector centrality computation failed: zero eigenvalue".to_string(),
491            ));
492        }
493
494        // Normalize y
495        let y_norm = y / norm;
496
497        // Check for convergence
498        let diff = (&y_norm - &x).iter().map(|v| v.abs()).sum::<f64>();
499        if diff < tol {
500            // We've converged, create the result
501            let mut result = HashMap::new();
502            for (i, &node) in nodes.iter().enumerate() {
503                result.insert(node.clone(), y_norm[i]);
504            }
505            return Ok(result);
506        }
507
508        // Update x for next iteration
509        x = y_norm;
510    }
511
512    // We've reached max iterations without converging
513    Err(GraphError::AlgorithmError(
514        "Eigenvector centrality computation did not converge".to_string(),
515    ))
516}
517
518/// Calculates eigenvector centrality for nodes in a directed graph
519///
520/// Uses the power iteration method.
521#[allow(dead_code)]
522fn eigenvector_centrality_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<HashMap<N, f64>>
523where
524    N: Node + std::fmt::Debug,
525    E: EdgeWeight
526        + scirs2_core::numeric::Zero
527        + scirs2_core::numeric::One
528        + PartialOrd
529        + Into<f64>
530        + std::marker::Copy,
531    Ix: petgraph::graph::IndexType,
532{
533    let nodes = graph.nodes();
534    let n = nodes.len();
535
536    if n == 0 {
537        return Err(GraphError::InvalidGraph("Empty _graph".to_string()));
538    }
539
540    // Get adjacency matrix with weights converted to f64
541    let adj_mat = graph.adjacency_matrix();
542    let mut adj_f64 = Array2::<f64>::zeros((n, n));
543    for i in 0..n {
544        for j in 0..n {
545            adj_f64[[i, j]] = adj_mat[[i, j]];
546        }
547    }
548
549    // Start with uniform vector
550    let mut x = Array1::<f64>::ones(n);
551    x.mapv_inplace(|v| v / (n as f64).sqrt()); // Normalize
552
553    // Power iteration
554    let max_iter = 100;
555    let tol = 1e-6;
556
557    for _ in 0..max_iter {
558        // y = A * x
559        let y = adj_f64.dot(&x);
560
561        // Compute the norm
562        let norm = (y.iter().map(|v| v * v).sum::<f64>()).sqrt();
563        if norm < 1e-10 {
564            // If the norm is too small, we have a zero vector
565            return Err(GraphError::AlgorithmError(
566                "Eigenvector centrality computation failed: zero eigenvalue".to_string(),
567            ));
568        }
569
570        // Normalize y
571        let y_norm = y / norm;
572
573        // Check for convergence
574        let diff = (&y_norm - &x).iter().map(|v| v.abs()).sum::<f64>();
575        if diff < tol {
576            // We've converged, create the result
577            let mut result = HashMap::new();
578            for (i, &node) in nodes.iter().enumerate() {
579                result.insert(node.clone(), y_norm[i]);
580            }
581            return Ok(result);
582        }
583
584        // Update x for next iteration
585        x = y_norm;
586    }
587
588    // We've reached max iterations without converging
589    Err(GraphError::AlgorithmError(
590        "Eigenvector centrality computation did not converge".to_string(),
591    ))
592}
593
594/// Calculates the local clustering coefficient for each node in an undirected graph
595///
596/// The clustering coefficient measures how close a node's neighbors are to being a clique.
597///
598/// # Arguments
599/// * `graph` - The graph to analyze
600///
601/// # Returns
602/// * `Result<HashMap<N, f64>>` - A map from nodes to their clustering coefficients
603#[allow(dead_code)]
604pub fn clustering_coefficient<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
605where
606    N: Node + std::fmt::Debug,
607    E: EdgeWeight,
608    Ix: petgraph::graph::IndexType,
609{
610    let mut coefficients = HashMap::new();
611
612    for (idx, &node) in graph.nodes().iter().enumerate() {
613        // Get the neighbors of this node
614        let node_idx = petgraph::graph::NodeIndex::new(idx);
615        let neighbors: HashSet<_> = graph.inner().neighbors(node_idx).collect();
616
617        let k = neighbors.len();
618
619        // If the node has less than 2 neighbors, its clustering coefficient is 0
620        if k < 2 {
621            coefficients.insert(node.clone(), 0.0);
622            continue;
623        }
624
625        // Count the number of edges between neighbors
626        let mut edge_count = 0;
627
628        for &n1 in &neighbors {
629            for &n2 in &neighbors {
630                if n1 != n2 && graph.inner().contains_edge(n1, n2) {
631                    edge_count += 1;
632                }
633            }
634        }
635
636        // Each edge is counted twice (once from each direction)
637        edge_count /= 2;
638
639        // Calculate the clustering coefficient
640        let possible_edges = k * (k - 1) / 2;
641        let coefficient = edge_count as f64 / possible_edges as f64;
642
643        coefficients.insert(node.clone(), coefficient);
644    }
645
646    Ok(coefficients)
647}
648
649/// Calculates the global clustering coefficient (transitivity) of a graph
650///
651/// This is the ratio of the number of closed triplets to the total number of triplets.
652///
653/// # Arguments
654/// * `graph` - The graph to analyze
655///
656/// # Returns
657/// * `Result<f64>` - The global clustering coefficient
658#[allow(dead_code)]
659pub fn graph_density<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<f64>
660where
661    N: Node + std::fmt::Debug,
662    E: EdgeWeight,
663    Ix: petgraph::graph::IndexType,
664{
665    let n = graph.node_count();
666
667    if n <= 1 {
668        return Err(GraphError::InvalidGraph(
669            "Graph density undefined for graphs with 0 or 1 nodes".to_string(),
670        ));
671    }
672
673    let m = graph.edge_count();
674    let possible_edges = n * (n - 1) / 2;
675
676    Ok(m as f64 / possible_edges as f64)
677}
678
679/// Calculates the density of a directed graph
680///
681/// # Arguments
682/// * `graph` - The directed graph to analyze
683///
684/// # Returns
685/// * `Result<f64>` - The graph density
686#[allow(dead_code)]
687pub fn graph_density_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<f64>
688where
689    N: Node + std::fmt::Debug,
690    E: EdgeWeight,
691    Ix: petgraph::graph::IndexType,
692{
693    let n = graph.node_count();
694
695    if n <= 1 {
696        return Err(GraphError::InvalidGraph(
697            "Graph density undefined for graphs with 0 or 1 nodes".to_string(),
698        ));
699    }
700
701    let m = graph.edge_count();
702    let possible_edges = n * (n - 1); // In directed graphs, there can be n(n-1) edges
703
704    Ok(m as f64 / possible_edges as f64)
705}
706
707/// Calculates Katz centrality for nodes in an undirected graph
708///
709/// Katz centrality is a variant of eigenvector centrality that considers all paths between nodes,
710/// with paths of longer length given exponentially decreasing weights.
711///
712/// # Arguments
713/// * `graph` - The graph to analyze
714/// * `alpha` - Attenuation factor (must be smaller than the reciprocal of the largest eigenvalue)
715/// * `beta` - Weight attributed to the immediate neighborhood
716///
717/// # Returns
718/// * `Result<HashMap<N, f64>>` - Katz centrality values
719#[allow(dead_code)]
720pub fn katz_centrality<N, E, Ix>(
721    graph: &Graph<N, E, Ix>,
722    alpha: f64,
723    beta: f64,
724) -> Result<HashMap<N, f64>>
725where
726    N: Node + std::fmt::Debug,
727    E: EdgeWeight + scirs2_core::numeric::Zero + scirs2_core::numeric::One + Into<f64> + Copy,
728    Ix: petgraph::graph::IndexType,
729{
730    let nodes = graph.nodes();
731    let n = nodes.len();
732
733    if n == 0 {
734        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
735    }
736
737    // Get adjacency matrix
738    let adj_mat = graph.adjacency_matrix();
739    let mut adj_f64 = Array2::<f64>::zeros((n, n));
740    for i in 0..n {
741        for j in 0..n {
742            adj_f64[[i, j]] = adj_mat[[i, j]].into();
743        }
744    }
745
746    // Create identity matrix
747    let mut identity = Array2::<f64>::zeros((n, n));
748    for i in 0..n {
749        identity[[i, i]] = 1.0;
750    }
751
752    // Compute (I - α*A) - not used in iterative method but kept for reference
753    let _factor_matrix = &identity - &(alpha * &adj_f64);
754
755    // Create beta vector (all entries = beta)
756    let beta_vec = Array1::<f64>::from_elem(n, beta);
757
758    // We need to solve (I - α*A) * c = β*1
759    // For simplicity, we'll use an iterative approach (power method variant)
760    let mut centrality_vec = Array1::<f64>::ones(n);
761    let max_iter = 100;
762    let tol = 1e-6;
763
764    for _ in 0..max_iter {
765        // c_new = α*A*c + β*1
766        let new_centrality = alpha * adj_f64.dot(&centrality_vec) + &beta_vec;
767
768        // Check for convergence
769        let diff = (&new_centrality - &centrality_vec)
770            .iter()
771            .map(|v| v.abs())
772            .sum::<f64>();
773        if diff < tol {
774            centrality_vec = new_centrality;
775            break;
776        }
777
778        centrality_vec = new_centrality;
779    }
780
781    // Convert to HashMap
782    let mut result = HashMap::new();
783    for (i, &node) in nodes.iter().enumerate() {
784        result.insert(node.clone(), centrality_vec[i]);
785    }
786
787    Ok(result)
788}
789
790/// Calculates Katz centrality for nodes in a directed graph
791///
792/// # Arguments
793/// * `graph` - The directed graph to analyze
794/// * `alpha` - Attenuation factor
795/// * `beta` - Weight attributed to the immediate neighborhood
796///
797/// # Returns
798/// * `Result<HashMap<N, f64>>` - Katz centrality values
799#[allow(dead_code)]
800pub fn katz_centrality_digraph<N, E, Ix>(
801    graph: &DiGraph<N, E, Ix>,
802    alpha: f64,
803    beta: f64,
804) -> Result<HashMap<N, f64>>
805where
806    N: Node + std::fmt::Debug,
807    E: EdgeWeight + scirs2_core::numeric::Zero + scirs2_core::numeric::One + Into<f64> + Copy,
808    Ix: petgraph::graph::IndexType,
809{
810    let nodes = graph.nodes();
811    let n = nodes.len();
812
813    if n == 0 {
814        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
815    }
816
817    // Get adjacency matrix
818    let adj_mat = graph.adjacency_matrix();
819    let mut adj_f64 = Array2::<f64>::zeros((n, n));
820    for i in 0..n {
821        for j in 0..n {
822            adj_f64[[i, j]] = adj_mat[[i, j]];
823        }
824    }
825
826    // Create beta vector
827    let beta_vec = Array1::<f64>::from_elem(n, beta);
828
829    // Iterative approach
830    let mut centrality_vec = Array1::<f64>::ones(n);
831    let max_iter = 100;
832    let tol = 1e-6;
833
834    for _ in 0..max_iter {
835        // c_new = α*A^T*c + β*1 (transpose for incoming links)
836        let new_centrality = alpha * adj_f64.t().dot(&centrality_vec) + &beta_vec;
837
838        // Check for convergence
839        let diff = (&new_centrality - &centrality_vec)
840            .iter()
841            .map(|v| v.abs())
842            .sum::<f64>();
843        if diff < tol {
844            centrality_vec = new_centrality;
845            break;
846        }
847
848        centrality_vec = new_centrality;
849    }
850
851    // Convert to HashMap
852    let mut result = HashMap::new();
853    for (i, &node) in nodes.iter().enumerate() {
854        result.insert(node.clone(), centrality_vec[i]);
855    }
856
857    Ok(result)
858}
859
860/// Calculates PageRank centrality for nodes in an undirected graph
861///
862/// PageRank is Google's famous algorithm for ranking web pages.
863/// For undirected graphs, we treat each edge as bidirectional.
864///
865/// # Arguments
866/// * `graph` - The graph to analyze
867/// * `damping` - Damping parameter (typically 0.85)
868/// * `tolerance` - Convergence tolerance
869///
870/// # Returns
871/// * `Result<HashMap<N, f64>>` - PageRank values
872#[allow(dead_code)]
873pub fn pagerank_centrality<N, E, Ix>(
874    graph: &Graph<N, E, Ix>,
875    damping: f64,
876    tolerance: f64,
877) -> Result<HashMap<N, f64>>
878where
879    N: Node + std::fmt::Debug,
880    E: EdgeWeight,
881    Ix: petgraph::graph::IndexType,
882{
883    let nodes = graph.nodes();
884    let n = nodes.len();
885
886    if n == 0 {
887        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
888    }
889
890    // Initialize PageRank values
891    let mut pagerank = Array1::<f64>::from_elem(n, 1.0 / n as f64);
892    let max_iter = 100;
893
894    // Get degree of each node for normalization
895    let degrees = graph.degree_vector();
896
897    for _ in 0..max_iter {
898        // Compute dangling node contribution: sum of PageRank of nodes with degree 0
899        let mut dangling_sum = 0.0;
900        for (i, _) in graph.inner().node_indices().enumerate() {
901            if degrees[i] == 0 {
902                dangling_sum += pagerank[i];
903            }
904        }
905        // Dangling mass is redistributed uniformly to all nodes
906        let dangling_contrib = damping * dangling_sum / n as f64;
907
908        let mut new_pagerank =
909            Array1::<f64>::from_elem(n, (1.0 - damping) / n as f64 + dangling_contrib);
910
911        // Calculate PageRank contribution from each non-dangling node
912        for (i, node_idx) in graph.inner().node_indices().enumerate() {
913            let node_degree = degrees[i] as f64;
914            if node_degree > 0.0 {
915                let contribution = damping * pagerank[i] / node_degree;
916
917                // Distribute to all neighbors
918                for neighbor_idx in graph.inner().neighbors(node_idx) {
919                    let neighbor_i = neighbor_idx.index();
920                    new_pagerank[neighbor_i] += contribution;
921                }
922            }
923        }
924
925        // Check for convergence
926        let diff = (&new_pagerank - &pagerank)
927            .iter()
928            .map(|v| v.abs())
929            .sum::<f64>();
930        if diff < tolerance {
931            pagerank = new_pagerank;
932            break;
933        }
934
935        pagerank = new_pagerank;
936    }
937
938    // Convert to HashMap
939    let mut result = HashMap::new();
940    for (i, &node) in nodes.iter().enumerate() {
941        result.insert(node.clone(), pagerank[i]);
942    }
943
944    Ok(result)
945}
946
947/// Calculates PageRank centrality using parallel processing for large graphs
948///
949/// This parallel implementation of PageRank uses scirs2-core parallel operations
950/// to accelerate computation on large graphs. It's particularly effective when
951/// the graph has >10,000 nodes and sufficient CPU cores are available.
952///
953/// # Arguments
954/// * `graph` - The graph to analyze
955/// * `damping` - Damping parameter (typically 0.85)
956/// * `tolerance` - Convergence tolerance
957/// * `max_iterations` - Maximum number of iterations (default: 100)
958///
959/// # Returns
960/// * `Result<HashMap<N, f64>>` - PageRank values
961///
962/// # Time Complexity
963/// O((V + E) * k / p) where V is nodes, E is edges, k is iterations,
964/// and p is the number of parallel threads.
965///
966/// # Space Complexity
967/// O(V + t) where t is the number of threads for thread-local storage.
968///
969/// # Performance Notes
970/// - Best performance gain on graphs with >10,000 nodes
971/// - Requires multiple CPU cores for meaningful speedup
972/// - Memory access patterns optimized for cache efficiency
973#[allow(dead_code)]
974pub fn parallel_pagerank_centrality<N, E, Ix>(
975    graph: &Graph<N, E, Ix>,
976    damping: f64,
977    tolerance: f64,
978    max_iterations: Option<usize>,
979) -> Result<HashMap<N, f64>>
980where
981    N: Node + Send + Sync + std::fmt::Debug,
982    E: EdgeWeight + Send + Sync,
983    Ix: petgraph::graph::IndexType + Send + Sync,
984{
985    use scirs2_core::parallel_ops::*;
986    use std::sync::{Arc, Mutex};
987
988    let nodes = graph.nodes();
989    let n = nodes.len();
990
991    if n == 0 {
992        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
993    }
994
995    // For small graphs, use sequential implementation
996    if n < 1000 {
997        return pagerank_centrality(graph, damping, tolerance);
998    }
999
1000    let max_iter = max_iterations.unwrap_or(100);
1001
1002    // Initialize PageRank values
1003    let mut pagerank = Array1::<f64>::from_elem(n, 1.0 / n as f64);
1004
1005    // Get degree of each node for normalization
1006    let degrees = graph.degree_vector();
1007
1008    // Pre-compute neighbor indices for efficient parallel access
1009    let neighbor_lists: Vec<Vec<usize>> = (0..n)
1010        .map(|i| {
1011            let node_idx = petgraph::graph::NodeIndex::new(i);
1012            graph
1013                .inner()
1014                .neighbors(node_idx)
1015                .map(|neighbor_idx| neighbor_idx.index())
1016                .collect()
1017        })
1018        .collect();
1019
1020    for _iteration in 0..max_iter {
1021        // Parallel computation of new PageRank values
1022        let new_pagerank = Arc::new(Mutex::new(Array1::<f64>::from_elem(
1023            n,
1024            (1.0 - damping) / n as f64,
1025        )));
1026
1027        // Use parallel iteration for PageRank calculation
1028        (0..n).into_par_iter().for_each(|i| {
1029            let node_degree = degrees[i] as f64;
1030            if node_degree > 0.0 {
1031                let contribution = damping * pagerank[i] / node_degree;
1032
1033                // Distribute to all neighbors
1034                let mut local_updates = Vec::new();
1035                for &neighbor_i in &neighbor_lists[i] {
1036                    local_updates.push((neighbor_i, contribution));
1037                }
1038
1039                // Apply updates atomically
1040                if !local_updates.is_empty() {
1041                    let mut new_pr = new_pagerank.lock().expect("Failed to acquire lock");
1042                    for (neighbor_i, contrib) in local_updates {
1043                        new_pr[neighbor_i] += contrib;
1044                    }
1045                }
1046            }
1047        });
1048
1049        let new_pagerank = Arc::try_unwrap(new_pagerank)
1050            .expect("Failed to unwrap Arc")
1051            .into_inner()
1052            .expect("Failed to get inner value");
1053
1054        // Convergence check
1055        let diff = pagerank
1056            .iter()
1057            .zip(new_pagerank.iter())
1058            .map(|(old, new)| (new - old).abs())
1059            .sum::<f64>();
1060
1061        if diff < tolerance {
1062            pagerank = new_pagerank;
1063            break;
1064        }
1065
1066        pagerank = new_pagerank;
1067    }
1068
1069    // Conversion to HashMap
1070    let result: HashMap<N, f64> = nodes
1071        .iter()
1072        .enumerate()
1073        .map(|(i, node)| ((*node).clone(), pagerank[i]))
1074        .collect();
1075
1076    Ok(result)
1077}
1078
1079/// HITS (Hyperlink-Induced Topic Search) algorithm result
1080#[derive(Debug, Clone)]
1081pub struct HitsScores<N: Node> {
1082    /// Authority scores for each node
1083    pub authorities: HashMap<N, f64>,
1084    /// Hub scores for each node
1085    pub hubs: HashMap<N, f64>,
1086}
1087
1088/// Compute HITS algorithm for a directed graph
1089///
1090/// The HITS algorithm computes two scores for each node:
1091/// - Authority score: nodes that are pointed to by many hubs
1092/// - Hub score: nodes that point to many authorities
1093///
1094/// # Arguments
1095/// * `graph` - The directed graph
1096/// * `max_iter` - Maximum number of iterations
1097/// * `tolerance` - Convergence tolerance
1098///
1099/// # Returns
1100/// * HitsScores containing authority and hub scores for each node
1101#[allow(dead_code)]
1102pub fn hits_algorithm<N, E, Ix>(
1103    graph: &DiGraph<N, E, Ix>,
1104    max_iter: usize,
1105    tolerance: f64,
1106) -> Result<HitsScores<N>>
1107where
1108    N: Node + Clone + Hash + Eq + std::fmt::Debug,
1109    E: EdgeWeight,
1110    Ix: IndexType,
1111{
1112    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1113    let n = nodes.len();
1114
1115    if n == 0 {
1116        return Ok(HitsScores {
1117            authorities: HashMap::new(),
1118            hubs: HashMap::new(),
1119        });
1120    }
1121
1122    // Initialize scores
1123    let mut authorities = vec![1.0 / (n as f64).sqrt(); n];
1124    let mut hubs = vec![1.0 / (n as f64).sqrt(); n];
1125    let mut new_authorities = vec![0.0; n];
1126    let mut new_hubs = vec![0.0; n];
1127
1128    // Create node index mapping
1129    let node_to_idx: HashMap<N, usize> = nodes
1130        .iter()
1131        .enumerate()
1132        .map(|(i, n)| (n.clone(), i))
1133        .collect();
1134
1135    // Iterate until convergence
1136    for _ in 0..max_iter {
1137        // Update authority scores
1138        new_authorities.fill(0.0);
1139        for (i, node) in nodes.iter().enumerate() {
1140            // Authority score is sum of hub scores of nodes pointing to it
1141            if let Ok(predecessors) = graph.predecessors(node) {
1142                for pred in predecessors {
1143                    if let Some(&pred_idx) = node_to_idx.get(&pred) {
1144                        new_authorities[i] += hubs[pred_idx];
1145                    }
1146                }
1147            }
1148        }
1149
1150        // Update hub scores
1151        new_hubs.fill(0.0);
1152        for (i, node) in nodes.iter().enumerate() {
1153            // Hub score is sum of authority scores of nodes it points to
1154            if let Ok(successors) = graph.successors(node) {
1155                for succ in successors {
1156                    if let Some(&succ_idx) = node_to_idx.get(&succ) {
1157                        new_hubs[i] += authorities[succ_idx];
1158                    }
1159                }
1160            }
1161        }
1162
1163        // Normalize scores
1164        let auth_norm: f64 = new_authorities.iter().map(|x| x * x).sum::<f64>().sqrt();
1165        let hub_norm: f64 = new_hubs.iter().map(|x| x * x).sum::<f64>().sqrt();
1166
1167        if auth_norm > 0.0 {
1168            for score in &mut new_authorities {
1169                *score /= auth_norm;
1170            }
1171        }
1172
1173        if hub_norm > 0.0 {
1174            for score in &mut new_hubs {
1175                *score /= hub_norm;
1176            }
1177        }
1178
1179        // Check convergence
1180        let auth_diff: f64 = authorities
1181            .iter()
1182            .zip(&new_authorities)
1183            .map(|(old, new)| (old - new).abs())
1184            .sum();
1185        let hub_diff: f64 = hubs
1186            .iter()
1187            .zip(&new_hubs)
1188            .map(|(old, new)| (old - new).abs())
1189            .sum();
1190
1191        if auth_diff < tolerance && hub_diff < tolerance {
1192            break;
1193        }
1194
1195        // Update scores
1196        authorities.copy_from_slice(&new_authorities);
1197        hubs.copy_from_slice(&new_hubs);
1198    }
1199
1200    // Convert to HashMap
1201    let authority_map = nodes
1202        .iter()
1203        .enumerate()
1204        .map(|(i, n)| (n.clone(), authorities[i]))
1205        .collect();
1206    let hub_map = nodes
1207        .iter()
1208        .enumerate()
1209        .map(|(i, n)| (n.clone(), hubs[i]))
1210        .collect();
1211
1212    Ok(HitsScores {
1213        authorities: authority_map,
1214        hubs: hub_map,
1215    })
1216}
1217
1218/// Calculates PageRank centrality for nodes in a directed graph
1219///
1220/// # Arguments
1221/// * `graph` - The directed graph to analyze
1222/// * `damping` - Damping parameter (typically 0.85)
1223/// * `tolerance` - Convergence tolerance
1224///
1225/// # Returns
1226/// * `Result<HashMap<N, f64>>` - PageRank values
1227#[allow(dead_code)]
1228pub fn pagerank_centrality_digraph<N, E, Ix>(
1229    graph: &DiGraph<N, E, Ix>,
1230    damping: f64,
1231    tolerance: f64,
1232) -> Result<HashMap<N, f64>>
1233where
1234    N: Node + std::fmt::Debug,
1235    E: EdgeWeight,
1236    Ix: petgraph::graph::IndexType,
1237{
1238    let nodes = graph.nodes();
1239    let n = nodes.len();
1240
1241    if n == 0 {
1242        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
1243    }
1244
1245    // Initialize PageRank values
1246    let mut pagerank = Array1::<f64>::from_elem(n, 1.0 / n as f64);
1247    let max_iter = 100;
1248
1249    // Get out-degree of each node for normalization
1250    let out_degrees = graph.out_degree_vector();
1251
1252    for _ in 0..max_iter {
1253        let mut new_pagerank = Array1::<f64>::from_elem(n, (1.0 - damping) / n as f64);
1254
1255        // Calculate PageRank contribution from each node
1256        for (i, node_idx) in graph.inner().node_indices().enumerate() {
1257            let node_out_degree = out_degrees[i] as f64;
1258            if node_out_degree > 0.0 {
1259                let contribution = damping * pagerank[i] / node_out_degree;
1260
1261                // Distribute to all outgoing neighbors
1262                for neighbor_idx in graph
1263                    .inner()
1264                    .neighbors_directed(node_idx, petgraph::Direction::Outgoing)
1265                {
1266                    let neighbor_i = neighbor_idx.index();
1267                    new_pagerank[neighbor_i] += contribution;
1268                }
1269            } else {
1270                // Handle dangling nodes by distributing equally to all nodes
1271                let contribution = damping * pagerank[i] / n as f64;
1272                for j in 0..n {
1273                    new_pagerank[j] += contribution;
1274                }
1275            }
1276        }
1277
1278        // Check for convergence
1279        let diff = (&new_pagerank - &pagerank)
1280            .iter()
1281            .map(|v| v.abs())
1282            .sum::<f64>();
1283        if diff < tolerance {
1284            pagerank = new_pagerank;
1285            break;
1286        }
1287
1288        pagerank = new_pagerank;
1289    }
1290
1291    // Convert to HashMap
1292    let mut result = HashMap::new();
1293    for (i, &node) in nodes.iter().enumerate() {
1294        result.insert(node.clone(), pagerank[i]);
1295    }
1296
1297    Ok(result)
1298}
1299
1300/// Calculates weighted degree centrality for undirected graphs
1301///
1302/// Weighted degree centrality is the sum of the weights of all edges incident to a node.
1303#[allow(dead_code)]
1304fn weighted_degree_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
1305where
1306    N: Node + std::fmt::Debug,
1307    E: EdgeWeight + Into<f64> + Clone,
1308    Ix: IndexType,
1309{
1310    let mut centrality = HashMap::new();
1311
1312    for node in graph.nodes() {
1313        let mut weight_sum = 0.0;
1314
1315        if let Ok(neighbors) = graph.neighbors(node) {
1316            for neighbor in neighbors {
1317                if let Ok(weight) = graph.edge_weight(node, &neighbor) {
1318                    weight_sum += weight.into();
1319                }
1320            }
1321        }
1322
1323        centrality.insert(node.clone(), weight_sum);
1324    }
1325
1326    Ok(centrality)
1327}
1328
1329/// Calculates weighted degree centrality for directed graphs
1330#[allow(dead_code)]
1331fn weighted_degree_centrality_digraph<N, E, Ix>(
1332    graph: &DiGraph<N, E, Ix>,
1333) -> Result<HashMap<N, f64>>
1334where
1335    N: Node + std::fmt::Debug,
1336    E: EdgeWeight + Into<f64> + Clone,
1337    Ix: IndexType,
1338{
1339    let mut centrality = HashMap::new();
1340
1341    for node in graph.nodes() {
1342        let mut in_weight = 0.0;
1343        let mut out_weight = 0.0;
1344
1345        // Sum incoming edge weights
1346        if let Ok(predecessors) = graph.predecessors(node) {
1347            for pred in predecessors {
1348                if let Ok(weight) = graph.edge_weight(&pred, node) {
1349                    in_weight += weight.into();
1350                }
1351            }
1352        }
1353
1354        // Sum outgoing edge weights
1355        if let Ok(successors) = graph.successors(node) {
1356            for succ in successors {
1357                if let Ok(weight) = graph.edge_weight(node, &succ) {
1358                    out_weight += weight.into();
1359                }
1360            }
1361        }
1362
1363        centrality.insert(node.clone(), in_weight + out_weight);
1364    }
1365
1366    Ok(centrality)
1367}
1368
1369/// Calculates weighted betweenness centrality for undirected graphs
1370///
1371/// Uses Dijkstra's algorithm to find shortest weighted paths between all pairs of nodes.
1372#[allow(dead_code)]
1373fn weighted_betweenness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
1374where
1375    N: Node + std::fmt::Debug,
1376    E: EdgeWeight
1377        + Into<f64>
1378        + scirs2_core::numeric::Zero
1379        + scirs2_core::numeric::One
1380        + std::ops::Add<Output = E>
1381        + PartialOrd
1382        + Copy
1383        + std::fmt::Debug
1384        + Default,
1385    Ix: IndexType,
1386{
1387    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1388    let n = nodes.len();
1389    let mut centrality: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
1390
1391    // For each pair of nodes, find shortest weighted paths
1392    for source in &nodes {
1393        for target in &nodes {
1394            if source != target {
1395                // Find shortest weighted path
1396                if let Ok(Some(path)) = dijkstra_path(graph, source, target) {
1397                    // Count how many times each intermediate node appears
1398                    for intermediate in &path.nodes[1..path.nodes.len() - 1] {
1399                        *centrality
1400                            .get_mut(intermediate)
1401                            .expect("Failed to get mutable reference") += 1.0;
1402                    }
1403                }
1404            }
1405        }
1406    }
1407
1408    // Normalize by (n-1)(n-2) for undirected graphs
1409    if n > 2 {
1410        let normalization = ((n - 1) * (n - 2)) as f64;
1411        for value in centrality.values_mut() {
1412            *value /= normalization;
1413        }
1414    }
1415
1416    Ok(centrality)
1417}
1418
1419/// Calculates weighted betweenness centrality for directed graphs
1420#[allow(dead_code)]
1421fn weighted_betweenness_centrality_digraph<N, E, Ix>(
1422    graph: &DiGraph<N, E, Ix>,
1423) -> Result<HashMap<N, f64>>
1424where
1425    N: Node + std::fmt::Debug,
1426    E: EdgeWeight
1427        + Into<f64>
1428        + scirs2_core::numeric::Zero
1429        + scirs2_core::numeric::One
1430        + std::ops::Add<Output = E>
1431        + PartialOrd
1432        + Copy
1433        + std::fmt::Debug
1434        + Default,
1435    Ix: IndexType,
1436{
1437    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1438    let n = nodes.len();
1439    let mut centrality: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
1440
1441    // For each pair of nodes, find shortest weighted paths
1442    for source in &nodes {
1443        for target in &nodes {
1444            if source != target {
1445                // Find shortest weighted path in directed graph
1446                if let Ok(Some(path)) = dijkstra_path_digraph(graph, source, target) {
1447                    // Count how many times each intermediate node appears
1448                    for intermediate in &path.nodes[1..path.nodes.len() - 1] {
1449                        *centrality
1450                            .get_mut(intermediate)
1451                            .expect("Failed to get mutable reference") += 1.0;
1452                    }
1453                }
1454            }
1455        }
1456    }
1457
1458    // Normalize by (n-1)(n-2) for directed graphs
1459    if n > 2 {
1460        let normalization = ((n - 1) * (n - 2)) as f64;
1461        for value in centrality.values_mut() {
1462            *value /= normalization;
1463        }
1464    }
1465
1466    Ok(centrality)
1467}
1468
1469/// Calculates weighted closeness centrality for undirected graphs
1470///
1471/// Weighted closeness centrality uses the shortest weighted path distances.
1472#[allow(dead_code)]
1473fn weighted_closeness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
1474where
1475    N: Node + std::fmt::Debug,
1476    E: EdgeWeight
1477        + Into<f64>
1478        + scirs2_core::numeric::Zero
1479        + scirs2_core::numeric::One
1480        + std::ops::Add<Output = E>
1481        + PartialOrd
1482        + Copy
1483        + std::fmt::Debug
1484        + Default,
1485    Ix: IndexType,
1486{
1487    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1488    let n = nodes.len();
1489    let mut centrality = HashMap::new();
1490
1491    for node in &nodes {
1492        let mut total_distance = 0.0;
1493        let mut reachable_count = 0;
1494
1495        // Calculate shortest weighted paths to all other nodes
1496        for other in &nodes {
1497            if node != other {
1498                if let Ok(Some(path)) = dijkstra_path(graph, node, other) {
1499                    let distance: f64 = path.total_weight.into();
1500                    total_distance += distance;
1501                    reachable_count += 1;
1502                }
1503            }
1504        }
1505
1506        if reachable_count > 0 && total_distance > 0.0 {
1507            let closeness = reachable_count as f64 / total_distance;
1508            // Normalize by (n-1) to get values between 0 and 1
1509            let normalized_closeness = if n > 1 {
1510                closeness * (reachable_count as f64 / (n - 1) as f64)
1511            } else {
1512                closeness
1513            };
1514            centrality.insert(node.clone(), normalized_closeness);
1515        } else {
1516            centrality.insert(node.clone(), 0.0);
1517        }
1518    }
1519
1520    Ok(centrality)
1521}
1522
1523/// Calculates weighted closeness centrality for directed graphs
1524#[allow(dead_code)]
1525fn weighted_closeness_centrality_digraph<N, E, Ix>(
1526    graph: &DiGraph<N, E, Ix>,
1527) -> Result<HashMap<N, f64>>
1528where
1529    N: Node + std::fmt::Debug,
1530    E: EdgeWeight
1531        + Into<f64>
1532        + scirs2_core::numeric::Zero
1533        + scirs2_core::numeric::One
1534        + std::ops::Add<Output = E>
1535        + PartialOrd
1536        + Copy
1537        + std::fmt::Debug
1538        + Default,
1539    Ix: IndexType,
1540{
1541    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1542    let n = nodes.len();
1543    let mut centrality = HashMap::new();
1544
1545    for node in &nodes {
1546        let mut total_distance = 0.0;
1547        let mut reachable_count = 0;
1548
1549        // Calculate shortest weighted paths to all other nodes
1550        for other in &nodes {
1551            if node != other {
1552                if let Ok(Some(path)) = dijkstra_path_digraph(graph, node, other) {
1553                    let distance: f64 = path.total_weight.into();
1554                    total_distance += distance;
1555                    reachable_count += 1;
1556                }
1557            }
1558        }
1559
1560        if reachable_count > 0 && total_distance > 0.0 {
1561            let closeness = reachable_count as f64 / total_distance;
1562            // Normalize by (n-1) to get values between 0 and 1
1563            let normalized_closeness = if n > 1 {
1564                closeness * (reachable_count as f64 / (n - 1) as f64)
1565            } else {
1566                closeness
1567            };
1568            centrality.insert(node.clone(), normalized_closeness);
1569        } else {
1570            centrality.insert(node.clone(), 0.0);
1571        }
1572    }
1573
1574    Ok(centrality)
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579    use super::*;
1580
1581    #[test]
1582    fn test_degree_centrality() {
1583        let mut graph: Graph<char, f64> = Graph::new();
1584
1585        // Create a star graph with A in the center
1586        // A -- B
1587        // |
1588        // |
1589        // C -- D
1590
1591        graph.add_edge('A', 'B', 1.0).expect("Test failed");
1592        graph.add_edge('A', 'C', 1.0).expect("Test failed");
1593        graph.add_edge('C', 'D', 1.0).expect("Test failed");
1594
1595        let centrality = centrality(&graph, CentralityType::Degree).expect("Test failed");
1596
1597        // A has 2 connections out of 3 possible, so centrality = 2/3
1598        // B has 1 connection out of 3 possible, so centrality = 1/3
1599        // C has 2 connections out of 3 possible, so centrality = 2/3
1600        // D has 1 connection out of 3 possible, so centrality = 1/3
1601
1602        assert_eq!(centrality[&'A'], 2.0 / 3.0);
1603        assert_eq!(centrality[&'B'], 1.0 / 3.0);
1604        assert_eq!(centrality[&'C'], 2.0 / 3.0);
1605        assert_eq!(centrality[&'D'], 1.0 / 3.0);
1606    }
1607
1608    #[test]
1609    fn test_clustering_coefficient() {
1610        let mut graph: Graph<i32, f64> = Graph::new();
1611
1612        // Create a graph:
1613        // 1 -- 2 -- 3
1614        // |         |
1615        // +----4----+
1616        //      |
1617        //      5
1618
1619        graph.add_edge(1, 2, 1.0).expect("Test failed");
1620        graph.add_edge(2, 3, 1.0).expect("Test failed");
1621        graph.add_edge(3, 4, 1.0).expect("Test failed");
1622        graph.add_edge(4, 1, 1.0).expect("Test failed");
1623        graph.add_edge(4, 5, 1.0).expect("Test failed");
1624
1625        let coefficients = clustering_coefficient(&graph).expect("Test failed");
1626
1627        // Node 1 has neighbors 2 and 4, and they're not connected, so coefficient = 0/1 = 0
1628        assert_eq!(coefficients[&1], 0.0);
1629
1630        // Node 2 has neighbors 1 and 3, and they're not connected, so coefficient = 0/1 = 0
1631        assert_eq!(coefficients[&2], 0.0);
1632
1633        // Node 3 has neighbors 2 and 4, and they're not connected, so coefficient = 0/1 = 0
1634        assert_eq!(coefficients[&3], 0.0);
1635
1636        // Node 4 has neighbors 1, 3, and 5.
1637        // Neighbors 1 and 3 are not directly connected, and 5 is not connected to either 1 or 3
1638        // So coefficient = 0/3 = 0
1639        assert_eq!(coefficients[&4], 0.0);
1640
1641        // Node 5 has only one neighbor (4), so coefficient = 0
1642        assert_eq!(coefficients[&5], 0.0);
1643
1644        // Now let's add an edge to create a triangle
1645        graph.add_edge(1, 3, 1.0).expect("Test failed");
1646
1647        let coefficients = clustering_coefficient(&graph).expect("Test failed");
1648
1649        // Now nodes 1, 3, and 4 form a triangle
1650        // Node 4 has 3 neighbors (1, 3, 5) with 1 edge between them (1-3)
1651        // Possible edges: 3 choose 2 = 3
1652        // So coefficient = 1/3
1653        assert!((coefficients[&4] - 1.0 / 3.0).abs() < 1e-10);
1654    }
1655
1656    #[test]
1657    fn test_graph_density() {
1658        let mut graph: Graph<i32, f64> = Graph::new();
1659
1660        // Empty graph should error
1661        assert!(graph_density(&graph).is_err());
1662
1663        // Add one node
1664        graph.add_node(1);
1665
1666        // Graph with one node should error
1667        assert!(graph_density(&graph).is_err());
1668
1669        // Create a graph with 4 nodes and 3 edges
1670        graph.add_node(2);
1671        graph.add_node(3);
1672        graph.add_node(4);
1673
1674        graph.add_edge(1, 2, 1.0).expect("Test failed");
1675        graph.add_edge(2, 3, 1.0).expect("Test failed");
1676        graph.add_edge(3, 4, 1.0).expect("Test failed");
1677
1678        // 3 edges out of 6 possible edges (4 choose 2 = 6)
1679        let density = graph_density(&graph).expect("Test failed");
1680        assert_eq!(density, 0.5);
1681
1682        // Add 3 more edges to make a complete graph
1683        graph.add_edge(1, 3, 1.0).expect("Test failed");
1684        graph.add_edge(1, 4, 1.0).expect("Test failed");
1685        graph.add_edge(2, 4, 1.0).expect("Test failed");
1686
1687        // 6 edges out of 6 possible edges
1688        let density = graph_density(&graph).expect("Test failed");
1689        assert_eq!(density, 1.0);
1690    }
1691
1692    #[test]
1693    fn test_katz_centrality() {
1694        let mut graph: Graph<char, f64> = Graph::new();
1695
1696        // Create a simple star graph with A in the center
1697        graph.add_edge('A', 'B', 1.0).expect("Test failed");
1698        graph.add_edge('A', 'C', 1.0).expect("Test failed");
1699        graph.add_edge('A', 'D', 1.0).expect("Test failed");
1700
1701        let centrality = katz_centrality(&graph, 0.1, 1.0).expect("Test failed");
1702
1703        // The center node should have higher Katz centrality
1704        assert!(centrality[&'A'] > centrality[&'B']);
1705        assert!(centrality[&'A'] > centrality[&'C']);
1706        assert!(centrality[&'A'] > centrality[&'D']);
1707
1708        // All leaf nodes should have similar centrality
1709        assert!((centrality[&'B'] - centrality[&'C']).abs() < 0.1);
1710        assert!((centrality[&'B'] - centrality[&'D']).abs() < 0.1);
1711    }
1712
1713    #[test]
1714    fn test_pagerank_centrality() {
1715        let mut graph: Graph<char, f64> = Graph::new();
1716
1717        // Create a simple triangle
1718        graph.add_edge('A', 'B', 1.0).expect("Test failed");
1719        graph.add_edge('B', 'C', 1.0).expect("Test failed");
1720        graph.add_edge('C', 'A', 1.0).expect("Test failed");
1721
1722        let centrality = pagerank_centrality(&graph, 0.85, 1e-6).expect("Test failed");
1723
1724        // All nodes should have equal PageRank in a symmetric triangle
1725        let values: Vec<f64> = centrality.values().cloned().collect();
1726        let expected = 1.0 / 3.0; // Should sum to 1, so each gets 1/3
1727
1728        for &value in &values {
1729            assert!((value - expected).abs() < 0.1);
1730        }
1731
1732        // Check that PageRank values sum to approximately 1
1733        let sum: f64 = values.iter().sum();
1734        assert!((sum - 1.0).abs() < 0.01);
1735    }
1736
1737    #[test]
1738    fn test_pagerank_centrality_digraph() {
1739        let mut graph: DiGraph<char, f64> = DiGraph::new();
1740
1741        // Create a directed path: A -> B -> C
1742        graph.add_edge('A', 'B', 1.0).expect("Test failed");
1743        graph.add_edge('B', 'C', 1.0).expect("Test failed");
1744
1745        let centrality = pagerank_centrality_digraph(&graph, 0.85, 1e-6).expect("Test failed");
1746
1747        // C should have the highest PageRank (receives links but doesn't give any)
1748        // A should have the lowest (gives links but doesn't receive any except random jumps)
1749        assert!(centrality[&'C'] > centrality[&'B']);
1750        assert!(centrality[&'B'] > centrality[&'A']);
1751    }
1752
1753    #[test]
1754    fn test_centrality_enum_katz_pagerank() {
1755        let mut graph: Graph<char, f64> = Graph::new();
1756
1757        graph.add_edge('A', 'B', 1.0).expect("Test failed");
1758        graph.add_edge('A', 'C', 1.0).expect("Test failed");
1759
1760        // Test that the enum-based centrality function works for new types
1761        let katz_result = centrality(&graph, CentralityType::Katz).expect("Test failed");
1762        let pagerank_result = centrality(&graph, CentralityType::PageRank).expect("Test failed");
1763
1764        // Both should return valid results
1765        assert_eq!(katz_result.len(), 3);
1766        assert_eq!(pagerank_result.len(), 3);
1767
1768        // All values should be positive
1769        for value in katz_result.values() {
1770            assert!(*value > 0.0);
1771        }
1772        for value in pagerank_result.values() {
1773            assert!(*value > 0.0);
1774        }
1775    }
1776
1777    #[test]
1778    fn test_hits_algorithm() {
1779        let mut graph: DiGraph<char, f64> = DiGraph::new();
1780
1781        // Create a small web graph
1782        // A and B are hubs (point to many pages)
1783        // C and D are authorities (pointed to by many pages)
1784        graph.add_edge('A', 'C', 1.0).expect("Test failed");
1785        graph.add_edge('A', 'D', 1.0).expect("Test failed");
1786        graph.add_edge('B', 'C', 1.0).expect("Test failed");
1787        graph.add_edge('B', 'D', 1.0).expect("Test failed");
1788        // E is both a hub and authority
1789        graph.add_edge('E', 'C', 1.0).expect("Test failed");
1790        graph.add_edge('B', 'E', 1.0).expect("Test failed");
1791
1792        let hits = hits_algorithm(&graph, 100, 1e-6).expect("Test failed");
1793
1794        // Check that we have scores for all nodes
1795        assert_eq!(hits.authorities.len(), 5);
1796        assert_eq!(hits.hubs.len(), 5);
1797
1798        // C and D should have high authority scores
1799        assert!(hits.authorities[&'C'] > hits.authorities[&'A']);
1800        assert!(hits.authorities[&'D'] > hits.authorities[&'A']);
1801
1802        // A and B should have high hub scores
1803        assert!(hits.hubs[&'A'] > hits.hubs[&'C']);
1804        assert!(hits.hubs[&'B'] > hits.hubs[&'C']);
1805
1806        // Check that scores are normalized (sum of squares = 1)
1807        let auth_norm: f64 = hits.authorities.values().map(|&x| x * x).sum::<f64>();
1808        let hub_norm: f64 = hits.hubs.values().map(|&x| x * x).sum::<f64>();
1809        assert!((auth_norm - 1.0).abs() < 0.01);
1810        assert!((hub_norm - 1.0).abs() < 0.01);
1811    }
1812
1813    #[test]
1814    fn test_weighted_degree_centrality() {
1815        let mut graph = crate::generators::create_graph::<&str, f64>();
1816
1817        // Create a simple weighted graph
1818        graph.add_edge("A", "B", 2.0).expect("Test failed");
1819        graph.add_edge("A", "C", 3.0).expect("Test failed");
1820        graph.add_edge("B", "C", 1.0).expect("Test failed");
1821
1822        let centrality = centrality(&graph, CentralityType::WeightedDegree).expect("Test failed");
1823
1824        // A has edges with weights 2.0 + 3.0 = 5.0
1825        assert_eq!(centrality[&"A"], 5.0);
1826
1827        // B has edges with weights 2.0 + 1.0 = 3.0
1828        assert_eq!(centrality[&"B"], 3.0);
1829
1830        // C has edges with weights 3.0 + 1.0 = 4.0
1831        assert_eq!(centrality[&"C"], 4.0);
1832    }
1833
1834    #[test]
1835    fn test_weighted_closeness_centrality() {
1836        let mut graph = crate::generators::create_graph::<&str, f64>();
1837
1838        // Create a simple weighted graph
1839        graph.add_edge("A", "B", 1.0).expect("Test failed");
1840        graph.add_edge("B", "C", 2.0).expect("Test failed");
1841
1842        let centrality =
1843            centrality(&graph, CentralityType::WeightedCloseness).expect("Test failed");
1844
1845        // All centrality values should be positive
1846        for value in centrality.values() {
1847            assert!(*value > 0.0);
1848        }
1849
1850        // Node B should have highest closeness (shortest paths to others)
1851        assert!(centrality[&"B"] > centrality[&"A"]);
1852        assert!(centrality[&"B"] > centrality[&"C"]);
1853    }
1854
1855    #[test]
1856    fn test_weighted_betweenness_centrality() {
1857        let mut graph = crate::generators::create_graph::<&str, f64>();
1858
1859        // Create a path graph A-B-C
1860        graph.add_edge("A", "B", 1.0).expect("Test failed");
1861        graph.add_edge("B", "C", 1.0).expect("Test failed");
1862
1863        let centrality =
1864            centrality(&graph, CentralityType::WeightedBetweenness).expect("Test failed");
1865
1866        // B should have positive betweenness (lies on path from A to C)
1867        assert!(centrality[&"B"] > 0.0);
1868
1869        // A and C should have zero betweenness (no paths pass through them)
1870        assert_eq!(centrality[&"A"], 0.0);
1871        assert_eq!(centrality[&"C"], 0.0);
1872    }
1873
1874    #[test]
1875    fn test_weighted_centrality_digraph() {
1876        let mut graph = crate::generators::create_digraph::<&str, f64>();
1877
1878        // Create a simple directed weighted graph
1879        graph.add_edge("A", "B", 2.0).expect("Test failed");
1880        graph.add_edge("B", "C", 3.0).expect("Test failed");
1881        graph.add_edge("A", "C", 1.0).expect("Test failed");
1882
1883        let degree_centrality =
1884            centrality_digraph(&graph, CentralityType::WeightedDegree).expect("Test failed");
1885        let closeness_centrality =
1886            centrality_digraph(&graph, CentralityType::WeightedCloseness).expect("Test failed");
1887
1888        // All centrality values should be non-negative
1889        for value in degree_centrality.values() {
1890            assert!(*value >= 0.0);
1891        }
1892        for value in closeness_centrality.values() {
1893            assert!(*value >= 0.0);
1894        }
1895
1896        // A has weighted degree 2.0 + 1.0 = 3.0 (outgoing only)
1897        // B has weighted degree 2.0 (incoming) + 3.0 (outgoing) = 5.0 total
1898        // C has weighted degree 3.0 + 1.0 = 4.0 (incoming only)
1899        // So B should have the highest total weighted degree
1900        assert!(degree_centrality[&"B"] > degree_centrality[&"A"]);
1901        assert!(degree_centrality[&"B"] > degree_centrality[&"C"]);
1902    }
1903}