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 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        + num_traits::Zero
68        + num_traits::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        + num_traits::Zero
114        + num_traits::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        + num_traits::Zero
211        + num_traits::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        + num_traits::Zero
269        + num_traits::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        + num_traits::Zero
327        + num_traits::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        + num_traits::Zero
388        + num_traits::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 + num_traits::Zero + num_traits::One + PartialOrd + Into<f64> + std::marker::Copy,
450    Ix: petgraph::graph::IndexType,
451{
452    let nodes = graph.nodes();
453    let n = nodes.len();
454
455    if n == 0 {
456        return Err(GraphError::InvalidGraph("Empty _graph".to_string()));
457    }
458
459    // Get adjacency matrix with weights converted to f64
460    let adj_mat = graph.adjacency_matrix();
461    let mut adj_f64 = Array2::<f64>::zeros((n, n));
462    for i in 0..n {
463        for j in 0..n {
464            adj_f64[[i, j]] = adj_mat[[i, j]].into();
465        }
466    }
467
468    // Start with uniform vector
469    let mut x = Array1::<f64>::ones(n);
470    x.mapv_inplace(|v| v / (n as f64).sqrt()); // Normalize
471
472    // Power iteration
473    let max_iter = 100;
474    let tol = 1e-6;
475
476    for _ in 0..max_iter {
477        // y = A * x
478        let y = adj_f64.dot(&x);
479
480        // Compute the norm
481        let norm = (y.iter().map(|v| v * v).sum::<f64>()).sqrt();
482        if norm < 1e-10 {
483            // If the norm is too small, we have a zero vector
484            return Err(GraphError::AlgorithmError(
485                "Eigenvector centrality computation failed: zero eigenvalue".to_string(),
486            ));
487        }
488
489        // Normalize y
490        let y_norm = y / norm;
491
492        // Check for convergence
493        let diff = (&y_norm - &x).iter().map(|v| v.abs()).sum::<f64>();
494        if diff < tol {
495            // We've converged, create the result
496            let mut result = HashMap::new();
497            for (i, &node) in nodes.iter().enumerate() {
498                result.insert(node.clone(), y_norm[i]);
499            }
500            return Ok(result);
501        }
502
503        // Update x for next iteration
504        x = y_norm;
505    }
506
507    // We've reached max iterations without converging
508    Err(GraphError::AlgorithmError(
509        "Eigenvector centrality computation did not converge".to_string(),
510    ))
511}
512
513/// Calculates eigenvector centrality for nodes in a directed graph
514///
515/// Uses the power iteration method.
516#[allow(dead_code)]
517fn eigenvector_centrality_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<HashMap<N, f64>>
518where
519    N: Node + std::fmt::Debug,
520    E: EdgeWeight + num_traits::Zero + num_traits::One + PartialOrd + Into<f64> + std::marker::Copy,
521    Ix: petgraph::graph::IndexType,
522{
523    let nodes = graph.nodes();
524    let n = nodes.len();
525
526    if n == 0 {
527        return Err(GraphError::InvalidGraph("Empty _graph".to_string()));
528    }
529
530    // Get adjacency matrix with weights converted to f64
531    let adj_mat = graph.adjacency_matrix();
532    let mut adj_f64 = Array2::<f64>::zeros((n, n));
533    for i in 0..n {
534        for j in 0..n {
535            adj_f64[[i, j]] = adj_mat[[i, j]].into();
536        }
537    }
538
539    // Start with uniform vector
540    let mut x = Array1::<f64>::ones(n);
541    x.mapv_inplace(|v| v / (n as f64).sqrt()); // Normalize
542
543    // Power iteration
544    let max_iter = 100;
545    let tol = 1e-6;
546
547    for _ in 0..max_iter {
548        // y = A * x
549        let y = adj_f64.dot(&x);
550
551        // Compute the norm
552        let norm = (y.iter().map(|v| v * v).sum::<f64>()).sqrt();
553        if norm < 1e-10 {
554            // If the norm is too small, we have a zero vector
555            return Err(GraphError::AlgorithmError(
556                "Eigenvector centrality computation failed: zero eigenvalue".to_string(),
557            ));
558        }
559
560        // Normalize y
561        let y_norm = y / norm;
562
563        // Check for convergence
564        let diff = (&y_norm - &x).iter().map(|v| v.abs()).sum::<f64>();
565        if diff < tol {
566            // We've converged, create the result
567            let mut result = HashMap::new();
568            for (i, &node) in nodes.iter().enumerate() {
569                result.insert(node.clone(), y_norm[i]);
570            }
571            return Ok(result);
572        }
573
574        // Update x for next iteration
575        x = y_norm;
576    }
577
578    // We've reached max iterations without converging
579    Err(GraphError::AlgorithmError(
580        "Eigenvector centrality computation did not converge".to_string(),
581    ))
582}
583
584/// Calculates the local clustering coefficient for each node in an undirected graph
585///
586/// The clustering coefficient measures how close a node's neighbors are to being a clique.
587///
588/// # Arguments
589/// * `graph` - The graph to analyze
590///
591/// # Returns
592/// * `Result<HashMap<N, f64>>` - A map from nodes to their clustering coefficients
593#[allow(dead_code)]
594pub fn clustering_coefficient<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
595where
596    N: Node + std::fmt::Debug,
597    E: EdgeWeight,
598    Ix: petgraph::graph::IndexType,
599{
600    let mut coefficients = HashMap::new();
601
602    for (idx, &node) in graph.nodes().iter().enumerate() {
603        // Get the neighbors of this node
604        let node_idx = petgraph::graph::NodeIndex::new(idx);
605        let neighbors: HashSet<_> = graph.inner().neighbors(node_idx).collect();
606
607        let k = neighbors.len();
608
609        // If the node has less than 2 neighbors, its clustering coefficient is 0
610        if k < 2 {
611            coefficients.insert(node.clone(), 0.0);
612            continue;
613        }
614
615        // Count the number of edges between neighbors
616        let mut edge_count = 0;
617
618        for &n1 in &neighbors {
619            for &n2 in &neighbors {
620                if n1 != n2 && graph.inner().contains_edge(n1, n2) {
621                    edge_count += 1;
622                }
623            }
624        }
625
626        // Each edge is counted twice (once from each direction)
627        edge_count /= 2;
628
629        // Calculate the clustering coefficient
630        let possible_edges = k * (k - 1) / 2;
631        let coefficient = edge_count as f64 / possible_edges as f64;
632
633        coefficients.insert(node.clone(), coefficient);
634    }
635
636    Ok(coefficients)
637}
638
639/// Calculates the global clustering coefficient (transitivity) of a graph
640///
641/// This is the ratio of the number of closed triplets to the total number of triplets.
642///
643/// # Arguments
644/// * `graph` - The graph to analyze
645///
646/// # Returns
647/// * `Result<f64>` - The global clustering coefficient
648#[allow(dead_code)]
649pub fn graph_density<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<f64>
650where
651    N: Node + std::fmt::Debug,
652    E: EdgeWeight,
653    Ix: petgraph::graph::IndexType,
654{
655    let n = graph.node_count();
656
657    if n <= 1 {
658        return Err(GraphError::InvalidGraph(
659            "Graph density undefined for graphs with 0 or 1 nodes".to_string(),
660        ));
661    }
662
663    let m = graph.edge_count();
664    let possible_edges = n * (n - 1) / 2;
665
666    Ok(m as f64 / possible_edges as f64)
667}
668
669/// Calculates the density of a directed graph
670///
671/// # Arguments
672/// * `graph` - The directed graph to analyze
673///
674/// # Returns
675/// * `Result<f64>` - The graph density
676#[allow(dead_code)]
677pub fn graph_density_digraph<N, E, Ix>(graph: &DiGraph<N, E, Ix>) -> Result<f64>
678where
679    N: Node + std::fmt::Debug,
680    E: EdgeWeight,
681    Ix: petgraph::graph::IndexType,
682{
683    let n = graph.node_count();
684
685    if n <= 1 {
686        return Err(GraphError::InvalidGraph(
687            "Graph density undefined for graphs with 0 or 1 nodes".to_string(),
688        ));
689    }
690
691    let m = graph.edge_count();
692    let possible_edges = n * (n - 1); // In directed graphs, there can be n(n-1) edges
693
694    Ok(m as f64 / possible_edges as f64)
695}
696
697/// Calculates Katz centrality for nodes in an undirected graph
698///
699/// Katz centrality is a variant of eigenvector centrality that considers all paths between nodes,
700/// with paths of longer length given exponentially decreasing weights.
701///
702/// # Arguments
703/// * `graph` - The graph to analyze
704/// * `alpha` - Attenuation factor (must be smaller than the reciprocal of the largest eigenvalue)
705/// * `beta` - Weight attributed to the immediate neighborhood
706///
707/// # Returns
708/// * `Result<HashMap<N, f64>>` - Katz centrality values
709#[allow(dead_code)]
710pub fn katz_centrality<N, E, Ix>(
711    graph: &Graph<N, E, Ix>,
712    alpha: f64,
713    beta: f64,
714) -> Result<HashMap<N, f64>>
715where
716    N: Node + std::fmt::Debug,
717    E: EdgeWeight + num_traits::Zero + num_traits::One + Into<f64> + Copy,
718    Ix: petgraph::graph::IndexType,
719{
720    let nodes = graph.nodes();
721    let n = nodes.len();
722
723    if n == 0 {
724        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
725    }
726
727    // Get adjacency matrix
728    let adj_mat = graph.adjacency_matrix();
729    let mut adj_f64 = Array2::<f64>::zeros((n, n));
730    for i in 0..n {
731        for j in 0..n {
732            adj_f64[[i, j]] = adj_mat[[i, j]].into();
733        }
734    }
735
736    // Create identity matrix
737    let mut identity = Array2::<f64>::zeros((n, n));
738    for i in 0..n {
739        identity[[i, i]] = 1.0;
740    }
741
742    // Compute (I - α*A) - not used in iterative method but kept for reference
743    let _factor_matrix = &identity - &(alpha * &adj_f64);
744
745    // Create beta vector (all entries = beta)
746    let beta_vec = Array1::<f64>::from_elem(n, beta);
747
748    // We need to solve (I - α*A) * c = β*1
749    // For simplicity, we'll use an iterative approach (power method variant)
750    let mut centrality_vec = Array1::<f64>::ones(n);
751    let max_iter = 100;
752    let tol = 1e-6;
753
754    for _ in 0..max_iter {
755        // c_new = α*A*c + β*1
756        let new_centrality = alpha * adj_f64.dot(&centrality_vec) + &beta_vec;
757
758        // Check for convergence
759        let diff = (&new_centrality - &centrality_vec)
760            .iter()
761            .map(|v| v.abs())
762            .sum::<f64>();
763        if diff < tol {
764            centrality_vec = new_centrality;
765            break;
766        }
767
768        centrality_vec = new_centrality;
769    }
770
771    // Convert to HashMap
772    let mut result = HashMap::new();
773    for (i, &node) in nodes.iter().enumerate() {
774        result.insert(node.clone(), centrality_vec[i]);
775    }
776
777    Ok(result)
778}
779
780/// Calculates Katz centrality for nodes in a directed graph
781///
782/// # Arguments
783/// * `graph` - The directed graph to analyze
784/// * `alpha` - Attenuation factor
785/// * `beta` - Weight attributed to the immediate neighborhood
786///
787/// # Returns
788/// * `Result<HashMap<N, f64>>` - Katz centrality values
789#[allow(dead_code)]
790pub fn katz_centrality_digraph<N, E, Ix>(
791    graph: &DiGraph<N, E, Ix>,
792    alpha: f64,
793    beta: f64,
794) -> Result<HashMap<N, f64>>
795where
796    N: Node + std::fmt::Debug,
797    E: EdgeWeight + num_traits::Zero + num_traits::One + Into<f64> + Copy,
798    Ix: petgraph::graph::IndexType,
799{
800    let nodes = graph.nodes();
801    let n = nodes.len();
802
803    if n == 0 {
804        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
805    }
806
807    // Get adjacency matrix
808    let adj_mat = graph.adjacency_matrix();
809    let mut adj_f64 = Array2::<f64>::zeros((n, n));
810    for i in 0..n {
811        for j in 0..n {
812            adj_f64[[i, j]] = adj_mat[[i, j]].into();
813        }
814    }
815
816    // Create beta vector
817    let beta_vec = Array1::<f64>::from_elem(n, beta);
818
819    // Iterative approach
820    let mut centrality_vec = Array1::<f64>::ones(n);
821    let max_iter = 100;
822    let tol = 1e-6;
823
824    for _ in 0..max_iter {
825        // c_new = α*A^T*c + β*1 (transpose for incoming links)
826        let new_centrality = alpha * adj_f64.t().dot(&centrality_vec) + &beta_vec;
827
828        // Check for convergence
829        let diff = (&new_centrality - &centrality_vec)
830            .iter()
831            .map(|v| v.abs())
832            .sum::<f64>();
833        if diff < tol {
834            centrality_vec = new_centrality;
835            break;
836        }
837
838        centrality_vec = new_centrality;
839    }
840
841    // Convert to HashMap
842    let mut result = HashMap::new();
843    for (i, &node) in nodes.iter().enumerate() {
844        result.insert(node.clone(), centrality_vec[i]);
845    }
846
847    Ok(result)
848}
849
850/// Calculates PageRank centrality for nodes in an undirected graph
851///
852/// PageRank is Google's famous algorithm for ranking web pages.
853/// For undirected graphs, we treat each edge as bidirectional.
854///
855/// # Arguments
856/// * `graph` - The graph to analyze
857/// * `damping` - Damping parameter (typically 0.85)
858/// * `tolerance` - Convergence tolerance
859///
860/// # Returns
861/// * `Result<HashMap<N, f64>>` - PageRank values
862#[allow(dead_code)]
863pub fn pagerank_centrality<N, E, Ix>(
864    graph: &Graph<N, E, Ix>,
865    damping: f64,
866    tolerance: f64,
867) -> Result<HashMap<N, f64>>
868where
869    N: Node + std::fmt::Debug,
870    E: EdgeWeight,
871    Ix: petgraph::graph::IndexType,
872{
873    let nodes = graph.nodes();
874    let n = nodes.len();
875
876    if n == 0 {
877        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
878    }
879
880    // Initialize PageRank values
881    let mut pagerank = Array1::<f64>::from_elem(n, 1.0 / n as f64);
882    let max_iter = 100;
883
884    // Get degree of each node for normalization
885    let degrees = graph.degree_vector();
886
887    for _ in 0..max_iter {
888        let mut new_pagerank = Array1::<f64>::from_elem(n, (1.0 - damping) / n as f64);
889
890        // Calculate PageRank contribution from each node
891        for (i, node_idx) in graph.inner().node_indices().enumerate() {
892            let node_degree = degrees[i] as f64;
893            if node_degree > 0.0 {
894                let contribution = damping * pagerank[i] / node_degree;
895
896                // Distribute to all neighbors
897                for neighbor_idx in graph.inner().neighbors(node_idx) {
898                    let neighbor_i = neighbor_idx.index();
899                    new_pagerank[neighbor_i] += contribution;
900                }
901            }
902        }
903
904        // Check for convergence
905        let diff = (&new_pagerank - &pagerank)
906            .iter()
907            .map(|v| v.abs())
908            .sum::<f64>();
909        if diff < tolerance {
910            pagerank = new_pagerank;
911            break;
912        }
913
914        pagerank = new_pagerank;
915    }
916
917    // Convert to HashMap
918    let mut result = HashMap::new();
919    for (i, &node) in nodes.iter().enumerate() {
920        result.insert(node.clone(), pagerank[i]);
921    }
922
923    Ok(result)
924}
925
926/// Calculates PageRank centrality using parallel processing for large graphs
927///
928/// This parallel implementation of PageRank uses scirs2-core parallel operations
929/// to accelerate computation on large graphs. It's particularly effective when
930/// the graph has >10,000 nodes and sufficient CPU cores are available.
931///
932/// # Arguments
933/// * `graph` - The graph to analyze
934/// * `damping` - Damping parameter (typically 0.85)
935/// * `tolerance` - Convergence tolerance
936/// * `max_iterations` - Maximum number of iterations (default: 100)
937///
938/// # Returns
939/// * `Result<HashMap<N, f64>>` - PageRank values
940///
941/// # Time Complexity
942/// O((V + E) * k / p) where V is nodes, E is edges, k is iterations,
943/// and p is the number of parallel threads.
944///
945/// # Space Complexity
946/// O(V + t) where t is the number of threads for thread-local storage.
947///
948/// # Performance Notes
949/// - Best performance gain on graphs with >10,000 nodes
950/// - Requires multiple CPU cores for meaningful speedup
951/// - Memory access patterns optimized for cache efficiency
952#[allow(dead_code)]
953pub fn parallel_pagerank_centrality<N, E, Ix>(
954    graph: &Graph<N, E, Ix>,
955    damping: f64,
956    tolerance: f64,
957    max_iterations: Option<usize>,
958) -> Result<HashMap<N, f64>>
959where
960    N: Node + Send + Sync + std::fmt::Debug,
961    E: EdgeWeight + Send + Sync,
962    Ix: petgraph::graph::IndexType + Send + Sync,
963{
964    use scirs2_core::parallel_ops::*;
965    use std::sync::{Arc, Mutex};
966
967    let nodes = graph.nodes();
968    let n = nodes.len();
969
970    if n == 0 {
971        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
972    }
973
974    // For small graphs, use sequential implementation
975    if n < 1000 {
976        return pagerank_centrality(graph, damping, tolerance);
977    }
978
979    let max_iter = max_iterations.unwrap_or(100);
980
981    // Initialize PageRank values
982    let mut pagerank = Array1::<f64>::from_elem(n, 1.0 / n as f64);
983
984    // Get degree of each node for normalization
985    let degrees = graph.degree_vector();
986
987    // Pre-compute neighbor indices for efficient parallel access
988    let neighbor_lists: Vec<Vec<usize>> = (0..n)
989        .map(|i| {
990            let node_idx = petgraph::graph::NodeIndex::new(i);
991            graph
992                .inner()
993                .neighbors(node_idx)
994                .map(|neighbor_idx| neighbor_idx.index())
995                .collect()
996        })
997        .collect();
998
999    for _iteration in 0..max_iter {
1000        // Parallel computation of new PageRank values
1001        let new_pagerank = Arc::new(Mutex::new(Array1::<f64>::from_elem(
1002            n,
1003            (1.0 - damping) / n as f64,
1004        )));
1005
1006        // Use parallel iteration for PageRank calculation
1007        (0..n).into_par_iter().for_each(|i| {
1008            let node_degree = degrees[i] as f64;
1009            if node_degree > 0.0 {
1010                let contribution = damping * pagerank[i] / node_degree;
1011
1012                // Distribute to all neighbors
1013                let mut local_updates = Vec::new();
1014                for &neighbor_i in &neighbor_lists[i] {
1015                    local_updates.push((neighbor_i, contribution));
1016                }
1017
1018                // Apply updates atomically
1019                if !local_updates.is_empty() {
1020                    let mut new_pr = new_pagerank.lock().unwrap();
1021                    for (neighbor_i, contrib) in local_updates {
1022                        new_pr[neighbor_i] += contrib;
1023                    }
1024                }
1025            }
1026        });
1027
1028        let new_pagerank = Arc::try_unwrap(new_pagerank).unwrap().into_inner().unwrap();
1029
1030        // Convergence check
1031        let diff = pagerank
1032            .iter()
1033            .zip(new_pagerank.iter())
1034            .map(|(old, new)| (new - old).abs())
1035            .sum::<f64>();
1036
1037        if diff < tolerance {
1038            pagerank = new_pagerank;
1039            break;
1040        }
1041
1042        pagerank = new_pagerank;
1043    }
1044
1045    // Conversion to HashMap
1046    let result: HashMap<N, f64> = nodes
1047        .iter()
1048        .enumerate()
1049        .map(|(i, node)| ((*node).clone(), pagerank[i]))
1050        .collect();
1051
1052    Ok(result)
1053}
1054
1055/// HITS (Hyperlink-Induced Topic Search) algorithm result
1056#[derive(Debug, Clone)]
1057pub struct HitsScores<N: Node> {
1058    /// Authority scores for each node
1059    pub authorities: HashMap<N, f64>,
1060    /// Hub scores for each node
1061    pub hubs: HashMap<N, f64>,
1062}
1063
1064/// Compute HITS algorithm for a directed graph
1065///
1066/// The HITS algorithm computes two scores for each node:
1067/// - Authority score: nodes that are pointed to by many hubs
1068/// - Hub score: nodes that point to many authorities
1069///
1070/// # Arguments
1071/// * `graph` - The directed graph
1072/// * `max_iter` - Maximum number of iterations
1073/// * `tolerance` - Convergence tolerance
1074///
1075/// # Returns
1076/// * HitsScores containing authority and hub scores for each node
1077#[allow(dead_code)]
1078pub fn hits_algorithm<N, E, Ix>(
1079    graph: &DiGraph<N, E, Ix>,
1080    max_iter: usize,
1081    tolerance: f64,
1082) -> Result<HitsScores<N>>
1083where
1084    N: Node + Clone + Hash + Eq + std::fmt::Debug,
1085    E: EdgeWeight,
1086    Ix: IndexType,
1087{
1088    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1089    let n = nodes.len();
1090
1091    if n == 0 {
1092        return Ok(HitsScores {
1093            authorities: HashMap::new(),
1094            hubs: HashMap::new(),
1095        });
1096    }
1097
1098    // Initialize scores
1099    let mut authorities = vec![1.0 / (n as f64).sqrt(); n];
1100    let mut hubs = vec![1.0 / (n as f64).sqrt(); n];
1101    let mut new_authorities = vec![0.0; n];
1102    let mut new_hubs = vec![0.0; n];
1103
1104    // Create node index mapping
1105    let node_to_idx: HashMap<N, usize> = nodes
1106        .iter()
1107        .enumerate()
1108        .map(|(i, n)| (n.clone(), i))
1109        .collect();
1110
1111    // Iterate until convergence
1112    for _ in 0..max_iter {
1113        // Update authority scores
1114        new_authorities.fill(0.0);
1115        for (i, node) in nodes.iter().enumerate() {
1116            // Authority score is sum of hub scores of nodes pointing to it
1117            if let Ok(predecessors) = graph.predecessors(node) {
1118                for pred in predecessors {
1119                    if let Some(&pred_idx) = node_to_idx.get(&pred) {
1120                        new_authorities[i] += hubs[pred_idx];
1121                    }
1122                }
1123            }
1124        }
1125
1126        // Update hub scores
1127        new_hubs.fill(0.0);
1128        for (i, node) in nodes.iter().enumerate() {
1129            // Hub score is sum of authority scores of nodes it points to
1130            if let Ok(successors) = graph.successors(node) {
1131                for succ in successors {
1132                    if let Some(&succ_idx) = node_to_idx.get(&succ) {
1133                        new_hubs[i] += authorities[succ_idx];
1134                    }
1135                }
1136            }
1137        }
1138
1139        // Normalize scores
1140        let auth_norm: f64 = new_authorities.iter().map(|x| x * x).sum::<f64>().sqrt();
1141        let hub_norm: f64 = new_hubs.iter().map(|x| x * x).sum::<f64>().sqrt();
1142
1143        if auth_norm > 0.0 {
1144            for score in &mut new_authorities {
1145                *score /= auth_norm;
1146            }
1147        }
1148
1149        if hub_norm > 0.0 {
1150            for score in &mut new_hubs {
1151                *score /= hub_norm;
1152            }
1153        }
1154
1155        // Check convergence
1156        let auth_diff: f64 = authorities
1157            .iter()
1158            .zip(&new_authorities)
1159            .map(|(old, new)| (old - new).abs())
1160            .sum();
1161        let hub_diff: f64 = hubs
1162            .iter()
1163            .zip(&new_hubs)
1164            .map(|(old, new)| (old - new).abs())
1165            .sum();
1166
1167        if auth_diff < tolerance && hub_diff < tolerance {
1168            break;
1169        }
1170
1171        // Update scores
1172        authorities.copy_from_slice(&new_authorities);
1173        hubs.copy_from_slice(&new_hubs);
1174    }
1175
1176    // Convert to HashMap
1177    let authority_map = nodes
1178        .iter()
1179        .enumerate()
1180        .map(|(i, n)| (n.clone(), authorities[i]))
1181        .collect();
1182    let hub_map = nodes
1183        .iter()
1184        .enumerate()
1185        .map(|(i, n)| (n.clone(), hubs[i]))
1186        .collect();
1187
1188    Ok(HitsScores {
1189        authorities: authority_map,
1190        hubs: hub_map,
1191    })
1192}
1193
1194/// Calculates PageRank centrality for nodes in a directed graph
1195///
1196/// # Arguments
1197/// * `graph` - The directed graph to analyze
1198/// * `damping` - Damping parameter (typically 0.85)
1199/// * `tolerance` - Convergence tolerance
1200///
1201/// # Returns
1202/// * `Result<HashMap<N, f64>>` - PageRank values
1203#[allow(dead_code)]
1204pub fn pagerank_centrality_digraph<N, E, Ix>(
1205    graph: &DiGraph<N, E, Ix>,
1206    damping: f64,
1207    tolerance: f64,
1208) -> Result<HashMap<N, f64>>
1209where
1210    N: Node + std::fmt::Debug,
1211    E: EdgeWeight,
1212    Ix: petgraph::graph::IndexType,
1213{
1214    let nodes = graph.nodes();
1215    let n = nodes.len();
1216
1217    if n == 0 {
1218        return Err(GraphError::InvalidGraph("Empty graph".to_string()));
1219    }
1220
1221    // Initialize PageRank values
1222    let mut pagerank = Array1::<f64>::from_elem(n, 1.0 / n as f64);
1223    let max_iter = 100;
1224
1225    // Get out-degree of each node for normalization
1226    let out_degrees = graph.out_degree_vector();
1227
1228    for _ in 0..max_iter {
1229        let mut new_pagerank = Array1::<f64>::from_elem(n, (1.0 - damping) / n as f64);
1230
1231        // Calculate PageRank contribution from each node
1232        for (i, node_idx) in graph.inner().node_indices().enumerate() {
1233            let node_out_degree = out_degrees[i] as f64;
1234            if node_out_degree > 0.0 {
1235                let contribution = damping * pagerank[i] / node_out_degree;
1236
1237                // Distribute to all outgoing neighbors
1238                for neighbor_idx in graph
1239                    .inner()
1240                    .neighbors_directed(node_idx, petgraph::Direction::Outgoing)
1241                {
1242                    let neighbor_i = neighbor_idx.index();
1243                    new_pagerank[neighbor_i] += contribution;
1244                }
1245            } else {
1246                // Handle dangling nodes by distributing equally to all nodes
1247                let contribution = damping * pagerank[i] / n as f64;
1248                for j in 0..n {
1249                    new_pagerank[j] += contribution;
1250                }
1251            }
1252        }
1253
1254        // Check for convergence
1255        let diff = (&new_pagerank - &pagerank)
1256            .iter()
1257            .map(|v| v.abs())
1258            .sum::<f64>();
1259        if diff < tolerance {
1260            pagerank = new_pagerank;
1261            break;
1262        }
1263
1264        pagerank = new_pagerank;
1265    }
1266
1267    // Convert to HashMap
1268    let mut result = HashMap::new();
1269    for (i, &node) in nodes.iter().enumerate() {
1270        result.insert(node.clone(), pagerank[i]);
1271    }
1272
1273    Ok(result)
1274}
1275
1276/// Calculates weighted degree centrality for undirected graphs
1277///
1278/// Weighted degree centrality is the sum of the weights of all edges incident to a node.
1279#[allow(dead_code)]
1280fn weighted_degree_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
1281where
1282    N: Node + std::fmt::Debug,
1283    E: EdgeWeight + Into<f64> + Clone,
1284    Ix: IndexType,
1285{
1286    let mut centrality = HashMap::new();
1287
1288    for node in graph.nodes() {
1289        let mut weight_sum = 0.0;
1290
1291        if let Ok(neighbors) = graph.neighbors(node) {
1292            for neighbor in neighbors {
1293                if let Ok(weight) = graph.edge_weight(node, &neighbor) {
1294                    weight_sum += weight.into();
1295                }
1296            }
1297        }
1298
1299        centrality.insert(node.clone(), weight_sum);
1300    }
1301
1302    Ok(centrality)
1303}
1304
1305/// Calculates weighted degree centrality for directed graphs
1306#[allow(dead_code)]
1307fn weighted_degree_centrality_digraph<N, E, Ix>(
1308    graph: &DiGraph<N, E, Ix>,
1309) -> Result<HashMap<N, f64>>
1310where
1311    N: Node + std::fmt::Debug,
1312    E: EdgeWeight + Into<f64> + Clone,
1313    Ix: IndexType,
1314{
1315    let mut centrality = HashMap::new();
1316
1317    for node in graph.nodes() {
1318        let mut in_weight = 0.0;
1319        let mut out_weight = 0.0;
1320
1321        // Sum incoming edge weights
1322        if let Ok(predecessors) = graph.predecessors(node) {
1323            for pred in predecessors {
1324                if let Ok(weight) = graph.edge_weight(&pred, node) {
1325                    in_weight += weight.into();
1326                }
1327            }
1328        }
1329
1330        // Sum outgoing edge weights
1331        if let Ok(successors) = graph.successors(node) {
1332            for succ in successors {
1333                if let Ok(weight) = graph.edge_weight(node, &succ) {
1334                    out_weight += weight.into();
1335                }
1336            }
1337        }
1338
1339        centrality.insert(node.clone(), in_weight + out_weight);
1340    }
1341
1342    Ok(centrality)
1343}
1344
1345/// Calculates weighted betweenness centrality for undirected graphs
1346///
1347/// Uses Dijkstra's algorithm to find shortest weighted paths between all pairs of nodes.
1348#[allow(dead_code)]
1349fn weighted_betweenness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
1350where
1351    N: Node + std::fmt::Debug,
1352    E: EdgeWeight
1353        + Into<f64>
1354        + num_traits::Zero
1355        + num_traits::One
1356        + std::ops::Add<Output = E>
1357        + PartialOrd
1358        + Copy
1359        + std::fmt::Debug
1360        + Default,
1361    Ix: IndexType,
1362{
1363    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1364    let n = nodes.len();
1365    let mut centrality: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
1366
1367    // For each pair of nodes, find shortest weighted paths
1368    for source in &nodes {
1369        for target in &nodes {
1370            if source != target {
1371                // Find shortest weighted path
1372                if let Ok(Some(path)) = dijkstra_path(graph, source, target) {
1373                    // Count how many times each intermediate node appears
1374                    for intermediate in &path.nodes[1..path.nodes.len() - 1] {
1375                        *centrality.get_mut(intermediate).unwrap() += 1.0;
1376                    }
1377                }
1378            }
1379        }
1380    }
1381
1382    // Normalize by (n-1)(n-2) for undirected graphs
1383    if n > 2 {
1384        let normalization = ((n - 1) * (n - 2)) as f64;
1385        for value in centrality.values_mut() {
1386            *value /= normalization;
1387        }
1388    }
1389
1390    Ok(centrality)
1391}
1392
1393/// Calculates weighted betweenness centrality for directed graphs
1394#[allow(dead_code)]
1395fn weighted_betweenness_centrality_digraph<N, E, Ix>(
1396    graph: &DiGraph<N, E, Ix>,
1397) -> Result<HashMap<N, f64>>
1398where
1399    N: Node + std::fmt::Debug,
1400    E: EdgeWeight
1401        + Into<f64>
1402        + num_traits::Zero
1403        + num_traits::One
1404        + std::ops::Add<Output = E>
1405        + PartialOrd
1406        + Copy
1407        + std::fmt::Debug
1408        + Default,
1409    Ix: IndexType,
1410{
1411    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1412    let n = nodes.len();
1413    let mut centrality: HashMap<N, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
1414
1415    // For each pair of nodes, find shortest weighted paths
1416    for source in &nodes {
1417        for target in &nodes {
1418            if source != target {
1419                // Find shortest weighted path in directed graph
1420                if let Ok(Some(path)) = dijkstra_path_digraph(graph, source, target) {
1421                    // Count how many times each intermediate node appears
1422                    for intermediate in &path.nodes[1..path.nodes.len() - 1] {
1423                        *centrality.get_mut(intermediate).unwrap() += 1.0;
1424                    }
1425                }
1426            }
1427        }
1428    }
1429
1430    // Normalize by (n-1)(n-2) for directed graphs
1431    if n > 2 {
1432        let normalization = ((n - 1) * (n - 2)) as f64;
1433        for value in centrality.values_mut() {
1434            *value /= normalization;
1435        }
1436    }
1437
1438    Ok(centrality)
1439}
1440
1441/// Calculates weighted closeness centrality for undirected graphs
1442///
1443/// Weighted closeness centrality uses the shortest weighted path distances.
1444#[allow(dead_code)]
1445fn weighted_closeness_centrality<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, f64>>
1446where
1447    N: Node + std::fmt::Debug,
1448    E: EdgeWeight
1449        + Into<f64>
1450        + num_traits::Zero
1451        + num_traits::One
1452        + std::ops::Add<Output = E>
1453        + PartialOrd
1454        + Copy
1455        + std::fmt::Debug
1456        + Default,
1457    Ix: IndexType,
1458{
1459    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1460    let n = nodes.len();
1461    let mut centrality = HashMap::new();
1462
1463    for node in &nodes {
1464        let mut total_distance = 0.0;
1465        let mut reachable_count = 0;
1466
1467        // Calculate shortest weighted paths to all other nodes
1468        for other in &nodes {
1469            if node != other {
1470                if let Ok(Some(path)) = dijkstra_path(graph, node, other) {
1471                    let distance: f64 = path.total_weight.into();
1472                    total_distance += distance;
1473                    reachable_count += 1;
1474                }
1475            }
1476        }
1477
1478        if reachable_count > 0 && total_distance > 0.0 {
1479            let closeness = reachable_count as f64 / total_distance;
1480            // Normalize by (n-1) to get values between 0 and 1
1481            let normalized_closeness = if n > 1 {
1482                closeness * (reachable_count as f64 / (n - 1) as f64)
1483            } else {
1484                closeness
1485            };
1486            centrality.insert(node.clone(), normalized_closeness);
1487        } else {
1488            centrality.insert(node.clone(), 0.0);
1489        }
1490    }
1491
1492    Ok(centrality)
1493}
1494
1495/// Calculates weighted closeness centrality for directed graphs
1496#[allow(dead_code)]
1497fn weighted_closeness_centrality_digraph<N, E, Ix>(
1498    graph: &DiGraph<N, E, Ix>,
1499) -> Result<HashMap<N, f64>>
1500where
1501    N: Node + std::fmt::Debug,
1502    E: EdgeWeight
1503        + Into<f64>
1504        + num_traits::Zero
1505        + num_traits::One
1506        + std::ops::Add<Output = E>
1507        + PartialOrd
1508        + Copy
1509        + std::fmt::Debug
1510        + Default,
1511    Ix: IndexType,
1512{
1513    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1514    let n = nodes.len();
1515    let mut centrality = HashMap::new();
1516
1517    for node in &nodes {
1518        let mut total_distance = 0.0;
1519        let mut reachable_count = 0;
1520
1521        // Calculate shortest weighted paths to all other nodes
1522        for other in &nodes {
1523            if node != other {
1524                if let Ok(Some(path)) = dijkstra_path_digraph(graph, node, other) {
1525                    let distance: f64 = path.total_weight.into();
1526                    total_distance += distance;
1527                    reachable_count += 1;
1528                }
1529            }
1530        }
1531
1532        if reachable_count > 0 && total_distance > 0.0 {
1533            let closeness = reachable_count as f64 / total_distance;
1534            // Normalize by (n-1) to get values between 0 and 1
1535            let normalized_closeness = if n > 1 {
1536                closeness * (reachable_count as f64 / (n - 1) as f64)
1537            } else {
1538                closeness
1539            };
1540            centrality.insert(node.clone(), normalized_closeness);
1541        } else {
1542            centrality.insert(node.clone(), 0.0);
1543        }
1544    }
1545
1546    Ok(centrality)
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551    use super::*;
1552
1553    #[test]
1554    fn test_degree_centrality() {
1555        let mut graph: Graph<char, f64> = Graph::new();
1556
1557        // Create a star graph with A in the center
1558        // A -- B
1559        // |
1560        // |
1561        // C -- D
1562
1563        graph.add_edge('A', 'B', 1.0).unwrap();
1564        graph.add_edge('A', 'C', 1.0).unwrap();
1565        graph.add_edge('C', 'D', 1.0).unwrap();
1566
1567        let centrality = centrality(&graph, CentralityType::Degree).unwrap();
1568
1569        // A has 2 connections out of 3 possible, so centrality = 2/3
1570        // B has 1 connection out of 3 possible, so centrality = 1/3
1571        // C has 2 connections out of 3 possible, so centrality = 2/3
1572        // D has 1 connection out of 3 possible, so centrality = 1/3
1573
1574        assert_eq!(centrality[&'A'], 2.0 / 3.0);
1575        assert_eq!(centrality[&'B'], 1.0 / 3.0);
1576        assert_eq!(centrality[&'C'], 2.0 / 3.0);
1577        assert_eq!(centrality[&'D'], 1.0 / 3.0);
1578    }
1579
1580    #[test]
1581    fn test_clustering_coefficient() {
1582        let mut graph: Graph<i32, f64> = Graph::new();
1583
1584        // Create a graph:
1585        // 1 -- 2 -- 3
1586        // |         |
1587        // +----4----+
1588        //      |
1589        //      5
1590
1591        graph.add_edge(1, 2, 1.0).unwrap();
1592        graph.add_edge(2, 3, 1.0).unwrap();
1593        graph.add_edge(3, 4, 1.0).unwrap();
1594        graph.add_edge(4, 1, 1.0).unwrap();
1595        graph.add_edge(4, 5, 1.0).unwrap();
1596
1597        let coefficients = clustering_coefficient(&graph).unwrap();
1598
1599        // Node 1 has neighbors 2 and 4, and they're not connected, so coefficient = 0/1 = 0
1600        assert_eq!(coefficients[&1], 0.0);
1601
1602        // Node 2 has neighbors 1 and 3, and they're not connected, so coefficient = 0/1 = 0
1603        assert_eq!(coefficients[&2], 0.0);
1604
1605        // Node 3 has neighbors 2 and 4, and they're not connected, so coefficient = 0/1 = 0
1606        assert_eq!(coefficients[&3], 0.0);
1607
1608        // Node 4 has neighbors 1, 3, and 5.
1609        // Neighbors 1 and 3 are not directly connected, and 5 is not connected to either 1 or 3
1610        // So coefficient = 0/3 = 0
1611        assert_eq!(coefficients[&4], 0.0);
1612
1613        // Node 5 has only one neighbor (4), so coefficient = 0
1614        assert_eq!(coefficients[&5], 0.0);
1615
1616        // Now let's add an edge to create a triangle
1617        graph.add_edge(1, 3, 1.0).unwrap();
1618
1619        let coefficients = clustering_coefficient(&graph).unwrap();
1620
1621        // Now nodes 1, 3, and 4 form a triangle
1622        // Node 4 has 3 neighbors (1, 3, 5) with 1 edge between them (1-3)
1623        // Possible edges: 3 choose 2 = 3
1624        // So coefficient = 1/3
1625        assert!((coefficients[&4] - 1.0 / 3.0).abs() < 1e-10);
1626    }
1627
1628    #[test]
1629    fn test_graph_density() {
1630        let mut graph: Graph<i32, f64> = Graph::new();
1631
1632        // Empty graph should error
1633        assert!(graph_density(&graph).is_err());
1634
1635        // Add one node
1636        graph.add_node(1);
1637
1638        // Graph with one node should error
1639        assert!(graph_density(&graph).is_err());
1640
1641        // Create a graph with 4 nodes and 3 edges
1642        graph.add_node(2);
1643        graph.add_node(3);
1644        graph.add_node(4);
1645
1646        graph.add_edge(1, 2, 1.0).unwrap();
1647        graph.add_edge(2, 3, 1.0).unwrap();
1648        graph.add_edge(3, 4, 1.0).unwrap();
1649
1650        // 3 edges out of 6 possible edges (4 choose 2 = 6)
1651        let density = graph_density(&graph).unwrap();
1652        assert_eq!(density, 0.5);
1653
1654        // Add 3 more edges to make a complete graph
1655        graph.add_edge(1, 3, 1.0).unwrap();
1656        graph.add_edge(1, 4, 1.0).unwrap();
1657        graph.add_edge(2, 4, 1.0).unwrap();
1658
1659        // 6 edges out of 6 possible edges
1660        let density = graph_density(&graph).unwrap();
1661        assert_eq!(density, 1.0);
1662    }
1663
1664    #[test]
1665    fn test_katz_centrality() {
1666        let mut graph: Graph<char, f64> = Graph::new();
1667
1668        // Create a simple star graph with A in the center
1669        graph.add_edge('A', 'B', 1.0).unwrap();
1670        graph.add_edge('A', 'C', 1.0).unwrap();
1671        graph.add_edge('A', 'D', 1.0).unwrap();
1672
1673        let centrality = katz_centrality(&graph, 0.1, 1.0).unwrap();
1674
1675        // The center node should have higher Katz centrality
1676        assert!(centrality[&'A'] > centrality[&'B']);
1677        assert!(centrality[&'A'] > centrality[&'C']);
1678        assert!(centrality[&'A'] > centrality[&'D']);
1679
1680        // All leaf nodes should have similar centrality
1681        assert!((centrality[&'B'] - centrality[&'C']).abs() < 0.1);
1682        assert!((centrality[&'B'] - centrality[&'D']).abs() < 0.1);
1683    }
1684
1685    #[test]
1686    fn test_pagerank_centrality() {
1687        let mut graph: Graph<char, f64> = Graph::new();
1688
1689        // Create a simple triangle
1690        graph.add_edge('A', 'B', 1.0).unwrap();
1691        graph.add_edge('B', 'C', 1.0).unwrap();
1692        graph.add_edge('C', 'A', 1.0).unwrap();
1693
1694        let centrality = pagerank_centrality(&graph, 0.85, 1e-6).unwrap();
1695
1696        // All nodes should have equal PageRank in a symmetric triangle
1697        let values: Vec<f64> = centrality.values().cloned().collect();
1698        let expected = 1.0 / 3.0; // Should sum to 1, so each gets 1/3
1699
1700        for &value in &values {
1701            assert!((value - expected).abs() < 0.1);
1702        }
1703
1704        // Check that PageRank values sum to approximately 1
1705        let sum: f64 = values.iter().sum();
1706        assert!((sum - 1.0).abs() < 0.01);
1707    }
1708
1709    #[test]
1710    fn test_pagerank_centrality_digraph() {
1711        let mut graph: DiGraph<char, f64> = DiGraph::new();
1712
1713        // Create a directed path: A -> B -> C
1714        graph.add_edge('A', 'B', 1.0).unwrap();
1715        graph.add_edge('B', 'C', 1.0).unwrap();
1716
1717        let centrality = pagerank_centrality_digraph(&graph, 0.85, 1e-6).unwrap();
1718
1719        // C should have the highest PageRank (receives links but doesn't give any)
1720        // A should have the lowest (gives links but doesn't receive any except random jumps)
1721        assert!(centrality[&'C'] > centrality[&'B']);
1722        assert!(centrality[&'B'] > centrality[&'A']);
1723    }
1724
1725    #[test]
1726    fn test_centrality_enum_katz_pagerank() {
1727        let mut graph: Graph<char, f64> = Graph::new();
1728
1729        graph.add_edge('A', 'B', 1.0).unwrap();
1730        graph.add_edge('A', 'C', 1.0).unwrap();
1731
1732        // Test that the enum-based centrality function works for new types
1733        let katz_result = centrality(&graph, CentralityType::Katz).unwrap();
1734        let pagerank_result = centrality(&graph, CentralityType::PageRank).unwrap();
1735
1736        // Both should return valid results
1737        assert_eq!(katz_result.len(), 3);
1738        assert_eq!(pagerank_result.len(), 3);
1739
1740        // All values should be positive
1741        for value in katz_result.values() {
1742            assert!(*value > 0.0);
1743        }
1744        for value in pagerank_result.values() {
1745            assert!(*value > 0.0);
1746        }
1747    }
1748
1749    #[test]
1750    fn test_hits_algorithm() {
1751        let mut graph: DiGraph<char, f64> = DiGraph::new();
1752
1753        // Create a small web graph
1754        // A and B are hubs (point to many pages)
1755        // C and D are authorities (pointed to by many pages)
1756        graph.add_edge('A', 'C', 1.0).unwrap();
1757        graph.add_edge('A', 'D', 1.0).unwrap();
1758        graph.add_edge('B', 'C', 1.0).unwrap();
1759        graph.add_edge('B', 'D', 1.0).unwrap();
1760        // E is both a hub and authority
1761        graph.add_edge('E', 'C', 1.0).unwrap();
1762        graph.add_edge('B', 'E', 1.0).unwrap();
1763
1764        let hits = hits_algorithm(&graph, 100, 1e-6).unwrap();
1765
1766        // Check that we have scores for all nodes
1767        assert_eq!(hits.authorities.len(), 5);
1768        assert_eq!(hits.hubs.len(), 5);
1769
1770        // C and D should have high authority scores
1771        assert!(hits.authorities[&'C'] > hits.authorities[&'A']);
1772        assert!(hits.authorities[&'D'] > hits.authorities[&'A']);
1773
1774        // A and B should have high hub scores
1775        assert!(hits.hubs[&'A'] > hits.hubs[&'C']);
1776        assert!(hits.hubs[&'B'] > hits.hubs[&'C']);
1777
1778        // Check that scores are normalized (sum of squares = 1)
1779        let auth_norm: f64 = hits.authorities.values().map(|&x| x * x).sum::<f64>();
1780        let hub_norm: f64 = hits.hubs.values().map(|&x| x * x).sum::<f64>();
1781        assert!((auth_norm - 1.0).abs() < 0.01);
1782        assert!((hub_norm - 1.0).abs() < 0.01);
1783    }
1784
1785    #[test]
1786    fn test_weighted_degree_centrality() {
1787        let mut graph = crate::generators::create_graph::<&str, f64>();
1788
1789        // Create a simple weighted graph
1790        graph.add_edge("A", "B", 2.0).unwrap();
1791        graph.add_edge("A", "C", 3.0).unwrap();
1792        graph.add_edge("B", "C", 1.0).unwrap();
1793
1794        let centrality = centrality(&graph, CentralityType::WeightedDegree).unwrap();
1795
1796        // A has edges with weights 2.0 + 3.0 = 5.0
1797        assert_eq!(centrality[&"A"], 5.0);
1798
1799        // B has edges with weights 2.0 + 1.0 = 3.0
1800        assert_eq!(centrality[&"B"], 3.0);
1801
1802        // C has edges with weights 3.0 + 1.0 = 4.0
1803        assert_eq!(centrality[&"C"], 4.0);
1804    }
1805
1806    #[test]
1807    fn test_weighted_closeness_centrality() {
1808        let mut graph = crate::generators::create_graph::<&str, f64>();
1809
1810        // Create a simple weighted graph
1811        graph.add_edge("A", "B", 1.0).unwrap();
1812        graph.add_edge("B", "C", 2.0).unwrap();
1813
1814        let centrality = centrality(&graph, CentralityType::WeightedCloseness).unwrap();
1815
1816        // All centrality values should be positive
1817        for value in centrality.values() {
1818            assert!(*value > 0.0);
1819        }
1820
1821        // Node B should have highest closeness (shortest paths to others)
1822        assert!(centrality[&"B"] > centrality[&"A"]);
1823        assert!(centrality[&"B"] > centrality[&"C"]);
1824    }
1825
1826    #[test]
1827    fn test_weighted_betweenness_centrality() {
1828        let mut graph = crate::generators::create_graph::<&str, f64>();
1829
1830        // Create a path graph A-B-C
1831        graph.add_edge("A", "B", 1.0).unwrap();
1832        graph.add_edge("B", "C", 1.0).unwrap();
1833
1834        let centrality = centrality(&graph, CentralityType::WeightedBetweenness).unwrap();
1835
1836        // B should have positive betweenness (lies on path from A to C)
1837        assert!(centrality[&"B"] > 0.0);
1838
1839        // A and C should have zero betweenness (no paths pass through them)
1840        assert_eq!(centrality[&"A"], 0.0);
1841        assert_eq!(centrality[&"C"], 0.0);
1842    }
1843
1844    #[test]
1845    fn test_weighted_centrality_digraph() {
1846        let mut graph = crate::generators::create_digraph::<&str, f64>();
1847
1848        // Create a simple directed weighted graph
1849        graph.add_edge("A", "B", 2.0).unwrap();
1850        graph.add_edge("B", "C", 3.0).unwrap();
1851        graph.add_edge("A", "C", 1.0).unwrap();
1852
1853        let degree_centrality = centrality_digraph(&graph, CentralityType::WeightedDegree).unwrap();
1854        let closeness_centrality =
1855            centrality_digraph(&graph, CentralityType::WeightedCloseness).unwrap();
1856
1857        // All centrality values should be non-negative
1858        for value in degree_centrality.values() {
1859            assert!(*value >= 0.0);
1860        }
1861        for value in closeness_centrality.values() {
1862            assert!(*value >= 0.0);
1863        }
1864
1865        // A has weighted degree 2.0 + 1.0 = 3.0 (outgoing only)
1866        // B has weighted degree 2.0 (incoming) + 3.0 (outgoing) = 5.0 total
1867        // C has weighted degree 3.0 + 1.0 = 4.0 (incoming only)
1868        // So B should have the highest total weighted degree
1869        assert!(degree_centrality[&"B"] > degree_centrality[&"A"]);
1870        assert!(degree_centrality[&"B"] > degree_centrality[&"C"]);
1871    }
1872}