aprender/graph/
mod.rs

1//! Graph construction and analysis with cache-optimized CSR representation.
2//!
3//! This module provides high-performance graph algorithms built on top of
4//! Compressed Sparse Row (CSR) format for maximum cache locality. Key features:
5//!
6//! - CSR representation (50-70% memory reduction vs HashMap)
7//! - Centrality measures (degree, betweenness, PageRank)
8//! - Parallel algorithms using Rayon
9//! - Numerical stability (Kahan summation in PageRank)
10//!
11//! # Examples
12//!
13//! ```
14//! use aprender::graph::Graph;
15//!
16//! let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
17//!
18//! let dc = g.degree_centrality();
19//! assert_eq!(dc.len(), 3);
20//! ```
21
22#[cfg(feature = "parallel")]
23use rayon::prelude::*;
24use std::collections::{HashMap, VecDeque};
25
26/// Graph node identifier (contiguous integers for cache efficiency).
27pub type NodeId = usize;
28
29/// Graph edge with optional weight.
30#[derive(Debug, Clone, PartialEq)]
31pub struct Edge {
32    pub source: NodeId,
33    pub target: NodeId,
34    pub weight: Option<f64>,
35}
36
37/// Graph structure using CSR (Compressed Sparse Row) for cache efficiency.
38///
39/// Memory layout inspired by Combinatorial BLAS (Buluc et al. 2009):
40/// - Adjacency stored as two flat vectors (CSR format)
41/// - Node labels stored separately (accessed rarely)
42/// - String→NodeId mapping via HashMap (build-time only)
43///
44/// # Performance
45/// - Memory: 50-70% reduction vs HashMap (no pointer overhead)
46/// - Cache misses: 3-5x fewer (sequential access pattern)
47/// - SIMD-friendly: Neighbor iteration can use vectorization
48#[derive(Debug)]
49pub struct Graph {
50    // CSR adjacency representation (cache-friendly)
51    row_ptr: Vec<usize>,      // Offset into col_indices (length = n_nodes + 1)
52    col_indices: Vec<NodeId>, // Flattened neighbor lists (length = n_edges)
53    edge_weights: Vec<f64>,   // Parallel to col_indices (empty if unweighted)
54
55    // Metadata (accessed less frequently)
56    #[allow(dead_code)]
57    node_labels: Vec<Option<String>>, // Indexed by NodeId
58    #[allow(dead_code)]
59    label_to_id: HashMap<String, NodeId>, // For label lookups
60
61    is_directed: bool,
62    n_nodes: usize,
63    n_edges: usize,
64}
65
66impl Graph {
67    /// Create empty graph.
68    ///
69    /// # Arguments
70    /// * `is_directed` - Whether the graph is directed
71    ///
72    /// # Examples
73    /// ```
74    /// use aprender::graph::Graph;
75    ///
76    /// let g = Graph::new(false); // undirected
77    /// assert_eq!(g.num_nodes(), 0);
78    /// ```
79    pub fn new(is_directed: bool) -> Self {
80        Self {
81            row_ptr: vec![0],
82            col_indices: Vec::new(),
83            edge_weights: Vec::new(),
84            node_labels: Vec::new(),
85            label_to_id: HashMap::new(),
86            is_directed,
87            n_nodes: 0,
88            n_edges: 0,
89        }
90    }
91
92    /// Get number of nodes in graph.
93    pub fn num_nodes(&self) -> usize {
94        self.n_nodes
95    }
96
97    /// Get number of edges in graph.
98    pub fn num_edges(&self) -> usize {
99        self.n_edges
100    }
101
102    /// Check if graph is directed.
103    pub fn is_directed(&self) -> bool {
104        self.is_directed
105    }
106
107    /// Get neighbors of node v in O(degree(v)) time with perfect cache locality.
108    ///
109    /// # Arguments
110    /// * `v` - Node ID
111    ///
112    /// # Returns
113    /// Slice of neighbor node IDs
114    ///
115    /// # Examples
116    /// ```
117    /// use aprender::graph::Graph;
118    ///
119    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
120    /// assert_eq!(g.neighbors(1), &[0, 2]);
121    /// ```
122    pub fn neighbors(&self, v: NodeId) -> &[NodeId] {
123        if v >= self.n_nodes {
124            return &[];
125        }
126        let start = self.row_ptr[v];
127        let end = self.row_ptr[v + 1];
128        &self.col_indices[start..end]
129    }
130
131    /// Build graph from edge list.
132    ///
133    /// This is the primary construction method. Automatically detects
134    /// the number of nodes from the edge list and builds CSR representation.
135    ///
136    /// # Arguments
137    /// * `edges` - Slice of (source, target) tuples
138    /// * `is_directed` - Whether the graph is directed
139    ///
140    /// # Examples
141    /// ```
142    /// use aprender::graph::Graph;
143    ///
144    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
145    /// assert_eq!(g.num_nodes(), 3);
146    /// assert_eq!(g.num_edges(), 3);
147    /// ```
148    pub fn from_edges(edges: &[(NodeId, NodeId)], is_directed: bool) -> Self {
149        if edges.is_empty() {
150            return Self::new(is_directed);
151        }
152
153        // Find max node ID to determine number of nodes
154        let max_node = edges.iter().flat_map(|&(s, t)| [s, t]).max().unwrap_or(0);
155        let n_nodes = max_node + 1;
156
157        // Build adjacency list first (for sorting and deduplication)
158        let mut adj_list: Vec<Vec<NodeId>> = vec![Vec::new(); n_nodes];
159        for &(source, target) in edges {
160            adj_list[source].push(target);
161            if !is_directed && source != target {
162                // For undirected graphs, add reverse edge
163                adj_list[target].push(source);
164            }
165        }
166
167        // Sort and deduplicate neighbors for each node
168        for neighbors in &mut adj_list {
169            neighbors.sort_unstable();
170            neighbors.dedup();
171        }
172
173        // Build CSR representation
174        let mut row_ptr = Vec::with_capacity(n_nodes + 1);
175        let mut col_indices = Vec::new();
176
177        row_ptr.push(0);
178        for neighbors in &adj_list {
179            col_indices.extend_from_slice(neighbors);
180            row_ptr.push(col_indices.len());
181        }
182
183        let n_edges = if is_directed {
184            edges.len()
185        } else {
186            // For undirected, each edge is counted once in input
187            edges.len()
188        };
189
190        Self {
191            row_ptr,
192            col_indices,
193            edge_weights: Vec::new(),
194            node_labels: vec![None; n_nodes],
195            label_to_id: HashMap::new(),
196            is_directed,
197            n_nodes,
198            n_edges,
199        }
200    }
201
202    /// Build weighted graph from edge list with weights.
203    ///
204    /// # Arguments
205    /// * `edges` - Slice of (source, target, weight) tuples
206    /// * `is_directed` - Whether the graph is directed
207    ///
208    /// # Examples
209    /// ```
210    /// use aprender::graph::Graph;
211    ///
212    /// let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.5)], false);
213    /// assert_eq!(g.num_nodes(), 3);
214    /// assert_eq!(g.num_edges(), 2);
215    /// ```
216    pub fn from_weighted_edges(edges: &[(NodeId, NodeId, f64)], is_directed: bool) -> Self {
217        if edges.is_empty() {
218            return Self::new(is_directed);
219        }
220
221        // Find max node ID
222        let max_node = edges
223            .iter()
224            .flat_map(|&(s, t, _)| [s, t])
225            .max()
226            .unwrap_or(0);
227        let n_nodes = max_node + 1;
228
229        // Build adjacency list with weights
230        let mut adj_list: Vec<Vec<(NodeId, f64)>> = vec![Vec::new(); n_nodes];
231        for &(source, target, weight) in edges {
232            adj_list[source].push((target, weight));
233            if !is_directed && source != target {
234                adj_list[target].push((source, weight));
235            }
236        }
237
238        // Sort and deduplicate (keep first weight for duplicates)
239        for neighbors in &mut adj_list {
240            neighbors.sort_unstable_by_key(|&(id, _)| id);
241            neighbors.dedup_by_key(|&mut (id, _)| id);
242        }
243
244        // Build CSR representation
245        let mut row_ptr = Vec::with_capacity(n_nodes + 1);
246        let mut col_indices = Vec::new();
247        let mut edge_weights = Vec::new();
248
249        row_ptr.push(0);
250        for neighbors in &adj_list {
251            for &(neighbor, weight) in neighbors {
252                col_indices.push(neighbor);
253                edge_weights.push(weight);
254            }
255            row_ptr.push(col_indices.len());
256        }
257
258        let n_edges = edges.len();
259
260        Self {
261            row_ptr,
262            col_indices,
263            edge_weights,
264            node_labels: vec![None; n_nodes],
265            label_to_id: HashMap::new(),
266            is_directed,
267            n_nodes,
268            n_edges,
269        }
270    }
271
272    /// Get edge weight between two nodes.
273    ///
274    /// # Returns
275    /// * `Some(weight)` if edge exists
276    /// * `None` if no edge exists
277    #[allow(dead_code)]
278    fn edge_weight(&self, source: NodeId, target: NodeId) -> Option<f64> {
279        if source >= self.n_nodes {
280            return None;
281        }
282
283        let start = self.row_ptr[source];
284        let end = self.row_ptr[source + 1];
285        let neighbors = &self.col_indices[start..end];
286
287        // Binary search for target
288        let pos = neighbors.binary_search(&target).ok()?;
289
290        if self.edge_weights.is_empty() {
291            Some(1.0) // Unweighted graph
292        } else {
293            Some(self.edge_weights[start + pos])
294        }
295    }
296
297    /// Compute degree centrality for all nodes.
298    ///
299    /// Uses Freeman's normalization (1978): C_D(v) = deg(v) / (n - 1)
300    ///
301    /// # Returns
302    /// HashMap mapping NodeId to centrality score in [0, 1]
303    ///
304    /// # Performance
305    /// O(n + m) where n = nodes, m = edges
306    ///
307    /// # Examples
308    /// ```
309    /// use aprender::graph::Graph;
310    ///
311    /// // Star graph: center has degree 3, leaves have degree 1
312    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
313    /// let dc = g.degree_centrality();
314    ///
315    /// assert_eq!(dc[&0], 1.0); // center connected to all others
316    /// assert!((dc[&1] - 0.333).abs() < 0.01); // leaves connected to 1 of 3
317    /// ```
318    pub fn degree_centrality(&self) -> HashMap<NodeId, f64> {
319        let mut centrality = HashMap::with_capacity(self.n_nodes);
320
321        if self.n_nodes <= 1 {
322            // Single node or empty graph
323            for v in 0..self.n_nodes {
324                centrality.insert(v, 0.0);
325            }
326            return centrality;
327        }
328
329        let norm = (self.n_nodes - 1) as f64;
330
331        #[allow(clippy::needless_range_loop)]
332        for v in 0..self.n_nodes {
333            let degree = self.neighbors(v).len() as f64;
334            centrality.insert(v, degree / norm);
335        }
336
337        centrality
338    }
339
340    /// Compute PageRank using power iteration with Kahan summation.
341    ///
342    /// Uses the PageRank algorithm (Page et al. 1999) with numerically
343    /// stable Kahan summation (Higham 1993) to prevent floating-point
344    /// drift in large graphs (>10K nodes).
345    ///
346    /// # Arguments
347    /// * `damping` - Damping factor (typically 0.85)
348    /// * `max_iter` - Maximum iterations (default 100)
349    /// * `tol` - Convergence tolerance (default 1e-6)
350    ///
351    /// # Returns
352    /// Vector of PageRank scores (one per node)
353    ///
354    /// # Performance
355    /// O(k * m) where k = iterations, m = edges
356    ///
357    /// # Examples
358    /// ```
359    /// use aprender::graph::Graph;
360    ///
361    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
362    /// let pr = g.pagerank(0.85, 100, 1e-6).expect("pagerank should converge for valid graph");
363    /// assert!((pr.iter().sum::<f64>() - 1.0).abs() < 1e-10); // Kahan ensures precision
364    /// ```
365    pub fn pagerank(&self, damping: f64, max_iter: usize, tol: f64) -> Result<Vec<f64>, String> {
366        if self.n_nodes == 0 {
367            return Ok(Vec::new());
368        }
369
370        let n = self.n_nodes;
371        let mut ranks = vec![1.0 / n as f64; n];
372        let mut new_ranks = vec![0.0; n];
373
374        for _ in 0..max_iter {
375            // Handle dangling nodes (nodes with no outgoing edges)
376            // Redistribute their rank uniformly to all nodes
377            let mut dangling_sum = 0.0;
378            #[allow(clippy::needless_range_loop)]
379            for v in 0..n {
380                if self.neighbors(v).is_empty() {
381                    dangling_sum += ranks[v];
382                }
383            }
384            let dangling_contribution = damping * dangling_sum / n as f64;
385
386            // Compute new ranks with Kahan summation
387            #[allow(clippy::needless_range_loop)]
388            for v in 0..n {
389                let incoming_neighbors = self.incoming_neighbors(v);
390
391                let mut sum = 0.0;
392                let mut c = 0.0; // Kahan compensation term
393
394                for u in &incoming_neighbors {
395                    let out_degree = self.neighbors(*u).len() as f64;
396                    if out_degree > 0.0 {
397                        let y = (ranks[*u] / out_degree) - c;
398                        let t = sum + y;
399                        c = (t - sum) - y; // Recover low-order bits
400                        sum = t;
401                    }
402                }
403
404                new_ranks[v] = (1.0 - damping) / n as f64 + damping * sum + dangling_contribution;
405            }
406
407            // Convergence check using Kahan for diff calculation
408            let diff = kahan_diff(&ranks, &new_ranks);
409            if diff < tol {
410                return Ok(new_ranks);
411            }
412
413            std::mem::swap(&mut ranks, &mut new_ranks);
414        }
415
416        Ok(ranks)
417    }
418
419    /// Get incoming neighbors for directed graphs (reverse edges).
420    ///
421    /// For undirected graphs, this is the same as neighbors().
422    /// For directed graphs, we need to scan all nodes to find incoming edges.
423    fn incoming_neighbors(&self, v: NodeId) -> Vec<NodeId> {
424        if !self.is_directed {
425            // For undirected graphs, incoming == outgoing
426            return self.neighbors(v).to_vec();
427        }
428
429        // For directed graphs, scan all nodes to find incoming edges
430        let mut incoming = Vec::new();
431        for u in 0..self.n_nodes {
432            if self.neighbors(u).contains(&v) {
433                incoming.push(u);
434            }
435        }
436        incoming
437    }
438
439    /// Compute betweenness centrality using parallel Brandes' algorithm.
440    ///
441    /// Uses Brandes' algorithm (2001) with Rayon parallelization for the outer loop.
442    /// Each source's BFS is independent, making this "embarrassingly parallel" (Bader & Madduri 2006).
443    ///
444    /// # Performance
445    /// - Serial: O(nm) for unweighted graphs
446    /// - Parallel: O(nm/p) where p = number of CPU cores
447    /// - Expected speedup: ~8x on 8-core CPU for graphs with >1K nodes
448    ///
449    /// # Returns
450    /// Vector of betweenness centrality scores (one per node)
451    ///
452    /// # Examples
453    /// ```
454    /// use aprender::graph::Graph;
455    ///
456    /// // Path graph: 0 -- 1 -- 2
457    /// // Middle node has higher betweenness
458    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
459    /// let bc = g.betweenness_centrality();
460    /// assert!(bc[1] > bc[0]); // middle node has highest betweenness
461    /// assert!(bc[1] > bc[2]);
462    /// ```
463    pub fn betweenness_centrality(&self) -> Vec<f64> {
464        if self.n_nodes == 0 {
465            return Vec::new();
466        }
467
468        // Compute partial betweenness from each source (parallel when available)
469        #[cfg(feature = "parallel")]
470        let partial_scores: Vec<Vec<f64>> = (0..self.n_nodes)
471            .into_par_iter()
472            .map(|source| self.brandes_bfs_from_source(source))
473            .collect();
474
475        #[cfg(not(feature = "parallel"))]
476        let partial_scores: Vec<Vec<f64>> = (0..self.n_nodes)
477            .map(|source| self.brandes_bfs_from_source(source))
478            .collect();
479
480        // Reduce partial scores (single-threaded, but fast)
481        let mut centrality = vec![0.0; self.n_nodes];
482        for partial in partial_scores {
483            for (i, &score) in partial.iter().enumerate() {
484                centrality[i] += score;
485            }
486        }
487
488        // Normalize for undirected graphs
489        if !self.is_directed {
490            for score in &mut centrality {
491                *score /= 2.0;
492            }
493        }
494
495        centrality
496    }
497
498    /// Brandes' BFS from a single source node.
499    ///
500    /// Computes the contribution to betweenness centrality from paths
501    /// starting at the given source node.
502    fn brandes_bfs_from_source(&self, source: NodeId) -> Vec<f64> {
503        let n = self.n_nodes;
504        let mut stack = Vec::new(); // Nodes in order of non-increasing distance
505        let mut paths = vec![0u64; n]; // Number of shortest paths from source to each node
506        let mut distance = vec![i32::MAX; n]; // Distance from source
507        let mut predecessors: Vec<Vec<NodeId>> = vec![Vec::new(); n]; // Predecessors on shortest paths
508        let mut dependency = vec![0.0; n]; // Dependency of source on each node
509
510        // BFS initialization
511        paths[source] = 1;
512        distance[source] = 0;
513        let mut queue = VecDeque::new();
514        queue.push_back(source);
515
516        // BFS to compute shortest paths
517        while let Some(v) = queue.pop_front() {
518            stack.push(v);
519            for &w in self.neighbors(v) {
520                // First time we see w?
521                if distance[w] == i32::MAX {
522                    distance[w] = distance[v] + 1;
523                    queue.push_back(w);
524                }
525                // Shortest path to w via v?
526                if distance[w] == distance[v] + 1 {
527                    paths[w] = paths[w].saturating_add(paths[v]);
528                    predecessors[w].push(v);
529                }
530            }
531        }
532
533        // Backward accumulation of dependencies
534        while let Some(w) = stack.pop() {
535            for &v in &predecessors[w] {
536                let coeff = (paths[v] as f64 / paths[w] as f64) * (1.0 + dependency[w]);
537                dependency[v] += coeff;
538            }
539        }
540
541        dependency
542    }
543
544    /// Compute modularity of a community partition.
545    ///
546    /// Modularity Q measures the density of edges within communities compared to
547    /// a random graph. Ranges from -0.5 to 1.0 (higher is better).
548    ///
549    /// Formula: Q = (1/2m) Σ[A_ij - k_i*k_j/2m] δ(c_i, c_j)
550    /// where:
551    /// - m = total edges
552    /// - A_ij = adjacency matrix
553    /// - k_i = degree of node i
554    /// - δ(c_i, c_j) = 1 if nodes i,j in same community, 0 otherwise
555    ///
556    /// # Arguments
557    /// * `communities` - Vector of communities, each community is a vector of node IDs
558    ///
559    /// # Returns
560    /// Modularity score Q ∈ [-0.5, 1.0]
561    pub fn modularity(&self, communities: &[Vec<NodeId>]) -> f64 {
562        if self.n_nodes == 0 || communities.is_empty() {
563            return 0.0;
564        }
565
566        // Build community membership map
567        let mut community_map = vec![None; self.n_nodes];
568        for (comm_id, community) in communities.iter().enumerate() {
569            for &node in community {
570                community_map[node] = Some(comm_id);
571            }
572        }
573
574        // Total edges
575        let m = self.n_edges as f64;
576
577        if m == 0.0 {
578            return 0.0;
579        }
580
581        let two_m = 2.0 * m;
582
583        // Compute degrees
584        let degrees: Vec<f64> = (0..self.n_nodes)
585            .map(|i| self.neighbors(i).len() as f64)
586            .collect();
587
588        // Compute modularity: Q = (1/2m) Σ[A_ij - k_i*k_j/2m] δ(c_i, c_j)
589        let mut q = 0.0;
590
591        for i in 0..self.n_nodes {
592            for j in 0..self.n_nodes {
593                // Skip if nodes not in same community
594                match (community_map[i], community_map[j]) {
595                    (Some(ci), Some(cj)) if ci == cj => {
596                        // Check if edge exists
597                        let a_ij = if self.neighbors(i).contains(&j) {
598                            1.0
599                        } else {
600                            0.0
601                        };
602
603                        // Expected edges in random graph
604                        let expected = (degrees[i] * degrees[j]) / two_m;
605
606                        q += a_ij - expected;
607                    }
608                    _ => {}
609                }
610            }
611        }
612
613        q / two_m
614    }
615
616    /// Detect communities using the Louvain algorithm.
617    ///
618    /// The Louvain method is a greedy modularity optimization algorithm that:
619    /// 1. Starts with each node in its own community
620    /// 2. Iteratively moves nodes to communities that maximize modularity gain
621    /// 3. Aggregates the graph by treating communities as super-nodes
622    /// 4. Repeats until modularity converges
623    ///
624    /// # Performance
625    /// - Time: O(m·log n) typical, where m = edges, n = nodes
626    /// - Space: O(n + m)
627    ///
628    /// # Returns
629    /// Vector of communities, each community is a vector of node IDs
630    ///
631    /// # Examples
632    /// ```
633    /// use aprender::graph::Graph;
634    ///
635    /// // Two triangles connected by one edge
636    /// let g = Graph::from_edges(&[
637    ///     (0, 1), (1, 2), (2, 0),  // Triangle 1
638    ///     (3, 4), (4, 5), (5, 3),  // Triangle 2
639    ///     (2, 3),                   // Connection
640    /// ], false);
641    ///
642    /// let communities = g.louvain();
643    /// assert_eq!(communities.len(), 2);  // Two communities detected
644    /// ```
645    pub fn louvain(&self) -> Vec<Vec<NodeId>> {
646        if self.n_nodes == 0 {
647            return Vec::new();
648        }
649
650        // Initialize: each node in its own community
651        let mut node_to_comm: Vec<usize> = (0..self.n_nodes).collect();
652        let mut improved = true;
653        let mut iteration = 0;
654        let max_iterations = 100;
655
656        // Phase 1: Iteratively move nodes to communities
657        while improved && iteration < max_iterations {
658            improved = false;
659            iteration += 1;
660
661            for node in 0..self.n_nodes {
662                let current_comm = node_to_comm[node];
663                let neighbors = self.neighbors(node);
664
665                if neighbors.is_empty() {
666                    continue;
667                }
668
669                // Find best community to move to
670                let mut best_comm = current_comm;
671                let mut best_gain = 0.0;
672
673                // Try moving to each neighbor's community
674                let mut tried_comms = std::collections::HashSet::new();
675                for &neighbor in neighbors {
676                    let neighbor_comm = node_to_comm[neighbor];
677
678                    if tried_comms.contains(&neighbor_comm) {
679                        continue;
680                    }
681                    tried_comms.insert(neighbor_comm);
682
683                    // Calculate modularity gain
684                    let gain =
685                        self.modularity_gain(node, current_comm, neighbor_comm, &node_to_comm);
686
687                    if gain > best_gain {
688                        best_gain = gain;
689                        best_comm = neighbor_comm;
690                    }
691                }
692
693                // Move node if improves modularity
694                if best_comm != current_comm && best_gain > 1e-10 {
695                    node_to_comm[node] = best_comm;
696                    improved = true;
697                }
698            }
699        }
700
701        // Convert node_to_comm map to community lists
702        let mut communities: HashMap<usize, Vec<NodeId>> = HashMap::new();
703
704        for (node, &comm) in node_to_comm.iter().enumerate() {
705            communities.entry(comm).or_default().push(node);
706        }
707
708        communities.into_values().collect()
709    }
710
711    /// Calculate modularity gain from moving a node to a different community.
712    fn modularity_gain(
713        &self,
714        node: NodeId,
715        from_comm: usize,
716        to_comm: usize,
717        node_to_comm: &[usize],
718    ) -> f64 {
719        if from_comm == to_comm {
720            return 0.0;
721        }
722
723        let m = self.n_edges as f64;
724        if m == 0.0 {
725            return 0.0;
726        }
727
728        let two_m = 2.0 * m;
729        let k_i = self.neighbors(node).len() as f64;
730
731        // Count edges from node to target community
732        let mut k_i_to = 0.0;
733        for &neighbor in self.neighbors(node) {
734            if node_to_comm[neighbor] == to_comm {
735                k_i_to += 1.0;
736            }
737        }
738
739        // Count edges from node to current community (excluding self)
740        let mut k_i_from = 0.0;
741        for &neighbor in self.neighbors(node) {
742            if node_to_comm[neighbor] == from_comm && neighbor != node {
743                k_i_from += 1.0;
744            }
745        }
746
747        // Total degree of target community (excluding node)
748        let sigma_tot_to: f64 = node_to_comm
749            .iter()
750            .enumerate()
751            .filter(|&(n, &comm)| comm == to_comm && n != node)
752            .map(|(n, _)| self.neighbors(n).len() as f64)
753            .sum();
754
755        // Total degree of current community (excluding node)
756        let sigma_tot_from: f64 = node_to_comm
757            .iter()
758            .enumerate()
759            .filter(|&(n, &comm)| comm == from_comm && n != node)
760            .map(|(n, _)| self.neighbors(n).len() as f64)
761            .sum();
762
763        // Modularity gain formula
764        (k_i_to - k_i_from) / m - k_i * (sigma_tot_to - sigma_tot_from) / (two_m * m)
765    }
766
767    /// Compute closeness centrality for all nodes.
768    ///
769    /// Closeness measures how close a node is to all other nodes in the graph.
770    /// Uses Wasserman & Faust (1994) normalization: C_C(v) = (n-1) / Σd(v,u)
771    ///
772    /// # Returns
773    /// Vector of closeness centrality scores (one per node)
774    /// For disconnected graphs, nodes unreachable from v are ignored in the sum.
775    ///
776    /// # Performance
777    /// O(n·(n + m)) where n = nodes, m = edges (BFS from each node)
778    ///
779    /// # Examples
780    /// ```
781    /// use aprender::graph::Graph;
782    ///
783    /// // Star graph: center is close to all nodes
784    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
785    /// let cc = g.closeness_centrality();
786    /// assert!(cc[0] > cc[1]); // center has highest closeness
787    /// ```
788    pub fn closeness_centrality(&self) -> Vec<f64> {
789        if self.n_nodes == 0 {
790            return Vec::new();
791        }
792
793        let mut centrality = vec![0.0; self.n_nodes];
794
795        #[allow(clippy::needless_range_loop)]
796        for v in 0..self.n_nodes {
797            let distances = self.bfs_distances(v);
798
799            // Sum of distances to all reachable nodes
800            let sum: usize = distances.iter().filter(|&&d| d != usize::MAX).sum();
801            let reachable = distances
802                .iter()
803                .filter(|&&d| d != usize::MAX && d > 0)
804                .count();
805
806            if reachable > 0 && sum > 0 {
807                // Normalized closeness: (n-1) / sum_of_distances
808                centrality[v] = reachable as f64 / sum as f64;
809            }
810        }
811
812        centrality
813    }
814
815    /// BFS to compute shortest path distances from a source node.
816    fn bfs_distances(&self, source: NodeId) -> Vec<usize> {
817        let mut distances = vec![usize::MAX; self.n_nodes];
818        distances[source] = 0;
819
820        let mut queue = VecDeque::new();
821        queue.push_back(source);
822
823        while let Some(v) = queue.pop_front() {
824            for &w in self.neighbors(v) {
825                if distances[w] == usize::MAX {
826                    distances[w] = distances[v] + 1;
827                    queue.push_back(w);
828                }
829            }
830        }
831
832        distances
833    }
834
835    /// Compute eigenvector centrality using power iteration.
836    ///
837    /// Eigenvector centrality measures node importance based on the importance
838    /// of its neighbors. Uses the dominant eigenvector of the adjacency matrix.
839    ///
840    /// # Arguments
841    /// * `max_iter` - Maximum power iterations (default 100)
842    /// * `tol` - Convergence tolerance (default 1e-6)
843    ///
844    /// # Returns
845    /// Vector of eigenvector centrality scores (one per node)
846    ///
847    /// # Performance
848    /// O(k·m) where k = iterations, m = edges
849    ///
850    /// # Examples
851    /// ```
852    /// use aprender::graph::Graph;
853    ///
854    /// // Path graph: middle nodes have higher eigenvector centrality
855    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
856    /// let ec = g.eigenvector_centrality(100, 1e-6).unwrap();
857    /// assert!(ec[1] > ec[0]); // middle nodes more central
858    /// ```
859    pub fn eigenvector_centrality(&self, max_iter: usize, tol: f64) -> Result<Vec<f64>, String> {
860        if self.n_nodes == 0 {
861            return Ok(Vec::new());
862        }
863
864        let n = self.n_nodes;
865        let mut x = vec![1.0 / (n as f64).sqrt(); n]; // Initial uniform vector
866        let mut x_new = vec![0.0; n];
867
868        for _ in 0..max_iter {
869            // Matrix-vector multiplication: x_new = A * x
870            #[allow(clippy::needless_range_loop)]
871            for v in 0..n {
872                x_new[v] = self.neighbors(v).iter().map(|&u| x[u]).sum();
873            }
874
875            // Normalize to unit vector (L2 norm)
876            let norm: f64 = x_new.iter().map(|&val| val * val).sum::<f64>().sqrt();
877
878            if norm < 1e-10 {
879                // Disconnected graph or no edges
880                return Ok(vec![0.0; n]);
881            }
882
883            for val in &mut x_new {
884                *val /= norm;
885            }
886
887            // Check convergence
888            let diff: f64 = x.iter().zip(&x_new).map(|(a, b)| (a - b).abs()).sum();
889
890            if diff < tol {
891                return Ok(x_new);
892            }
893
894            std::mem::swap(&mut x, &mut x_new);
895        }
896
897        Ok(x)
898    }
899
900    /// Compute Katz centrality with attenuation factor.
901    ///
902    /// Katz centrality generalizes eigenvector centrality by adding an attenuation
903    /// factor for long-range connections: C_K = Σ(α^k · A^k · 1)
904    ///
905    /// # Arguments
906    /// * `alpha` - Attenuation factor (typically 0.1-0.5, must be < 1/λ_max)
907    /// * `max_iter` - Maximum iterations (default 100)
908    /// * `tol` - Convergence tolerance (default 1e-6)
909    ///
910    /// # Returns
911    /// Vector of Katz centrality scores
912    ///
913    /// # Performance
914    /// O(k·m) where k = iterations, m = edges
915    ///
916    /// # Examples
917    /// ```
918    /// use aprender::graph::Graph;
919    ///
920    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
921    /// let kc = g.katz_centrality(0.1, 100, 1e-6).unwrap();
922    /// assert_eq!(kc.len(), 3);
923    /// ```
924    pub fn katz_centrality(
925        &self,
926        alpha: f64,
927        max_iter: usize,
928        tol: f64,
929    ) -> Result<Vec<f64>, String> {
930        if self.n_nodes == 0 {
931            return Ok(Vec::new());
932        }
933
934        if alpha <= 0.0 || alpha >= 1.0 {
935            return Err("Alpha must be in (0, 1)".to_string());
936        }
937
938        let n = self.n_nodes;
939        let mut x = vec![1.0; n]; // Initial vector of ones
940        let mut x_new = vec![0.0; n];
941
942        for _ in 0..max_iter {
943            // Katz iteration: x_new = β + α·A^T·x (where β = 1)
944            // Use incoming neighbors (transpose adjacency matrix)
945            #[allow(clippy::needless_range_loop)]
946            for v in 0..n {
947                let incoming = self.incoming_neighbors(v);
948                let neighbors_sum: f64 = incoming.iter().map(|&u| x[u]).sum();
949                x_new[v] = 1.0 + alpha * neighbors_sum;
950            }
951
952            // Check convergence
953            let diff: f64 = x.iter().zip(&x_new).map(|(a, b)| (a - b).abs()).sum();
954
955            if diff < tol {
956                return Ok(x_new);
957            }
958
959            std::mem::swap(&mut x, &mut x_new);
960        }
961
962        Ok(x)
963    }
964
965    /// Compute harmonic centrality for all nodes.
966    ///
967    /// Harmonic centrality is the sum of reciprocal distances to all other nodes.
968    /// More robust than closeness for disconnected graphs (Boldi & Vigna 2014).
969    ///
970    /// Formula: C_H(v) = Σ(1/d(v,u)) for all u ≠ v
971    ///
972    /// # Returns
973    /// Vector of harmonic centrality scores
974    ///
975    /// # Performance
976    /// O(n·(n + m)) where n = nodes, m = edges
977    ///
978    /// # Examples
979    /// ```
980    /// use aprender::graph::Graph;
981    ///
982    /// // Star graph: center has highest harmonic centrality
983    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
984    /// let hc = g.harmonic_centrality();
985    /// assert!(hc[0] > hc[1]); // center most central
986    /// ```
987    pub fn harmonic_centrality(&self) -> Vec<f64> {
988        if self.n_nodes == 0 {
989            return Vec::new();
990        }
991
992        let mut centrality = vec![0.0; self.n_nodes];
993
994        #[allow(clippy::needless_range_loop)]
995        for v in 0..self.n_nodes {
996            let distances = self.bfs_distances(v);
997
998            // Sum reciprocals of distances (skip unreachable nodes)
999            for &dist in &distances {
1000                if dist > 0 && dist != usize::MAX {
1001                    centrality[v] += 1.0 / dist as f64;
1002                }
1003            }
1004        }
1005
1006        centrality
1007    }
1008
1009    /// Compute graph density.
1010    ///
1011    /// Density is the ratio of actual edges to possible edges.
1012    /// For undirected: d = 2m / (n(n-1))
1013    /// For directed: d = m / (n(n-1))
1014    ///
1015    /// # Returns
1016    /// Density in [0, 1] where 1 is a complete graph
1017    ///
1018    /// # Examples
1019    /// ```
1020    /// use aprender::graph::Graph;
1021    ///
1022    /// // Complete graph K4 has density 1.0
1023    /// let g = Graph::from_edges(&[(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)], false);
1024    /// assert!((g.density() - 1.0).abs() < 1e-6);
1025    /// ```
1026    pub fn density(&self) -> f64 {
1027        if self.n_nodes <= 1 {
1028            return 0.0;
1029        }
1030
1031        let n = self.n_nodes as f64;
1032        let m = self.n_edges as f64;
1033        let possible = n * (n - 1.0);
1034
1035        if self.is_directed {
1036            m / possible
1037        } else {
1038            (2.0 * m) / possible
1039        }
1040    }
1041
1042    /// Compute graph diameter (longest shortest path).
1043    ///
1044    /// Returns None if graph is disconnected.
1045    ///
1046    /// # Returns
1047    /// Some(diameter) if connected, None otherwise
1048    ///
1049    /// # Performance
1050    /// O(n·(n + m)) - runs BFS from all nodes
1051    ///
1052    /// # Examples
1053    /// ```
1054    /// use aprender::graph::Graph;
1055    ///
1056    /// // Path graph 0--1--2--3 has diameter 3
1057    /// let g = Graph::from_edges(&[(0,1), (1,2), (2,3)], false);
1058    /// assert_eq!(g.diameter(), Some(3));
1059    /// ```
1060    pub fn diameter(&self) -> Option<usize> {
1061        if self.n_nodes == 0 {
1062            return None;
1063        }
1064
1065        let mut max_dist = 0;
1066
1067        for v in 0..self.n_nodes {
1068            let distances = self.bfs_distances(v);
1069
1070            for &dist in &distances {
1071                if dist == usize::MAX {
1072                    // Disconnected graph
1073                    return None;
1074                }
1075                if dist > max_dist {
1076                    max_dist = dist;
1077                }
1078            }
1079        }
1080
1081        Some(max_dist)
1082    }
1083
1084    /// Compute global clustering coefficient.
1085    ///
1086    /// Measures the probability that two neighbors of a node are connected.
1087    /// Formula: C = 3 × triangles / triads
1088    ///
1089    /// # Returns
1090    /// Clustering coefficient in [0, 1]
1091    ///
1092    /// # Performance
1093    /// O(n·d²) where d = average degree
1094    ///
1095    /// # Examples
1096    /// ```
1097    /// use aprender::graph::Graph;
1098    ///
1099    /// // Triangle has clustering coefficient 1.0
1100    /// let g = Graph::from_edges(&[(0,1), (1,2), (2,0)], false);
1101    /// assert!((g.clustering_coefficient() - 1.0).abs() < 1e-6);
1102    /// ```
1103    #[allow(clippy::cast_lossless)]
1104    pub fn clustering_coefficient(&self) -> f64 {
1105        if self.n_nodes == 0 {
1106            return 0.0;
1107        }
1108
1109        let mut triangles = 0;
1110        let mut triads = 0;
1111
1112        for v in 0..self.n_nodes {
1113            let neighbors = self.neighbors(v);
1114            let deg = neighbors.len();
1115
1116            if deg < 2 {
1117                continue;
1118            }
1119
1120            // Count triads (pairs of neighbors)
1121            triads += deg * (deg - 1) / 2;
1122
1123            // Count triangles (connected pairs of neighbors)
1124            for i in 0..neighbors.len() {
1125                for j in (i + 1)..neighbors.len() {
1126                    let u = neighbors[i];
1127                    let w = neighbors[j];
1128
1129                    // Check if u and w are connected
1130                    if self.neighbors(u).contains(&w) {
1131                        triangles += 1;
1132                    }
1133                }
1134            }
1135        }
1136
1137        if triads == 0 {
1138            return 0.0;
1139        }
1140
1141        // Each triangle is counted 3 times (once from each vertex)
1142        // So we divide by 3 to get actual triangle count
1143        (triangles as f64) / (triads as f64)
1144    }
1145
1146    /// Compute degree assortativity coefficient.
1147    ///
1148    /// Measures correlation between degrees of connected nodes.
1149    /// Positive: high-degree nodes connect to high-degree nodes
1150    /// Negative: high-degree nodes connect to low-degree nodes
1151    ///
1152    /// # Returns
1153    /// Assortativity coefficient in [-1, 1]
1154    ///
1155    /// # Performance
1156    /// O(m) where m = edges
1157    ///
1158    /// # Examples
1159    /// ```
1160    /// use aprender::graph::Graph;
1161    ///
1162    /// // Star graph has negative assortativity
1163    /// let g = Graph::from_edges(&[(0,1), (0,2), (0,3)], false);
1164    /// assert!(g.assortativity() < 0.0);
1165    /// ```
1166    pub fn assortativity(&self) -> f64 {
1167        if self.n_edges == 0 {
1168            return 0.0;
1169        }
1170
1171        // Compute degrees
1172        let degrees: Vec<f64> = (0..self.n_nodes)
1173            .map(|i| self.neighbors(i).len() as f64)
1174            .collect();
1175
1176        let m = self.n_edges as f64;
1177        let mut sum_jk = 0.0;
1178        let mut sum_j = 0.0;
1179        let mut sum_k = 0.0;
1180        let mut sum_j_sq = 0.0;
1181        let mut sum_k_sq = 0.0;
1182
1183        // Sum over all edges
1184        for v in 0..self.n_nodes {
1185            let j = degrees[v];
1186            for &u in self.neighbors(v) {
1187                let k = degrees[u];
1188                sum_jk += j * k;
1189                sum_j += j;
1190                sum_k += k;
1191                sum_j_sq += j * j;
1192                sum_k_sq += k * k;
1193            }
1194        }
1195
1196        // For undirected graphs, each edge is counted twice
1197        let normalization = if self.is_directed { m } else { 2.0 * m };
1198
1199        sum_jk /= normalization;
1200        sum_j /= normalization;
1201        sum_k /= normalization;
1202        sum_j_sq /= normalization;
1203        sum_k_sq /= normalization;
1204
1205        let numerator = sum_jk - sum_j * sum_k;
1206        let denominator = ((sum_j_sq - sum_j * sum_j) * (sum_k_sq - sum_k * sum_k)).sqrt();
1207
1208        if denominator < 1e-10 {
1209            return 0.0;
1210        }
1211
1212        numerator / denominator
1213    }
1214
1215    /// Compute shortest path between two nodes using BFS.
1216    ///
1217    /// Finds the shortest path (minimum number of hops) from source to target
1218    /// using breadth-first search. Works for both directed and undirected graphs.
1219    ///
1220    /// # Algorithm
1221    /// Uses BFS with predecessor tracking (Pohl 1971, bidirectional variant).
1222    ///
1223    /// # Arguments
1224    /// * `source` - Starting node ID
1225    /// * `target` - Destination node ID
1226    ///
1227    /// # Returns
1228    /// * `Some(path)` - Shortest path as vector of node IDs from source to target
1229    /// * `None` - No path exists between source and target
1230    ///
1231    /// # Complexity
1232    /// * Time: O(n + m) where n = nodes, m = edges
1233    /// * Space: O(n) for predecessor tracking
1234    ///
1235    /// # Examples
1236    /// ```
1237    /// use aprender::graph::Graph;
1238    ///
1239    /// let edges = vec![(0, 1), (1, 2), (2, 3), (0, 3)];
1240    /// let g = Graph::from_edges(&edges, false);
1241    ///
1242    /// // Shortest path from 0 to 3
1243    /// let path = g.shortest_path(0, 3).unwrap();
1244    /// assert_eq!(path.len(), 2); // 0 -> 3 (direct edge)
1245    /// assert_eq!(path[0], 0);
1246    /// assert_eq!(path[1], 3);
1247    ///
1248    /// // Path 0 to 2
1249    /// let path = g.shortest_path(0, 2).unwrap();
1250    /// assert!(path.len() <= 3); // Either 0->1->2 or 0->3->2
1251    /// ```
1252    pub fn shortest_path(&self, source: NodeId, target: NodeId) -> Option<Vec<NodeId>> {
1253        // Bounds checking
1254        if source >= self.n_nodes || target >= self.n_nodes {
1255            return None;
1256        }
1257
1258        // Special case: source == target
1259        if source == target {
1260            return Some(vec![source]);
1261        }
1262
1263        // BFS with predecessor tracking
1264        let mut visited = vec![false; self.n_nodes];
1265        let mut predecessor = vec![None; self.n_nodes];
1266        let mut queue = VecDeque::new();
1267
1268        visited[source] = true;
1269        queue.push_back(source);
1270
1271        while let Some(v) = queue.pop_front() {
1272            // Early termination if we reach target
1273            if v == target {
1274                break;
1275            }
1276
1277            for &w in self.neighbors(v) {
1278                if !visited[w] {
1279                    visited[w] = true;
1280                    predecessor[w] = Some(v);
1281                    queue.push_back(w);
1282                }
1283            }
1284        }
1285
1286        // Reconstruct path from target to source
1287        if !visited[target] {
1288            return None; // No path exists
1289        }
1290
1291        let mut path = Vec::new();
1292        let mut current = Some(target);
1293
1294        while let Some(node) = current {
1295            path.push(node);
1296            current = predecessor[node];
1297        }
1298
1299        path.reverse();
1300        Some(path)
1301    }
1302
1303    /// Compute shortest path using Dijkstra's algorithm for weighted graphs.
1304    ///
1305    /// Finds the shortest path from source to target using Dijkstra's algorithm
1306    /// with a binary heap priority queue. Handles both weighted and unweighted graphs.
1307    ///
1308    /// # Algorithm
1309    /// Uses Dijkstra's algorithm (1959) with priority queue for O((n+m) log n) complexity.
1310    ///
1311    /// # Arguments
1312    /// * `source` - Starting node ID
1313    /// * `target` - Destination node ID
1314    ///
1315    /// # Returns
1316    /// * `Some((path, distance))` - Shortest path and total distance
1317    /// * `None` - No path exists
1318    ///
1319    /// # Panics
1320    /// Panics if graph contains negative edge weights.
1321    ///
1322    /// # Complexity
1323    /// * Time: O((n + m) log n)
1324    /// * Space: O(n) for distance tracking and priority queue
1325    ///
1326    /// # Examples
1327    /// ```
1328    /// use aprender::graph::Graph;
1329    ///
1330    /// let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.0), (0, 2, 5.0)], false);
1331    /// let (path, dist) = g.dijkstra(0, 2).unwrap();
1332    /// assert_eq!(dist, 3.0); // 0->1->2 is shorter than 0->2
1333    /// ```
1334    pub fn dijkstra(&self, source: NodeId, target: NodeId) -> Option<(Vec<NodeId>, f64)> {
1335        use std::cmp::Ordering;
1336        use std::collections::BinaryHeap;
1337
1338        // Bounds checking
1339        if source >= self.n_nodes || target >= self.n_nodes {
1340            return None;
1341        }
1342
1343        // Special case: source == target
1344        if source == target {
1345            return Some((vec![source], 0.0));
1346        }
1347
1348        // Priority queue entry: (negative distance, node)
1349        // Use Reverse to make min-heap
1350        #[derive(Copy, Clone, PartialEq)]
1351        struct State {
1352            cost: f64,
1353            node: NodeId,
1354        }
1355
1356        impl Eq for State {}
1357
1358        impl Ord for State {
1359            fn cmp(&self, other: &Self) -> Ordering {
1360                // Reverse ordering for min-heap (negate costs)
1361                other
1362                    .cost
1363                    .partial_cmp(&self.cost)
1364                    .unwrap_or(Ordering::Equal)
1365            }
1366        }
1367
1368        impl PartialOrd for State {
1369            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1370                Some(self.cmp(other))
1371            }
1372        }
1373
1374        let mut distances = vec![f64::INFINITY; self.n_nodes];
1375        let mut predecessor = vec![None; self.n_nodes];
1376        let mut heap = BinaryHeap::new();
1377
1378        distances[source] = 0.0;
1379        heap.push(State {
1380            cost: 0.0,
1381            node: source,
1382        });
1383
1384        while let Some(State { cost, node }) = heap.pop() {
1385            // Early termination if we reach target
1386            if node == target {
1387                break;
1388            }
1389
1390            // Skip if we've already found a better path
1391            if cost > distances[node] {
1392                continue;
1393            }
1394
1395            // Explore neighbors
1396            let start = self.row_ptr[node];
1397            let end = self.row_ptr[node + 1];
1398
1399            for i in start..end {
1400                let neighbor = self.col_indices[i];
1401                let edge_weight = if self.edge_weights.is_empty() {
1402                    1.0
1403                } else {
1404                    self.edge_weights[i]
1405                };
1406
1407                // Panic on negative weights (Dijkstra requirement)
1408                assert!(
1409                    edge_weight >= 0.0,
1410                    "Dijkstra's algorithm requires non-negative edge weights. \
1411                     Found negative weight {edge_weight} on edge ({node}, {neighbor})"
1412                );
1413
1414                let next_cost = cost + edge_weight;
1415
1416                // Relaxation step
1417                if next_cost < distances[neighbor] {
1418                    distances[neighbor] = next_cost;
1419                    predecessor[neighbor] = Some(node);
1420                    heap.push(State {
1421                        cost: next_cost,
1422                        node: neighbor,
1423                    });
1424                }
1425            }
1426        }
1427
1428        // Check if target is reachable
1429        if distances[target].is_infinite() {
1430            return None;
1431        }
1432
1433        // Reconstruct path
1434        let mut path = Vec::new();
1435        let mut current = Some(target);
1436
1437        while let Some(node) = current {
1438            path.push(node);
1439            current = predecessor[node];
1440        }
1441
1442        path.reverse();
1443        Some((path, distances[target]))
1444    }
1445
1446    /// Compute all-pairs shortest paths using repeated BFS.
1447    ///
1448    /// Computes the shortest path distance between all pairs of nodes.
1449    /// Uses BFS for unweighted graphs (O(nm)) which is faster than
1450    /// Floyd-Warshall (O(n³)) for sparse graphs.
1451    ///
1452    /// # Algorithm
1453    /// Runs BFS from each node (Floyd 1962 for weighted, BFS for unweighted).
1454    ///
1455    /// # Returns
1456    /// Distance matrix where `[i][j]` = shortest distance from node i to node j.
1457    /// Returns `None` if nodes are not connected.
1458    ///
1459    /// # Complexity
1460    /// * Time: O(n·(n + m)) for unweighted graphs
1461    /// * Space: O(n²) for distance matrix
1462    ///
1463    /// # Examples
1464    /// ```
1465    /// use aprender::graph::Graph;
1466    ///
1467    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
1468    /// let dist = g.all_pairs_shortest_paths();
1469    ///
1470    /// assert_eq!(dist[0][0], Some(0));
1471    /// assert_eq!(dist[0][1], Some(1));
1472    /// assert_eq!(dist[0][2], Some(2));
1473    /// ```
1474    pub fn all_pairs_shortest_paths(&self) -> Vec<Vec<Option<usize>>> {
1475        let n = self.n_nodes;
1476        let mut distances = vec![vec![None; n]; n];
1477
1478        // Run BFS from each node
1479        for (source, row) in distances.iter_mut().enumerate().take(n) {
1480            let dist = self.bfs_distances(source);
1481
1482            for (target, cell) in row.iter_mut().enumerate().take(n) {
1483                if dist[target] != usize::MAX {
1484                    *cell = Some(dist[target]);
1485                }
1486            }
1487        }
1488
1489        distances
1490    }
1491
1492    /// Compute shortest path using A* search algorithm with heuristic.
1493    ///
1494    /// Finds the shortest path from source to target using A* algorithm
1495    /// with a user-provided heuristic function. The heuristic must be
1496    /// admissible (never overestimate) for optimality guarantees.
1497    ///
1498    /// # Algorithm
1499    /// Uses A* search (Hart et al. 1968) with f(n) = g(n) + h(n) where:
1500    /// - g(n) = actual cost from source to n
1501    /// - h(n) = estimated cost from n to target (heuristic)
1502    ///
1503    /// # Arguments
1504    /// * `source` - Starting node ID
1505    /// * `target` - Destination node ID
1506    /// * `heuristic` - Function mapping NodeId to estimated distance to target
1507    ///
1508    /// # Returns
1509    /// * `Some(path)` - Shortest path as vector of node IDs
1510    /// * `None` - No path exists
1511    ///
1512    /// # Complexity
1513    /// * Time: O((n + m) log n) with admissible heuristic
1514    /// * Space: O(n) for tracking
1515    ///
1516    /// # Examples
1517    /// ```
1518    /// use aprender::graph::Graph;
1519    ///
1520    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
1521    ///
1522    /// // Manhattan distance heuristic (example)
1523    /// let heuristic = |node: usize| (3 - node) as f64;
1524    ///
1525    /// let path = g.a_star(0, 3, heuristic).unwrap();
1526    /// assert_eq!(path, vec![0, 1, 2, 3]);
1527    /// ```
1528    pub fn a_star<F>(&self, source: NodeId, target: NodeId, heuristic: F) -> Option<Vec<NodeId>>
1529    where
1530        F: Fn(NodeId) -> f64,
1531    {
1532        use std::cmp::Ordering;
1533        use std::collections::BinaryHeap;
1534
1535        // Bounds checking
1536        if source >= self.n_nodes || target >= self.n_nodes {
1537            return None;
1538        }
1539
1540        // Special case: source == target
1541        if source == target {
1542            return Some(vec![source]);
1543        }
1544
1545        // Priority queue entry with f-score
1546        #[derive(Copy, Clone, PartialEq)]
1547        struct State {
1548            f_score: f64, // g + h
1549            g_score: f64, // actual cost
1550            node: NodeId,
1551        }
1552
1553        impl Eq for State {}
1554
1555        impl Ord for State {
1556            fn cmp(&self, other: &Self) -> Ordering {
1557                // Min-heap: lower f-score has higher priority
1558                other
1559                    .f_score
1560                    .partial_cmp(&self.f_score)
1561                    .unwrap_or(Ordering::Equal)
1562            }
1563        }
1564
1565        impl PartialOrd for State {
1566            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1567                Some(self.cmp(other))
1568            }
1569        }
1570
1571        let mut g_scores = vec![f64::INFINITY; self.n_nodes];
1572        let mut predecessor = vec![None; self.n_nodes];
1573        let mut heap = BinaryHeap::new();
1574
1575        g_scores[source] = 0.0;
1576        heap.push(State {
1577            f_score: heuristic(source),
1578            g_score: 0.0,
1579            node: source,
1580        });
1581
1582        while let Some(State {
1583            f_score: _,
1584            g_score,
1585            node,
1586        }) = heap.pop()
1587        {
1588            // Early termination if we reach target
1589            if node == target {
1590                break;
1591            }
1592
1593            // Skip if we've already found a better path
1594            if g_score > g_scores[node] {
1595                continue;
1596            }
1597
1598            // Explore neighbors
1599            let start = self.row_ptr[node];
1600            let end = self.row_ptr[node + 1];
1601
1602            for i in start..end {
1603                let neighbor = self.col_indices[i];
1604                let edge_weight = if self.edge_weights.is_empty() {
1605                    1.0
1606                } else {
1607                    self.edge_weights[i]
1608                };
1609
1610                let tentative_g = g_score + edge_weight;
1611
1612                // Relaxation step
1613                if tentative_g < g_scores[neighbor] {
1614                    g_scores[neighbor] = tentative_g;
1615                    predecessor[neighbor] = Some(node);
1616
1617                    let f = tentative_g + heuristic(neighbor);
1618                    heap.push(State {
1619                        f_score: f,
1620                        g_score: tentative_g,
1621                        node: neighbor,
1622                    });
1623                }
1624            }
1625        }
1626
1627        // Check if target is reachable
1628        if g_scores[target].is_infinite() {
1629            return None;
1630        }
1631
1632        // Reconstruct path
1633        let mut path = Vec::new();
1634        let mut current = Some(target);
1635
1636        while let Some(node) = current {
1637            path.push(node);
1638            current = predecessor[node];
1639        }
1640
1641        path.reverse();
1642        Some(path)
1643    }
1644
1645    /// Depth-First Search (DFS) traversal starting from a given node.
1646    ///
1647    /// Returns nodes in the order they were visited (pre-order traversal).
1648    /// Only visits nodes reachable from the source node.
1649    ///
1650    /// # Arguments
1651    /// * `source` - Starting node for traversal
1652    ///
1653    /// # Returns
1654    /// Vector of visited nodes in DFS order, or None if source is invalid
1655    ///
1656    /// # Time Complexity
1657    /// O(n + m) where n = number of nodes, m = number of edges
1658    ///
1659    /// # Examples
1660    /// ```
1661    /// use aprender::graph::Graph;
1662    ///
1663    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 3)], false);
1664    /// let visited = g.dfs(0).expect("valid source");
1665    /// assert_eq!(visited.len(), 4); // All nodes reachable
1666    /// assert_eq!(visited[0], 0); // Starts at source
1667    /// ```
1668    pub fn dfs(&self, source: NodeId) -> Option<Vec<NodeId>> {
1669        // Validate source node
1670        if source >= self.n_nodes {
1671            return None;
1672        }
1673
1674        let mut visited = vec![false; self.n_nodes];
1675        let mut stack = Vec::new();
1676        let mut order = Vec::new();
1677
1678        // Start DFS from source
1679        stack.push(source);
1680
1681        while let Some(node) = stack.pop() {
1682            if visited[node] {
1683                continue;
1684            }
1685
1686            visited[node] = true;
1687            order.push(node);
1688
1689            // Add neighbors to stack (in reverse order for consistent left-to-right traversal)
1690            let neighbors = self.neighbors(node);
1691            for &neighbor in neighbors.iter().rev() {
1692                if !visited[neighbor] {
1693                    stack.push(neighbor);
1694                }
1695            }
1696        }
1697
1698        Some(order)
1699    }
1700
1701    /// Find connected components using Union-Find algorithm.
1702    ///
1703    /// Returns a vector where each index is a node ID and the value is its component ID.
1704    /// Nodes in the same component have the same component ID.
1705    ///
1706    /// For directed graphs, this finds weakly connected components (ignores edge direction).
1707    ///
1708    /// # Returns
1709    /// Vector mapping each node to its component ID (0-indexed)
1710    ///
1711    /// # Time Complexity
1712    /// O(m·α(n)) where α is the inverse Ackermann function (effectively constant)
1713    ///
1714    /// # Examples
1715    /// ```
1716    /// use aprender::graph::Graph;
1717    ///
1718    /// // Two disconnected components: (0,1) and (2,3)
1719    /// let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
1720    /// let components = g.connected_components();
1721    ///
1722    /// assert_eq!(components[0], components[1]); // Same component
1723    /// assert_ne!(components[0], components[2]); // Different components
1724    /// ```
1725    pub fn connected_components(&self) -> Vec<usize> {
1726        let n = self.n_nodes;
1727        if n == 0 {
1728            return Vec::new();
1729        }
1730
1731        // Union-Find data structure
1732        let mut parent: Vec<usize> = (0..n).collect();
1733        let mut rank = vec![0; n];
1734
1735        // Find with path compression
1736        fn find(parent: &mut [usize], mut x: usize) -> usize {
1737            while parent[x] != x {
1738                let next = parent[x];
1739                parent[x] = parent[next]; // Path compression
1740                x = next;
1741            }
1742            x
1743        }
1744
1745        // Union by rank
1746        fn union(parent: &mut [usize], rank: &mut [usize], x: usize, y: usize) {
1747            let root_x = find(parent, x);
1748            let root_y = find(parent, y);
1749
1750            if root_x == root_y {
1751                return;
1752            }
1753
1754            // Union by rank
1755            use std::cmp::Ordering;
1756            match rank[root_x].cmp(&rank[root_y]) {
1757                Ordering::Less => parent[root_x] = root_y,
1758                Ordering::Greater => parent[root_y] = root_x,
1759                Ordering::Equal => {
1760                    parent[root_y] = root_x;
1761                    rank[root_x] += 1;
1762                }
1763            }
1764        }
1765
1766        // Process all edges (treat directed graphs as undirected for weak connectivity)
1767        for node in 0..n {
1768            for &neighbor in self.neighbors(node) {
1769                union(&mut parent, &mut rank, node, neighbor);
1770            }
1771        }
1772
1773        // Assign component IDs (compress paths and renumber)
1774        let mut component_map = HashMap::new();
1775        let mut next_component_id = 0;
1776        let mut result = vec![0; n];
1777
1778        for (node, component) in result.iter_mut().enumerate().take(n) {
1779            let root = find(&mut parent, node);
1780            let component_id = *component_map.entry(root).or_insert_with(|| {
1781                let id = next_component_id;
1782                next_component_id += 1;
1783                id
1784            });
1785            *component = component_id;
1786        }
1787
1788        result
1789    }
1790
1791    /// Find strongly connected components using Tarjan's algorithm.
1792    ///
1793    /// A strongly connected component (SCC) is a maximal set of vertices where
1794    /// every vertex is reachable from every other vertex in the set.
1795    ///
1796    /// Only meaningful for directed graphs. For undirected graphs, use `connected_components()`.
1797    ///
1798    /// # Returns
1799    /// Vector mapping each node to its SCC ID (0-indexed)
1800    ///
1801    /// # Time Complexity
1802    /// O(n + m) - single-pass DFS
1803    ///
1804    /// # Examples
1805    /// ```
1806    /// use aprender::graph::Graph;
1807    ///
1808    /// // Directed cycle: 0 -> 1 -> 2 -> 0
1809    /// let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
1810    /// let sccs = g.strongly_connected_components();
1811    ///
1812    /// // All nodes in same SCC
1813    /// assert_eq!(sccs[0], sccs[1]);
1814    /// assert_eq!(sccs[1], sccs[2]);
1815    /// ```
1816    pub fn strongly_connected_components(&self) -> Vec<usize> {
1817        let n = self.n_nodes;
1818        if n == 0 {
1819            return Vec::new();
1820        }
1821
1822        // Tarjan's algorithm state
1823        struct TarjanState {
1824            disc: Vec<Option<usize>>,
1825            low: Vec<usize>,
1826            on_stack: Vec<bool>,
1827            stack: Vec<usize>,
1828            time: usize,
1829            scc_id: Vec<usize>,
1830            scc_counter: usize,
1831        }
1832
1833        impl TarjanState {
1834            fn new(n: usize) -> Self {
1835                Self {
1836                    disc: vec![None; n],
1837                    low: vec![0; n],
1838                    on_stack: vec![false; n],
1839                    stack: Vec::new(),
1840                    time: 0,
1841                    scc_id: vec![0; n],
1842                    scc_counter: 0,
1843                }
1844            }
1845
1846            fn dfs(&mut self, v: usize, graph: &Graph) {
1847                // Initialize discovery time and low-link value
1848                self.disc[v] = Some(self.time);
1849                self.low[v] = self.time;
1850                self.time += 1;
1851                self.stack.push(v);
1852                self.on_stack[v] = true;
1853
1854                // Visit all neighbors
1855                for &w in graph.neighbors(v) {
1856                    if self.disc[w].is_none() {
1857                        // Tree edge: recurse
1858                        self.dfs(w, graph);
1859                        self.low[v] = self.low[v].min(self.low[w]);
1860                    } else if self.on_stack[w] {
1861                        // Back edge to node on stack
1862                        self.low[v] =
1863                            self.low[v].min(self.disc[w].expect("disc[w] should be Some"));
1864                    }
1865                }
1866
1867                // If v is a root node, pop the stack and create SCC
1868                if let Some(disc_v) = self.disc[v] {
1869                    if self.low[v] == disc_v {
1870                        // Found an SCC
1871                        loop {
1872                            let w = self.stack.pop().expect("stack should not be empty");
1873                            self.on_stack[w] = false;
1874                            self.scc_id[w] = self.scc_counter;
1875                            if w == v {
1876                                break;
1877                            }
1878                        }
1879                        self.scc_counter += 1;
1880                    }
1881                }
1882            }
1883        }
1884
1885        let mut state = TarjanState::new(n);
1886
1887        // Run Tarjan's algorithm from each unvisited node
1888        for v in 0..n {
1889            if state.disc[v].is_none() {
1890                state.dfs(v, self);
1891            }
1892        }
1893
1894        state.scc_id
1895    }
1896
1897    /// Topological sort for directed acyclic graphs (DAGs).
1898    ///
1899    /// Returns a linear ordering of vertices where for every directed edge (u,v),
1900    /// u appears before v in the ordering.
1901    ///
1902    /// Only valid for DAGs. If the graph contains a cycle, returns None.
1903    ///
1904    /// # Returns
1905    /// Some(Vec<NodeId>) with nodes in topological order, or None if graph has cycles
1906    ///
1907    /// # Time Complexity
1908    /// O(n + m) - DFS-based approach
1909    ///
1910    /// # Examples
1911    /// ```
1912    /// use aprender::graph::Graph;
1913    ///
1914    /// // DAG: 0 -> 1 -> 2
1915    /// let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
1916    /// let order = g.topological_sort().expect("DAG should have topological order");
1917    ///
1918    /// // 0 comes before 1, 1 comes before 2
1919    /// assert!(order.iter().position(|&x| x == 0) < order.iter().position(|&x| x == 1));
1920    /// assert!(order.iter().position(|&x| x == 1) < order.iter().position(|&x| x == 2));
1921    /// ```
1922    pub fn topological_sort(&self) -> Option<Vec<NodeId>> {
1923        let n = self.n_nodes;
1924        if n == 0 {
1925            return Some(Vec::new());
1926        }
1927
1928        // DFS-based topological sort with cycle detection
1929        let mut visited = vec![false; n];
1930        let mut in_stack = vec![false; n]; // For cycle detection
1931        let mut order = Vec::new();
1932
1933        fn dfs(
1934            v: usize,
1935            graph: &Graph,
1936            visited: &mut [bool],
1937            in_stack: &mut [bool],
1938            order: &mut Vec<usize>,
1939        ) -> bool {
1940            if in_stack[v] {
1941                // Back edge found - cycle detected
1942                return false;
1943            }
1944            if visited[v] {
1945                // Already processed
1946                return true;
1947            }
1948
1949            visited[v] = true;
1950            in_stack[v] = true;
1951
1952            // Visit all neighbors
1953            for &neighbor in graph.neighbors(v) {
1954                if !dfs(neighbor, graph, visited, in_stack, order) {
1955                    return false; // Cycle detected
1956                }
1957            }
1958
1959            in_stack[v] = false;
1960            order.push(v); // Add to order in post-order (reverse topological)
1961
1962            true
1963        }
1964
1965        // Run DFS from each unvisited node
1966        for v in 0..n {
1967            if !visited[v] && !dfs(v, self, &mut visited, &mut in_stack, &mut order) {
1968                return None; // Cycle detected
1969            }
1970        }
1971
1972        // Reverse to get topological order
1973        order.reverse();
1974        Some(order)
1975    }
1976
1977    /// Count common neighbors between two nodes (link prediction metric).
1978    ///
1979    /// Returns the number of neighbors shared by both nodes u and v.
1980    /// Used for link prediction: nodes with many common neighbors are
1981    /// more likely to form a connection.
1982    ///
1983    /// # Arguments
1984    /// * `u` - First node
1985    /// * `v` - Second node
1986    ///
1987    /// # Returns
1988    /// Number of common neighbors, or None if either node is invalid
1989    ///
1990    /// # Time Complexity
1991    /// O(min(deg(u), deg(v))) - intersection of neighbor sets
1992    ///
1993    /// # Examples
1994    /// ```
1995    /// use aprender::graph::Graph;
1996    ///
1997    /// // Triangle: 0-1, 0-2, 1-2
1998    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], false);
1999    ///
2000    /// // Nodes 1 and 2 share neighbor 0
2001    /// assert_eq!(g.common_neighbors(1, 2), Some(1));
2002    /// ```
2003    pub fn common_neighbors(&self, u: NodeId, v: NodeId) -> Option<usize> {
2004        // Validate nodes
2005        if u >= self.n_nodes || v >= self.n_nodes {
2006            return None;
2007        }
2008
2009        let neighbors_u = self.neighbors(u);
2010        let neighbors_v = self.neighbors(v);
2011
2012        // Use two-pointer technique (both are sorted)
2013        let mut count = 0;
2014        let mut i = 0;
2015        let mut j = 0;
2016
2017        while i < neighbors_u.len() && j < neighbors_v.len() {
2018            use std::cmp::Ordering;
2019            match neighbors_u[i].cmp(&neighbors_v[j]) {
2020                Ordering::Equal => {
2021                    count += 1;
2022                    i += 1;
2023                    j += 1;
2024                }
2025                Ordering::Less => i += 1,
2026                Ordering::Greater => j += 1,
2027            }
2028        }
2029
2030        Some(count)
2031    }
2032
2033    /// Adamic-Adar index for link prediction between two nodes.
2034    ///
2035    /// Computes a weighted measure of common neighbors, where neighbors with
2036    /// fewer connections are weighted more heavily. This captures the intuition
2037    /// that sharing a rare neighbor is more significant than sharing a common one.
2038    ///
2039    /// Formula: AA(u,v) = Σ 1/log(deg(z)) for all common neighbors z
2040    ///
2041    /// # Arguments
2042    /// * `u` - First node
2043    /// * `v` - Second node
2044    ///
2045    /// # Returns
2046    /// Adamic-Adar index score, or None if either node is invalid
2047    ///
2048    /// # Time Complexity
2049    /// O(min(deg(u), deg(v))) - intersection of neighbor sets
2050    ///
2051    /// # Examples
2052    /// ```
2053    /// use aprender::graph::Graph;
2054    ///
2055    /// // Graph with shared neighbors
2056    /// let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], false);
2057    ///
2058    /// // Adamic-Adar index for nodes 1 and 2
2059    /// let aa = g.adamic_adar_index(1, 2).expect("valid nodes");
2060    /// assert!(aa > 0.0);
2061    /// ```
2062    pub fn adamic_adar_index(&self, u: NodeId, v: NodeId) -> Option<f64> {
2063        // Validate nodes
2064        if u >= self.n_nodes || v >= self.n_nodes {
2065            return None;
2066        }
2067
2068        let neighbors_u = self.neighbors(u);
2069        let neighbors_v = self.neighbors(v);
2070
2071        // Use two-pointer technique to find common neighbors
2072        let mut score = 0.0;
2073        let mut i = 0;
2074        let mut j = 0;
2075
2076        while i < neighbors_u.len() && j < neighbors_v.len() {
2077            use std::cmp::Ordering;
2078            match neighbors_u[i].cmp(&neighbors_v[j]) {
2079                Ordering::Equal => {
2080                    let common_neighbor = neighbors_u[i];
2081                    let degree = self.neighbors(common_neighbor).len();
2082
2083                    // Avoid log(1) = 0 division by zero
2084                    if degree > 1 {
2085                        score += 1.0 / (degree as f64).ln();
2086                    }
2087
2088                    i += 1;
2089                    j += 1;
2090                }
2091                Ordering::Less => i += 1,
2092                Ordering::Greater => j += 1,
2093            }
2094        }
2095
2096        Some(score)
2097    }
2098
2099    /// Label propagation algorithm for community detection.
2100    ///
2101    /// Iteratively assigns each node the most common label among its neighbors.
2102    /// Nodes with the same final label belong to the same community.
2103    ///
2104    /// # Arguments
2105    /// * `max_iter` - Maximum number of iterations (default: 100)
2106    /// * `seed` - Random seed for deterministic tie-breaking (optional)
2107    ///
2108    /// # Returns
2109    /// Vector mapping each node to its community label (0-indexed)
2110    ///
2111    /// # Time Complexity
2112    /// O(max_iter · m) where m = number of edges
2113    ///
2114    /// # Examples
2115    /// ```
2116    /// use aprender::graph::Graph;
2117    ///
2118    /// // Graph with two communities: (0,1,2) and (3,4,5)
2119    /// let g = Graph::from_edges(
2120    ///     &[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)],
2121    ///     false
2122    /// );
2123    ///
2124    /// let communities = g.label_propagation(100, Some(42));
2125    /// // Nodes in same community have same label
2126    /// assert_eq!(communities[0], communities[1]);
2127    /// ```
2128    pub fn label_propagation(&self, max_iter: usize, seed: Option<u64>) -> Vec<usize> {
2129        let n = self.n_nodes;
2130        if n == 0 {
2131            return Vec::new();
2132        }
2133
2134        // Initialize each node with unique label
2135        let mut labels: Vec<usize> = (0..n).collect();
2136
2137        // Simple deterministic ordering based on seed
2138        let mut node_order: Vec<usize> = (0..n).collect();
2139        if let Some(s) = seed {
2140            // Simple shuffle based on seed for deterministic results
2141            for i in 0..n {
2142                let j = ((s.wrapping_mul(i as u64 + 1)) % (n as u64)) as usize;
2143                node_order.swap(i, j);
2144            }
2145        }
2146
2147        for _ in 0..max_iter {
2148            let mut changed = false;
2149
2150            // Process nodes in random order
2151            for &node in &node_order {
2152                let neighbors = self.neighbors(node);
2153                if neighbors.is_empty() {
2154                    continue;
2155                }
2156
2157                // Count neighbor labels
2158                let mut label_counts = HashMap::new();
2159                for &neighbor in neighbors {
2160                    *label_counts.entry(labels[neighbor]).or_insert(0) += 1;
2161                }
2162
2163                // Find most common label (with deterministic tie-breaking)
2164                let most_common_label = label_counts
2165                    .iter()
2166                    .max_by_key(|(label, count)| (*count, std::cmp::Reverse(*label)))
2167                    .map(|(label, _)| *label)
2168                    .expect("label_counts should not be empty");
2169
2170                if labels[node] != most_common_label {
2171                    labels[node] = most_common_label;
2172                    changed = true;
2173                }
2174            }
2175
2176            // Early termination if converged
2177            if !changed {
2178                break;
2179            }
2180        }
2181
2182        labels
2183    }
2184}
2185
2186/// Kahan summation for computing L1 distance between two vectors.
2187///
2188/// Uses compensated summation to prevent floating-point drift.
2189/// Accumulates O(n·ε) error where ε is machine epsilon without compensation.
2190fn kahan_diff(a: &[f64], b: &[f64]) -> f64 {
2191    let mut sum = 0.0;
2192    let mut c = 0.0;
2193    for i in 0..a.len() {
2194        let y = (a[i] - b[i]).abs() - c;
2195        let t = sum + y;
2196        c = (t - sum) - y;
2197        sum = t;
2198    }
2199    sum
2200}
2201
2202#[cfg(test)]
2203mod tests {
2204    use super::*;
2205
2206    #[test]
2207    fn test_empty_graph() {
2208        let g = Graph::new(false);
2209        assert_eq!(g.num_nodes(), 0);
2210        assert_eq!(g.num_edges(), 0);
2211        assert!(!g.is_directed());
2212    }
2213
2214    #[test]
2215    fn test_directed_graph() {
2216        let g = Graph::new(true);
2217        assert!(g.is_directed());
2218    }
2219
2220    #[test]
2221    fn test_from_edges_empty() {
2222        let g = Graph::from_edges(&[], false);
2223        assert_eq!(g.num_nodes(), 0);
2224        assert_eq!(g.num_edges(), 0);
2225    }
2226
2227    #[test]
2228    fn test_from_edges_undirected() {
2229        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
2230        assert_eq!(g.num_nodes(), 3);
2231        assert_eq!(g.num_edges(), 3);
2232
2233        // Check neighbors (should be sorted)
2234        assert_eq!(g.neighbors(0), &[1, 2]);
2235        assert_eq!(g.neighbors(1), &[0, 2]);
2236        assert_eq!(g.neighbors(2), &[0, 1]);
2237    }
2238
2239    #[test]
2240    fn test_from_edges_directed() {
2241        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
2242        assert_eq!(g.num_nodes(), 3);
2243        assert_eq!(g.num_edges(), 2);
2244
2245        // Directed: edges only go one way
2246        assert_eq!(g.neighbors(0), &[1]);
2247        assert_eq!(g.neighbors(1), &[2]);
2248        assert!(g.neighbors(2).is_empty()); // no outgoing edges
2249    }
2250
2251    #[test]
2252    fn test_from_edges_with_gaps() {
2253        // Node IDs don't have to be contiguous
2254        let g = Graph::from_edges(&[(0, 5), (5, 10)], false);
2255        assert_eq!(g.num_nodes(), 11); // max node + 1
2256        assert_eq!(g.num_edges(), 2);
2257
2258        assert_eq!(g.neighbors(0), &[5]);
2259        assert_eq!(g.neighbors(5), &[0, 10]);
2260        assert!(g.neighbors(1).is_empty()); // isolated node
2261    }
2262
2263    #[test]
2264    fn test_from_edges_duplicate_edges() {
2265        // Duplicate edges should be deduplicated
2266        let g = Graph::from_edges(&[(0, 1), (0, 1), (1, 0)], false);
2267        assert_eq!(g.num_nodes(), 2);
2268
2269        // Should only have one edge (0,1) in undirected graph
2270        assert_eq!(g.neighbors(0), &[1]);
2271        assert_eq!(g.neighbors(1), &[0]);
2272    }
2273
2274    #[test]
2275    fn test_from_edges_self_loop() {
2276        let g = Graph::from_edges(&[(0, 0), (0, 1)], false);
2277        assert_eq!(g.num_nodes(), 2);
2278
2279        // Self-loop should appear once
2280        assert_eq!(g.neighbors(0), &[0, 1]);
2281    }
2282
2283    #[test]
2284    fn test_neighbors_invalid_node() {
2285        let g = Graph::from_edges(&[(0, 1)], false);
2286        assert!(g.neighbors(999).is_empty()); // non-existent node
2287    }
2288
2289    #[test]
2290    fn test_degree_centrality_empty() {
2291        let g = Graph::new(false);
2292        let dc = g.degree_centrality();
2293        assert_eq!(dc.len(), 0);
2294    }
2295
2296    #[test]
2297    fn test_degree_centrality_single_node() {
2298        let g = Graph::from_edges(&[(0, 0)], false);
2299        let dc = g.degree_centrality();
2300        assert_eq!(dc[&0], 0.0); // single node, normalized degree is 0
2301    }
2302
2303    #[test]
2304    fn test_degree_centrality_star_graph() {
2305        // Star graph: center node connected to 3 leaves
2306        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
2307        let dc = g.degree_centrality();
2308
2309        assert_eq!(dc[&0], 1.0); // center: degree 3 / (4-1) = 1.0
2310        assert!((dc[&1] - 1.0 / 3.0).abs() < 1e-6); // leaves: degree 1 / 3
2311        assert!((dc[&2] - 1.0 / 3.0).abs() < 1e-6);
2312        assert!((dc[&3] - 1.0 / 3.0).abs() < 1e-6);
2313    }
2314
2315    #[test]
2316    fn test_degree_centrality_complete_graph() {
2317        // Complete graph K4: every node connected to every other
2318        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
2319        let dc = g.degree_centrality();
2320
2321        // All nodes have degree 3 in K4, normalized: 3/3 = 1.0
2322        for v in 0..4 {
2323            assert_eq!(dc[&v], 1.0);
2324        }
2325    }
2326
2327    #[test]
2328    fn test_degree_centrality_path_graph() {
2329        // Path graph: 0 -- 1 -- 2 -- 3
2330        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
2331        let dc = g.degree_centrality();
2332
2333        // Endpoints have degree 1, middle nodes have degree 2
2334        assert!((dc[&0] - 1.0 / 3.0).abs() < 1e-6);
2335        assert!((dc[&1] - 2.0 / 3.0).abs() < 1e-6);
2336        assert!((dc[&2] - 2.0 / 3.0).abs() < 1e-6);
2337        assert!((dc[&3] - 1.0 / 3.0).abs() < 1e-6);
2338    }
2339
2340    #[test]
2341    fn test_degree_centrality_directed() {
2342        // Directed: only count outgoing edges
2343        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], true);
2344        let dc = g.degree_centrality();
2345
2346        assert!((dc[&0] - 2.0 / 2.0).abs() < 1e-6); // 2 outgoing edges
2347        assert!((dc[&1] - 1.0 / 2.0).abs() < 1e-6); // 1 outgoing edge
2348        assert_eq!(dc[&2], 0.0); // 0 outgoing edges
2349    }
2350
2351    // PageRank tests
2352
2353    #[test]
2354    fn test_pagerank_empty() {
2355        let g = Graph::new(true);
2356        let pr = g
2357            .pagerank(0.85, 100, 1e-6)
2358            .expect("pagerank should succeed for empty graph");
2359        assert!(pr.is_empty());
2360    }
2361
2362    #[test]
2363    fn test_pagerank_single_node() {
2364        let g = Graph::from_edges(&[(0, 0)], true);
2365        let pr = g
2366            .pagerank(0.85, 100, 1e-6)
2367            .expect("pagerank should succeed for single node graph");
2368        assert_eq!(pr.len(), 1);
2369        assert!((pr[0] - 1.0).abs() < 1e-6); // Single node has all rank
2370    }
2371
2372    #[test]
2373    fn test_pagerank_sum_equals_one() {
2374        // PageRank scores must sum to 1.0 (within numerical precision)
2375        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
2376        let pr = g
2377            .pagerank(0.85, 100, 1e-6)
2378            .expect("pagerank should converge for cycle graph");
2379        let sum: f64 = pr.iter().sum();
2380        assert!((sum - 1.0).abs() < 1e-10); // Kahan ensures high precision
2381    }
2382
2383    #[test]
2384    fn test_pagerank_cycle_graph() {
2385        // Cycle graph: 0 -> 1 -> 2 -> 0
2386        // All nodes should have equal PageRank (by symmetry)
2387        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
2388        let pr = g
2389            .pagerank(0.85, 100, 1e-6)
2390            .expect("pagerank should converge for symmetric cycle");
2391
2392        assert_eq!(pr.len(), 3);
2393        // All nodes have equal rank in symmetric cycle
2394        assert!((pr[0] - 1.0 / 3.0).abs() < 1e-6);
2395        assert!((pr[1] - 1.0 / 3.0).abs() < 1e-6);
2396        assert!((pr[2] - 1.0 / 3.0).abs() < 1e-6);
2397    }
2398
2399    #[test]
2400    fn test_pagerank_star_graph_directed() {
2401        // Star graph: 0 -> {1, 2, 3}
2402        // Node 0 distributes rank equally to 1, 2, 3
2403        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], true);
2404        let pr = g
2405            .pagerank(0.85, 100, 1e-6)
2406            .expect("pagerank should converge for directed star graph");
2407
2408        assert_eq!(pr.len(), 4);
2409        // Leaves have no incoming edges except from 0
2410        // Node 0 has no incoming edges (lowest rank)
2411        assert!(pr[0] < pr[1]); // 0 has lowest rank
2412        assert!((pr[1] - pr[2]).abs() < 1e-6); // leaves have equal rank
2413        assert!((pr[2] - pr[3]).abs() < 1e-6);
2414    }
2415
2416    #[test]
2417    fn test_pagerank_convergence() {
2418        // Test that PageRank converges within max_iter
2419        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (1, 0)], true);
2420        let pr = g
2421            .pagerank(0.85, 100, 1e-6)
2422            .expect("pagerank should converge within max iterations");
2423
2424        // Should converge (not hit max_iter)
2425        assert_eq!(pr.len(), 3);
2426        assert!((pr.iter().sum::<f64>() - 1.0).abs() < 1e-10);
2427    }
2428
2429    #[test]
2430    fn test_pagerank_no_outgoing_edges() {
2431        // Node with no outgoing edges (dangling node)
2432        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
2433        let pr = g
2434            .pagerank(0.85, 100, 1e-6)
2435            .expect("pagerank should handle dangling nodes correctly");
2436
2437        // Node 2 has no outgoing edges, but should still have rank
2438        assert_eq!(pr.len(), 3);
2439        assert!(pr[2] > 0.0);
2440        assert!((pr.iter().sum::<f64>() - 1.0).abs() < 1e-10);
2441    }
2442
2443    #[test]
2444    fn test_pagerank_undirected() {
2445        // Undirected graph: each edge goes both ways
2446        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
2447        let pr = g
2448            .pagerank(0.85, 100, 1e-6)
2449            .expect("pagerank should converge for undirected path graph");
2450
2451        assert_eq!(pr.len(), 3);
2452        // Middle node should have highest rank
2453        assert!(pr[1] > pr[0]);
2454        assert!(pr[1] > pr[2]);
2455        assert!((pr[0] - pr[2]).abs() < 1e-6); // endpoints equal
2456    }
2457
2458    #[test]
2459    fn test_kahan_diff() {
2460        let a = vec![1.0, 2.0, 3.0];
2461        let b = vec![1.1, 2.1, 2.9];
2462        let diff = kahan_diff(&a, &b);
2463        assert!((diff - 0.3).abs() < 1e-10);
2464    }
2465
2466    // Betweenness centrality tests
2467
2468    #[test]
2469    fn test_betweenness_centrality_empty() {
2470        let g = Graph::new(false);
2471        let bc = g.betweenness_centrality();
2472        assert!(bc.is_empty());
2473    }
2474
2475    #[test]
2476    fn test_betweenness_centrality_single_node() {
2477        let g = Graph::from_edges(&[(0, 0)], false);
2478        let bc = g.betweenness_centrality();
2479        assert_eq!(bc.len(), 1);
2480        assert_eq!(bc[0], 0.0); // Single node has no betweenness
2481    }
2482
2483    #[test]
2484    fn test_betweenness_centrality_path_graph() {
2485        // Path graph: 0 -- 1 -- 2
2486        // Middle node lies on all paths between endpoints
2487        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
2488        let bc = g.betweenness_centrality();
2489
2490        assert_eq!(bc.len(), 3);
2491        // Middle node has highest betweenness (all paths go through it)
2492        assert!(bc[1] > bc[0]);
2493        assert!(bc[1] > bc[2]);
2494        // Endpoints should have equal betweenness (by symmetry)
2495        assert!((bc[0] - bc[2]).abs() < 1e-6);
2496    }
2497
2498    #[test]
2499    fn test_betweenness_centrality_star_graph() {
2500        // Star graph: center (0) connected to leaves {1, 2, 3}
2501        // Center lies on all paths between leaves
2502        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
2503        let bc = g.betweenness_centrality();
2504
2505        assert_eq!(bc.len(), 4);
2506        // Center has highest betweenness
2507        assert!(bc[0] > bc[1]);
2508        assert!(bc[0] > bc[2]);
2509        assert!(bc[0] > bc[3]);
2510        // Leaves should have equal betweenness (by symmetry)
2511        assert!((bc[1] - bc[2]).abs() < 1e-6);
2512        assert!((bc[2] - bc[3]).abs() < 1e-6);
2513    }
2514
2515    #[test]
2516    fn test_betweenness_centrality_cycle_graph() {
2517        // Cycle graph: 0 -- 1 -- 2 -- 3 -- 0
2518        // All nodes have equal betweenness by symmetry
2519        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false);
2520        let bc = g.betweenness_centrality();
2521
2522        assert_eq!(bc.len(), 4);
2523        // All nodes should have equal betweenness
2524        for i in 0..4 {
2525            for j in i + 1..4 {
2526                assert!((bc[i] - bc[j]).abs() < 1e-6);
2527            }
2528        }
2529    }
2530
2531    #[test]
2532    fn test_betweenness_centrality_complete_graph() {
2533        // Complete graph K4: every node connected to every other
2534        // All nodes have equal betweenness by symmetry
2535        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
2536        let bc = g.betweenness_centrality();
2537
2538        assert_eq!(bc.len(), 4);
2539        // All nodes should have equal betweenness (by symmetry)
2540        for i in 0..4 {
2541            for j in i + 1..4 {
2542                assert!((bc[i] - bc[j]).abs() < 1e-6);
2543            }
2544        }
2545    }
2546
2547    #[test]
2548    fn test_betweenness_centrality_bridge_graph() {
2549        // Bridge graph: (0 -- 1) -- 2 -- (3 -- 4)
2550        // Node 2 is a bridge and should have high betweenness
2551        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false);
2552        let bc = g.betweenness_centrality();
2553
2554        assert_eq!(bc.len(), 5);
2555        // Bridge node (2) has highest betweenness
2556        assert!(bc[2] > bc[0]);
2557        assert!(bc[2] > bc[1]);
2558        assert!(bc[2] > bc[3]);
2559        assert!(bc[2] > bc[4]);
2560        // Nodes 1 and 3 also have some betweenness (but less than 2)
2561        assert!(bc[1] > bc[0]);
2562        assert!(bc[3] > bc[4]);
2563    }
2564
2565    #[test]
2566    fn test_betweenness_centrality_directed() {
2567        // Directed path: 0 -> 1 -> 2
2568        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
2569        let bc = g.betweenness_centrality();
2570
2571        assert_eq!(bc.len(), 3);
2572        // In a directed path, middle node should have positive betweenness
2573        // (it lies on the path from 0 to 2)
2574        // All nodes should have some betweenness in directed graphs
2575        assert!(bc.iter().any(|&x| x > 0.0));
2576    }
2577
2578    #[test]
2579    fn test_betweenness_centrality_disconnected() {
2580        // Disconnected graph: (0 -- 1) and (2 -- 3)
2581        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
2582        let bc = g.betweenness_centrality();
2583
2584        assert_eq!(bc.len(), 4);
2585        // Nodes within same component should have equal betweenness
2586        assert!((bc[0] - bc[1]).abs() < 1e-6);
2587        assert!((bc[2] - bc[3]).abs() < 1e-6);
2588    }
2589
2590    // Community Detection Tests
2591
2592    #[test]
2593    fn test_modularity_empty_graph() {
2594        let g = Graph::new(false);
2595        let communities = vec![];
2596        let modularity = g.modularity(&communities);
2597        assert_eq!(modularity, 0.0);
2598    }
2599
2600    #[test]
2601    fn test_modularity_single_community() {
2602        // Triangle: all nodes in one community
2603        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
2604        let communities = vec![vec![0, 1, 2]];
2605        let modularity = g.modularity(&communities);
2606        // For single community covering whole graph, Q = 0
2607        assert!((modularity - 0.0).abs() < 1e-6);
2608    }
2609
2610    #[test]
2611    fn test_modularity_two_communities() {
2612        // Two triangles connected by single edge: 0-1-2 and 3-4-5, edge 2-3
2613        let g = Graph::from_edges(
2614            &[
2615                (0, 1),
2616                (1, 2),
2617                (2, 0), // Triangle 1
2618                (3, 4),
2619                (4, 5),
2620                (5, 3), // Triangle 2
2621                (2, 3), // Inter-community edge
2622            ],
2623            false,
2624        );
2625
2626        let communities = vec![vec![0, 1, 2], vec![3, 4, 5]];
2627        let modularity = g.modularity(&communities);
2628
2629        // Should have positive modularity (good community structure)
2630        assert!(modularity > 0.0);
2631        assert!(modularity < 1.0); // Not perfect due to inter-community edge
2632    }
2633
2634    #[test]
2635    fn test_modularity_perfect_split() {
2636        // Two disconnected triangles
2637        let g = Graph::from_edges(
2638            &[
2639                (0, 1),
2640                (1, 2),
2641                (2, 0), // Triangle 1
2642                (3, 4),
2643                (4, 5),
2644                (5, 3), // Triangle 2
2645            ],
2646            false,
2647        );
2648
2649        let communities = vec![vec![0, 1, 2], vec![3, 4, 5]];
2650        let modularity = g.modularity(&communities);
2651
2652        // Perfect split should have high modularity
2653        assert!(modularity > 0.5);
2654    }
2655
2656    #[test]
2657    // Implementation complete
2658    fn test_louvain_empty_graph() {
2659        let g = Graph::new(false);
2660        let communities = g.louvain();
2661        assert_eq!(communities.len(), 0);
2662    }
2663
2664    #[test]
2665    // Implementation complete
2666    fn test_louvain_single_node() {
2667        // Single node with self-loop
2668        let g = Graph::from_edges(&[(0, 0)], false);
2669        let communities = g.louvain();
2670        assert_eq!(communities.len(), 1);
2671        assert_eq!(communities[0].len(), 1);
2672    }
2673
2674    #[test]
2675    // Implementation complete
2676    fn test_louvain_two_nodes() {
2677        let g = Graph::from_edges(&[(0, 1)], false);
2678        let communities = g.louvain();
2679
2680        // Should find 1 community containing both nodes
2681        assert_eq!(communities.len(), 1);
2682        assert_eq!(communities[0].len(), 2);
2683    }
2684
2685    #[test]
2686    // Implementation complete
2687    fn test_louvain_triangle() {
2688        // Single triangle - should be one community
2689        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
2690        let communities = g.louvain();
2691
2692        assert_eq!(communities.len(), 1);
2693        assert_eq!(communities[0].len(), 3);
2694    }
2695
2696    #[test]
2697    // Implementation complete
2698    fn test_louvain_two_triangles_connected() {
2699        // Two triangles connected by one edge
2700        let g = Graph::from_edges(
2701            &[
2702                (0, 1),
2703                (1, 2),
2704                (2, 0), // Triangle 1
2705                (3, 4),
2706                (4, 5),
2707                (5, 3), // Triangle 2
2708                (2, 3), // Connection
2709            ],
2710            false,
2711        );
2712
2713        let communities = g.louvain();
2714
2715        // Should find 2 communities
2716        assert_eq!(communities.len(), 2);
2717
2718        // Verify all nodes are assigned
2719        let all_nodes: Vec<_> = communities.iter().flat_map(|c| c.iter()).copied().collect();
2720        assert_eq!(all_nodes.len(), 6);
2721    }
2722
2723    #[test]
2724    // Implementation complete
2725    fn test_louvain_disconnected_components() {
2726        // Two separate triangles (no connection)
2727        let g = Graph::from_edges(
2728            &[
2729                (0, 1),
2730                (1, 2),
2731                (2, 0), // Component 1
2732                (3, 4),
2733                (4, 5),
2734                (5, 3), // Component 2
2735            ],
2736            false,
2737        );
2738
2739        let communities = g.louvain();
2740
2741        // Should find at least 2 communities (one per component)
2742        assert!(communities.len() >= 2);
2743
2744        // Verify nodes 0,1,2 are in different community than 3,4,5
2745        let comm1_nodes: Vec<_> = communities
2746            .iter()
2747            .find(|c| c.contains(&0))
2748            .expect("node 0 should be assigned to a community")
2749            .clone();
2750        let comm2_nodes: Vec<_> = communities
2751            .iter()
2752            .find(|c| c.contains(&3))
2753            .expect("node 3 should be assigned to a community")
2754            .clone();
2755
2756        assert!(comm1_nodes.contains(&0));
2757        assert!(comm1_nodes.contains(&1));
2758        assert!(comm1_nodes.contains(&2));
2759
2760        assert!(comm2_nodes.contains(&3));
2761        assert!(comm2_nodes.contains(&4));
2762        assert!(comm2_nodes.contains(&5));
2763
2764        // Verify no overlap
2765        assert!(!comm1_nodes.contains(&3));
2766        assert!(!comm2_nodes.contains(&0));
2767    }
2768
2769    #[test]
2770    // Implementation complete
2771    fn test_louvain_karate_club() {
2772        // Zachary's Karate Club network (simplified 4-node version)
2773        // Known ground truth: 2 factions
2774        let g = Graph::from_edges(
2775            &[
2776                (0, 1),
2777                (0, 2),
2778                (1, 2), // Group 1
2779                (2, 3), // Bridge
2780                (3, 4),
2781                (3, 5),
2782                (4, 5), // Group 2
2783            ],
2784            false,
2785        );
2786
2787        let communities = g.louvain();
2788
2789        // Should detect at least 2 communities
2790        assert!(communities.len() >= 2);
2791
2792        // Node 2 and 3 are bridge nodes - could be in either community
2793        // But groups {0,1} and {4,5} should be detected
2794        let all_nodes: Vec<_> = communities.iter().flat_map(|c| c.iter()).copied().collect();
2795        assert_eq!(all_nodes.len(), 6);
2796    }
2797
2798    #[test]
2799    // Implementation complete
2800    fn test_louvain_star_graph() {
2801        // Star graph: central node 0 connected to 1,2,3,4
2802        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false);
2803
2804        let communities = g.louvain();
2805
2806        // Star graph could be 1 community or split
2807        // Just verify all nodes are assigned
2808        assert!(!communities.is_empty());
2809        let all_nodes: Vec<_> = communities.iter().flat_map(|c| c.iter()).copied().collect();
2810        assert_eq!(all_nodes.len(), 5);
2811    }
2812
2813    #[test]
2814    // Implementation complete
2815    fn test_louvain_complete_graph() {
2816        // Complete graph K4 - all nodes connected
2817        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
2818
2819        let communities = g.louvain();
2820
2821        // Complete graph should be single community
2822        assert_eq!(communities.len(), 1);
2823        assert_eq!(communities[0].len(), 4);
2824    }
2825
2826    #[test]
2827    // Implementation complete
2828    fn test_louvain_modularity_improves() {
2829        // Two clear communities
2830        let g = Graph::from_edges(
2831            &[
2832                (0, 1),
2833                (1, 2),
2834                (2, 0), // Triangle 1
2835                (3, 4),
2836                (4, 5),
2837                (5, 3), // Triangle 2
2838            ],
2839            false,
2840        );
2841
2842        let communities = g.louvain();
2843        let modularity = g.modularity(&communities);
2844
2845        // Louvain should find good communities (high modularity)
2846        assert!(modularity > 0.3);
2847    }
2848
2849    #[test]
2850    // Implementation complete
2851    fn test_louvain_all_nodes_assigned() {
2852        // Verify every node gets assigned to exactly one community
2853        let g = Graph::from_edges(
2854            &[
2855                (0, 1),
2856                (1, 2),
2857                (2, 3),
2858                (3, 4),
2859                (4, 0), // Pentagon
2860            ],
2861            false,
2862        );
2863
2864        let communities = g.louvain();
2865
2866        let mut assigned_nodes: Vec<NodeId> = Vec::new();
2867        for community in &communities {
2868            assigned_nodes.extend(community);
2869        }
2870
2871        // All 5 nodes should be assigned
2872        assigned_nodes.sort_unstable();
2873        assert_eq!(assigned_nodes, vec![0, 1, 2, 3, 4]);
2874
2875        // No node should appear twice
2876        let unique_count = assigned_nodes.len();
2877        assigned_nodes.dedup();
2878        assert_eq!(assigned_nodes.len(), unique_count);
2879    }
2880
2881    #[test]
2882    fn test_modularity_bad_partition() {
2883        // Triangle with each node in separate community (worst partition)
2884        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
2885
2886        let communities = vec![vec![0], vec![1], vec![2]];
2887        let modularity = g.modularity(&communities);
2888
2889        // Bad partition should have negative or very low modularity
2890        assert!(modularity < 0.1);
2891    }
2892
2893    // Closeness Centrality Tests
2894
2895    #[test]
2896    fn test_closeness_centrality_empty() {
2897        let g = Graph::new(false);
2898        let cc = g.closeness_centrality();
2899        assert!(cc.is_empty());
2900    }
2901
2902    #[test]
2903    fn test_closeness_centrality_star_graph() {
2904        // Star graph: center (0) is close to all nodes
2905        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
2906        let cc = g.closeness_centrality();
2907
2908        assert_eq!(cc.len(), 4);
2909        // Center has highest closeness
2910        assert!(cc[0] > cc[1]);
2911        assert!(cc[0] > cc[2]);
2912        assert!(cc[0] > cc[3]);
2913        // Leaves have equal closeness by symmetry
2914        assert!((cc[1] - cc[2]).abs() < 1e-6);
2915        assert!((cc[2] - cc[3]).abs() < 1e-6);
2916    }
2917
2918    #[test]
2919    fn test_closeness_centrality_path_graph() {
2920        // Path: 0--1--2--3
2921        // Middle nodes have higher closeness
2922        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
2923        let cc = g.closeness_centrality();
2924
2925        assert_eq!(cc.len(), 4);
2926        // Middle nodes more central
2927        assert!(cc[1] > cc[0]);
2928        assert!(cc[2] > cc[0]);
2929        assert!(cc[1] > cc[3]);
2930        assert!(cc[2] > cc[3]);
2931    }
2932
2933    // Eigenvector Centrality Tests
2934
2935    #[test]
2936    fn test_eigenvector_centrality_empty() {
2937        let g = Graph::new(false);
2938        let ec = g
2939            .eigenvector_centrality(100, 1e-6)
2940            .expect("eigenvector centrality should succeed on empty graph");
2941        assert!(ec.is_empty());
2942    }
2943
2944    #[test]
2945    fn test_eigenvector_centrality_star_graph() {
2946        // Star graph: center has highest eigenvector centrality
2947        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
2948        let ec = g
2949            .eigenvector_centrality(100, 1e-6)
2950            .expect("eigenvector centrality should succeed on star graph");
2951
2952        assert_eq!(ec.len(), 4);
2953        // Center should have highest score
2954        assert!(ec[0] > ec[1]);
2955        assert!(ec[0] > ec[2]);
2956        assert!(ec[0] > ec[3]);
2957    }
2958
2959    #[test]
2960    fn test_eigenvector_centrality_path_graph() {
2961        // Path graph: middle nodes more central
2962        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
2963        let ec = g
2964            .eigenvector_centrality(100, 1e-6)
2965            .expect("eigenvector centrality should succeed on path graph");
2966
2967        assert_eq!(ec.len(), 4);
2968        // Middle nodes should have higher scores
2969        assert!(ec[1] > ec[0]);
2970        assert!(ec[2] > ec[3]);
2971    }
2972
2973    #[test]
2974    fn test_eigenvector_centrality_disconnected() {
2975        // Graph with no edges
2976        let g = Graph::from_edges(&[], false);
2977        let ec = g
2978            .eigenvector_centrality(100, 1e-6)
2979            .expect("eigenvector centrality should succeed on graph with no edges");
2980        assert!(ec.is_empty());
2981    }
2982
2983    // Katz Centrality Tests
2984
2985    #[test]
2986    fn test_katz_centrality_empty() {
2987        let g = Graph::new(true);
2988        let kc = g
2989            .katz_centrality(0.1, 100, 1e-6)
2990            .expect("katz centrality should succeed on empty graph");
2991        assert!(kc.is_empty());
2992    }
2993
2994    #[test]
2995    fn test_katz_centrality_invalid_alpha() {
2996        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
2997
2998        // Alpha = 0 should fail
2999        assert!(g.katz_centrality(0.0, 100, 1e-6).is_err());
3000
3001        // Alpha = 1 should fail
3002        assert!(g.katz_centrality(1.0, 100, 1e-6).is_err());
3003
3004        // Alpha > 1 should fail
3005        assert!(g.katz_centrality(1.5, 100, 1e-6).is_err());
3006    }
3007
3008    #[test]
3009    fn test_katz_centrality_cycle() {
3010        // Cycle graph: all nodes should have equal Katz centrality
3011        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
3012        let kc = g
3013            .katz_centrality(0.1, 100, 1e-6)
3014            .expect("katz centrality should succeed on cycle graph");
3015
3016        assert_eq!(kc.len(), 3);
3017        // All nodes equal by symmetry
3018        assert!((kc[0] - kc[1]).abs() < 1e-3);
3019        assert!((kc[1] - kc[2]).abs() < 1e-3);
3020    }
3021
3022    #[test]
3023    fn test_katz_centrality_star_directed() {
3024        // Directed star: 0 -> {1,2,3}
3025        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], true);
3026        let kc = g
3027            .katz_centrality(0.1, 100, 1e-6)
3028            .expect("katz centrality should succeed on directed star graph");
3029
3030        assert_eq!(kc.len(), 4);
3031        // Nodes with incoming edges have higher Katz centrality
3032        assert!(kc[1] > kc[0]);
3033        assert!(kc[2] > kc[0]);
3034        assert!(kc[3] > kc[0]);
3035    }
3036
3037    // Harmonic Centrality Tests
3038
3039    #[test]
3040    fn test_harmonic_centrality_empty() {
3041        let g = Graph::new(false);
3042        let hc = g.harmonic_centrality();
3043        assert!(hc.is_empty());
3044    }
3045
3046    #[test]
3047    fn test_harmonic_centrality_star_graph() {
3048        // Star graph: center has highest harmonic centrality
3049        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3050        let hc = g.harmonic_centrality();
3051
3052        assert_eq!(hc.len(), 4);
3053        // Center most central
3054        assert!(hc[0] > hc[1]);
3055        assert!(hc[0] > hc[2]);
3056        assert!(hc[0] > hc[3]);
3057    }
3058
3059    #[test]
3060    fn test_harmonic_centrality_disconnected() {
3061        // Disconnected graph: (0--1) and (2--3)
3062        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
3063        let hc = g.harmonic_centrality();
3064
3065        assert_eq!(hc.len(), 4);
3066        // Nodes within same component have equal harmonic centrality
3067        assert!((hc[0] - hc[1]).abs() < 1e-6);
3068        assert!((hc[2] - hc[3]).abs() < 1e-6);
3069    }
3070
3071    // Density Tests
3072
3073    #[test]
3074    fn test_density_empty() {
3075        let g = Graph::new(false);
3076        assert_eq!(g.density(), 0.0);
3077    }
3078
3079    #[test]
3080    fn test_density_single_node() {
3081        let g = Graph::from_edges(&[(0, 0)], false);
3082        assert_eq!(g.density(), 0.0);
3083    }
3084
3085    #[test]
3086    fn test_density_complete_graph() {
3087        // Complete graph K4: all nodes connected
3088        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
3089        assert!((g.density() - 1.0).abs() < 1e-6);
3090    }
3091
3092    #[test]
3093    fn test_density_path_graph() {
3094        // Path: 0--1--2--3 (3 edges, 4 nodes)
3095        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3096
3097        // Undirected: density = 2*m / (n*(n-1)) = 2*3 / (4*3) = 0.5
3098        assert!((g.density() - 0.5).abs() < 1e-6);
3099    }
3100
3101    #[test]
3102    fn test_density_directed() {
3103        // Directed: 0->1, 1->2 (2 edges, 3 nodes)
3104        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
3105
3106        // Directed: density = m / (n*(n-1)) = 2 / (3*2) = 1/3
3107        assert!((g.density() - 1.0 / 3.0).abs() < 1e-6);
3108    }
3109
3110    // Diameter Tests
3111
3112    #[test]
3113    fn test_diameter_empty() {
3114        let g = Graph::new(false);
3115        assert_eq!(g.diameter(), None);
3116    }
3117
3118    #[test]
3119    fn test_diameter_single_node() {
3120        let g = Graph::from_edges(&[(0, 0)], false);
3121        assert_eq!(g.diameter(), Some(0));
3122    }
3123
3124    #[test]
3125    fn test_diameter_path_graph() {
3126        // Path: 0--1--2--3 has diameter 3
3127        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3128        assert_eq!(g.diameter(), Some(3));
3129    }
3130
3131    #[test]
3132    fn test_diameter_star_graph() {
3133        // Star graph: center to any leaf is 1, leaf to leaf is 2
3134        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3135        assert_eq!(g.diameter(), Some(2));
3136    }
3137
3138    #[test]
3139    fn test_diameter_disconnected() {
3140        // Disconnected: (0--1) and (2--3)
3141        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
3142        assert_eq!(g.diameter(), None); // Disconnected
3143    }
3144
3145    #[test]
3146    fn test_diameter_complete_graph() {
3147        // Complete graph K4: diameter is 1 (all nodes adjacent)
3148        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
3149        assert_eq!(g.diameter(), Some(1));
3150    }
3151
3152    // Clustering Coefficient Tests
3153
3154    #[test]
3155    fn test_clustering_coefficient_empty() {
3156        let g = Graph::new(false);
3157        assert_eq!(g.clustering_coefficient(), 0.0);
3158    }
3159
3160    #[test]
3161    fn test_clustering_coefficient_triangle() {
3162        // Triangle: perfect clustering (coefficient = 1.0)
3163        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
3164        assert!((g.clustering_coefficient() - 1.0).abs() < 1e-6);
3165    }
3166
3167    #[test]
3168    fn test_clustering_coefficient_star_graph() {
3169        // Star graph: no triangles (coefficient = 0.0)
3170        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3171        assert_eq!(g.clustering_coefficient(), 0.0);
3172    }
3173
3174    #[test]
3175    fn test_clustering_coefficient_partial() {
3176        // Graph with one triangle among 4 nodes
3177        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (0, 3)], false);
3178
3179        // Node 0: 3 neighbors, 1 triangle (0-1-2)
3180        // Node 1: 2 neighbors, 1 triangle
3181        // Node 2: 2 neighbors, 1 triangle
3182        // Node 3: 1 neighbor, 0 triangles
3183        let cc = g.clustering_coefficient();
3184        assert!(cc > 0.0);
3185        assert!(cc < 1.0);
3186    }
3187
3188    // Assortativity Tests
3189
3190    #[test]
3191    fn test_assortativity_empty() {
3192        let g = Graph::new(false);
3193        assert_eq!(g.assortativity(), 0.0);
3194    }
3195
3196    #[test]
3197    fn test_assortativity_star_graph() {
3198        // Star graph: hub (deg 3) connects to leaves (deg 1)
3199        // Negative assortativity (high-degree connects to low-degree)
3200        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3201        assert!(g.assortativity() < 0.0);
3202    }
3203
3204    #[test]
3205    fn test_assortativity_complete_graph() {
3206        // Complete graph K4: all nodes have same degree
3207        // Should have assortativity close to 0 (or NaN due to zero variance)
3208        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
3209        let assort = g.assortativity();
3210
3211        // All nodes have degree 3, so variance is 0
3212        // Assortativity is undefined but we return 0.0
3213        assert_eq!(assort, 0.0);
3214    }
3215
3216    #[test]
3217    fn test_assortativity_path_graph() {
3218        // Path: 0--1--2--3
3219        // Endpoints (deg 1) connect to middle (deg 2)
3220        // Middle nodes (deg 2) connect to mixed
3221        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3222        let assort = g.assortativity();
3223
3224        // Should have negative assortativity
3225        assert!(assort < 0.0);
3226    }
3227
3228    // ========================================================================
3229    // Pathfinding Algorithm Tests
3230    // ========================================================================
3231
3232    #[test]
3233    fn test_shortest_path_direct_edge() {
3234        // Simplest case: direct edge between source and target
3235        let g = Graph::from_edges(&[(0, 1)], false);
3236        let path = g.shortest_path(0, 1).expect("path should exist");
3237        assert_eq!(path, vec![0, 1]);
3238        assert_eq!(path.len(), 2);
3239    }
3240
3241    #[test]
3242    fn test_shortest_path_same_node() {
3243        // Source == target should return single-node path
3244        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
3245        let path = g.shortest_path(1, 1).expect("path should exist");
3246        assert_eq!(path, vec![1]);
3247    }
3248
3249    #[test]
3250    fn test_shortest_path_disconnected() {
3251        // No path between disconnected components
3252        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
3253        assert!(g.shortest_path(0, 3).is_none());
3254        assert!(g.shortest_path(1, 2).is_none());
3255    }
3256
3257    #[test]
3258    fn test_shortest_path_invalid_nodes() {
3259        // Out-of-bounds node IDs should return None
3260        let g = Graph::from_edges(&[(0, 1)], false);
3261        assert!(g.shortest_path(0, 10).is_none());
3262        assert!(g.shortest_path(10, 0).is_none());
3263    }
3264
3265    #[test]
3266    fn test_shortest_path_multiple_paths() {
3267        // Graph with multiple paths of same length
3268        // 0 -- 1
3269        // |    |
3270        // 2 -- 3
3271        let g = Graph::from_edges(&[(0, 1), (1, 3), (0, 2), (2, 3)], false);
3272        let path = g.shortest_path(0, 3).expect("path should exist");
3273
3274        // Both 0->1->3 and 0->2->3 are shortest paths (length 3)
3275        assert_eq!(path.len(), 3);
3276        assert_eq!(path[0], 0);
3277        assert_eq!(path[2], 3);
3278        assert!(path[1] == 1 || path[1] == 2); // Either path is valid
3279    }
3280
3281    #[test]
3282    fn test_shortest_path_linear_chain() {
3283        // Path graph: 0 -- 1 -- 2 -- 3 -- 4
3284        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false);
3285
3286        // Test various source-target pairs
3287        let path = g.shortest_path(0, 4).expect("path should exist");
3288        assert_eq!(path, vec![0, 1, 2, 3, 4]);
3289
3290        let path = g.shortest_path(0, 2).expect("path should exist");
3291        assert_eq!(path, vec![0, 1, 2]);
3292
3293        let path = g.shortest_path(1, 3).expect("path should exist");
3294        assert_eq!(path, vec![1, 2, 3]);
3295    }
3296
3297    #[test]
3298    fn test_shortest_path_triangle() {
3299        // Triangle graph
3300        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
3301
3302        // All pairs should have path length 2
3303        let path = g.shortest_path(0, 1).expect("path should exist");
3304        assert_eq!(path.len(), 2);
3305
3306        let path = g.shortest_path(0, 2).expect("path should exist");
3307        assert_eq!(path.len(), 2);
3308
3309        let path = g.shortest_path(1, 2).expect("path should exist");
3310        assert_eq!(path.len(), 2);
3311    }
3312
3313    #[test]
3314    fn test_shortest_path_directed() {
3315        // Directed graph: 0 -> 1 -> 2
3316        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
3317
3318        // Forward paths exist
3319        let path = g.shortest_path(0, 2).expect("forward path should exist");
3320        assert_eq!(path, vec![0, 1, 2]);
3321
3322        // Backward paths don't exist
3323        assert!(g.shortest_path(2, 0).is_none());
3324    }
3325
3326    #[test]
3327    fn test_shortest_path_cycle() {
3328        // Cycle graph: 0 -> 1 -> 2 -> 3 -> 0
3329        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], true);
3330
3331        // Test path that uses cycle
3332        let path = g.shortest_path(0, 3).expect("path should exist");
3333
3334        // Direct path 0->1->2->3 (length 4) vs backward 0<-3 (not possible in directed)
3335        assert_eq!(path.len(), 4);
3336        assert_eq!(path, vec![0, 1, 2, 3]);
3337    }
3338
3339    #[test]
3340    fn test_shortest_path_star_graph() {
3341        // Star graph: 0 connected to 1, 2, 3, 4
3342        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false);
3343
3344        // Center to leaf: length 2
3345        let path = g.shortest_path(0, 1).expect("path should exist");
3346        assert_eq!(path.len(), 2);
3347
3348        // Leaf to leaf through center: length 3
3349        let path = g.shortest_path(1, 2).expect("path should exist");
3350        assert_eq!(path.len(), 3);
3351        assert_eq!(path[0], 1);
3352        assert_eq!(path[1], 0); // Must go through center
3353        assert_eq!(path[2], 2);
3354    }
3355
3356    #[test]
3357    fn test_shortest_path_empty_graph() {
3358        // Empty graph
3359        let g = Graph::new(false);
3360        assert!(g.shortest_path(0, 0).is_none());
3361    }
3362
3363    #[test]
3364    fn test_shortest_path_single_node_graph() {
3365        // Graph with single self-loop
3366        let g = Graph::from_edges(&[(0, 0)], false);
3367        let path = g.shortest_path(0, 0).expect("path should exist");
3368        assert_eq!(path, vec![0]);
3369    }
3370
3371    #[test]
3372    fn test_shortest_path_complete_graph() {
3373        // Complete graph K4
3374        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
3375
3376        // All pairs should have direct edge (length 2)
3377        for i in 0..4 {
3378            for j in 0..4 {
3379                if i != j {
3380                    let path = g.shortest_path(i, j).expect("path should exist");
3381                    assert_eq!(path.len(), 2, "Path from {i} to {j} should be direct");
3382                    assert_eq!(path[0], i);
3383                    assert_eq!(path[1], j);
3384                }
3385            }
3386        }
3387    }
3388
3389    #[test]
3390    fn test_shortest_path_bidirectional() {
3391        // Undirected: path should exist in both directions
3392        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
3393
3394        let path_forward = g.shortest_path(0, 2).expect("forward path should exist");
3395        let path_backward = g.shortest_path(2, 0).expect("backward path should exist");
3396
3397        assert_eq!(path_forward.len(), path_backward.len());
3398        assert_eq!(path_forward.len(), 3);
3399
3400        // Paths should be reverses of each other
3401        let reversed: Vec<_> = path_backward.iter().rev().copied().collect();
3402        assert_eq!(path_forward, reversed);
3403    }
3404
3405    // ========================================================================
3406    // Dijkstra's Algorithm Tests
3407    // ========================================================================
3408
3409    #[test]
3410    fn test_dijkstra_simple_weighted() {
3411        // Simple weighted graph
3412        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.0), (0, 2, 5.0)], false);
3413
3414        let (path, dist) = g.dijkstra(0, 2).expect("path should exist");
3415        assert_eq!(dist, 3.0); // 0->1->2 is shorter than 0->2
3416        assert_eq!(path, vec![0, 1, 2]);
3417    }
3418
3419    #[test]
3420    fn test_dijkstra_same_node() {
3421        let g = Graph::from_weighted_edges(&[(0, 1, 1.0)], false);
3422        let (path, dist) = g.dijkstra(0, 0).expect("path should exist");
3423        assert_eq!(path, vec![0]);
3424        assert_eq!(dist, 0.0);
3425    }
3426
3427    #[test]
3428    fn test_dijkstra_disconnected() {
3429        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (2, 3, 1.0)], false);
3430        assert!(g.dijkstra(0, 3).is_none());
3431    }
3432
3433    #[test]
3434    fn test_dijkstra_unweighted() {
3435        // Unweighted graph (uses weight 1.0 for all edges)
3436        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
3437        let (path, dist) = g.dijkstra(0, 2).expect("path should exist");
3438        assert_eq!(dist, 2.0);
3439        assert_eq!(path, vec![0, 1, 2]);
3440    }
3441
3442    #[test]
3443    fn test_dijkstra_triangle_weighted() {
3444        // Triangle with different weights
3445        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 1.0), (0, 2, 5.0)], false);
3446
3447        let (path, dist) = g.dijkstra(0, 2).expect("path should exist");
3448        assert_eq!(dist, 2.0); // 0->1->2 (cost 2) vs 0->2 (cost 5)
3449        assert_eq!(path, vec![0, 1, 2]);
3450    }
3451
3452    #[test]
3453    fn test_dijkstra_multiple_paths() {
3454        // Graph with multiple paths of different costs
3455        //     1 ----2.0---- 2
3456        //    /              |
3457        //   /               |
3458        //  0                1.0
3459        //   \               |
3460        //    \              |
3461        //     3 ----1.0---- 4
3462        let g = Graph::from_weighted_edges(
3463            &[
3464                (0, 1, 1.0),
3465                (1, 2, 2.0),
3466                (0, 3, 1.0),
3467                (3, 4, 1.0),
3468                (4, 2, 1.0),
3469            ],
3470            false,
3471        );
3472
3473        let (_path, dist) = g.dijkstra(0, 2).expect("path should exist");
3474        assert_eq!(dist, 3.0); // Best path: 0->3->4->2 or 0->1->2
3475    }
3476
3477    #[test]
3478    fn test_dijkstra_linear_chain() {
3479        // Weighted linear chain
3480        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.0), (2, 3, 3.0)], false);
3481
3482        let (path, dist) = g.dijkstra(0, 3).expect("path should exist");
3483        assert_eq!(dist, 6.0);
3484        assert_eq!(path, vec![0, 1, 2, 3]);
3485    }
3486
3487    #[test]
3488    fn test_dijkstra_directed_graph() {
3489        // Directed weighted graph
3490        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.0)], true);
3491
3492        let (path, dist) = g.dijkstra(0, 2).expect("forward path should exist");
3493        assert_eq!(dist, 3.0);
3494        assert_eq!(path, vec![0, 1, 2]);
3495
3496        // Backward path doesn't exist
3497        assert!(g.dijkstra(2, 0).is_none());
3498    }
3499
3500    #[test]
3501    fn test_dijkstra_invalid_nodes() {
3502        let g = Graph::from_weighted_edges(&[(0, 1, 1.0)], false);
3503        assert!(g.dijkstra(0, 10).is_none());
3504        assert!(g.dijkstra(10, 0).is_none());
3505    }
3506
3507    #[test]
3508    #[should_panic(expected = "negative edge weights")]
3509    fn test_dijkstra_negative_weights() {
3510        // Dijkstra should panic on negative weights
3511        let g = Graph::from_weighted_edges(&[(0, 1, -1.0)], false);
3512        let _ = g.dijkstra(0, 1);
3513    }
3514
3515    #[test]
3516    fn test_dijkstra_zero_weight_edges() {
3517        // Zero-weight edges should work
3518        let g = Graph::from_weighted_edges(&[(0, 1, 0.0), (1, 2, 1.0)], false);
3519        let (path, dist) = g.dijkstra(0, 2).expect("path should exist");
3520        assert_eq!(dist, 1.0);
3521        assert_eq!(path, vec![0, 1, 2]);
3522    }
3523
3524    #[test]
3525    fn test_dijkstra_complete_graph_weighted() {
3526        // Complete graph K3 with different weights
3527        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 1.0), (0, 2, 3.0)], false);
3528
3529        // Direct edge 0->2 costs 3.0, but 0->1->2 costs 2.0
3530        let (path, dist) = g.dijkstra(0, 2).expect("path should exist");
3531        assert_eq!(dist, 2.0);
3532        assert_eq!(path, vec![0, 1, 2]);
3533    }
3534
3535    #[test]
3536    fn test_dijkstra_star_graph_weighted() {
3537        // Star graph with center at node 0
3538        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (0, 2, 2.0), (0, 3, 3.0)], false);
3539
3540        // Path from 1 to 3 must go through 0
3541        let (path, dist) = g.dijkstra(1, 3).expect("path should exist");
3542        assert_eq!(dist, 4.0); // 1->0 (1.0) + 0->3 (3.0)
3543        assert_eq!(path, vec![1, 0, 3]);
3544    }
3545
3546    #[test]
3547    fn test_dijkstra_vs_shortest_path() {
3548        // On unweighted graph, Dijkstra should match shortest_path
3549        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3550
3551        let sp_path = g
3552            .shortest_path(0, 3)
3553            .expect("shortest_path should find path");
3554        let (dij_path, dij_dist) = g.dijkstra(0, 3).expect("dijkstra should find path");
3555
3556        assert_eq!(sp_path.len(), dij_path.len());
3557        assert_eq!(dij_dist, (dij_path.len() - 1) as f64);
3558    }
3559
3560    #[test]
3561    fn test_dijkstra_floating_point_precision() {
3562        // Test with fractional weights
3563        let g = Graph::from_weighted_edges(&[(0, 1, 0.1), (1, 2, 0.2), (0, 2, 0.31)], false);
3564
3565        let (path, dist) = g.dijkstra(0, 2).expect("path should exist");
3566        assert!((dist - 0.3).abs() < 1e-10); // 0.1 + 0.2 = 0.3
3567        assert_eq!(path, vec![0, 1, 2]);
3568    }
3569
3570    // ========================================================================
3571    // All-Pairs Shortest Paths Tests
3572    // ========================================================================
3573
3574    #[test]
3575    fn test_apsp_linear_chain() {
3576        // Linear chain: 0 -- 1 -- 2 -- 3
3577        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3578        let dist = g.all_pairs_shortest_paths();
3579
3580        // Check diagonal (distance to self = 0)
3581        for (i, row) in dist.iter().enumerate().take(4) {
3582            assert_eq!(row[i], Some(0));
3583        }
3584
3585        // Check distances
3586        assert_eq!(dist[0][1], Some(1));
3587        assert_eq!(dist[0][2], Some(2));
3588        assert_eq!(dist[0][3], Some(3));
3589        assert_eq!(dist[1][2], Some(1));
3590        assert_eq!(dist[1][3], Some(2));
3591        assert_eq!(dist[2][3], Some(1));
3592
3593        // Check symmetry (undirected graph)
3594        assert_eq!(dist[0][3], dist[3][0]);
3595        assert_eq!(dist[1][2], dist[2][1]);
3596    }
3597
3598    #[test]
3599    fn test_apsp_complete_graph() {
3600        // Complete graph K4
3601        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
3602        let dist = g.all_pairs_shortest_paths();
3603
3604        // All pairs should have distance 1 (direct edge) except diagonal
3605        for (i, row) in dist.iter().enumerate().take(4) {
3606            for (j, &cell) in row.iter().enumerate().take(4) {
3607                if i == j {
3608                    assert_eq!(cell, Some(0));
3609                } else {
3610                    assert_eq!(cell, Some(1));
3611                }
3612            }
3613        }
3614    }
3615
3616    #[test]
3617    fn test_apsp_disconnected() {
3618        // Two disconnected components: (0, 1) and (2, 3)
3619        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
3620        let dist = g.all_pairs_shortest_paths();
3621
3622        // Within components
3623        assert_eq!(dist[0][1], Some(1));
3624        assert_eq!(dist[1][0], Some(1));
3625        assert_eq!(dist[2][3], Some(1));
3626        assert_eq!(dist[3][2], Some(1));
3627
3628        // Between components (no path)
3629        assert_eq!(dist[0][2], None);
3630        assert_eq!(dist[0][3], None);
3631        assert_eq!(dist[1][2], None);
3632        assert_eq!(dist[1][3], None);
3633    }
3634
3635    #[test]
3636    fn test_apsp_directed() {
3637        // Directed graph: 0 -> 1 -> 2
3638        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
3639        let dist = g.all_pairs_shortest_paths();
3640
3641        // Forward paths
3642        assert_eq!(dist[0][1], Some(1));
3643        assert_eq!(dist[0][2], Some(2));
3644        assert_eq!(dist[1][2], Some(1));
3645
3646        // Backward paths (no reverse edges)
3647        assert_eq!(dist[1][0], None);
3648        assert_eq!(dist[2][0], None);
3649        assert_eq!(dist[2][1], None);
3650    }
3651
3652    #[test]
3653    fn test_apsp_triangle() {
3654        // Triangle graph
3655        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
3656        let dist = g.all_pairs_shortest_paths();
3657
3658        // All pairs should have distance 1 (triangle) except diagonal
3659        for (i, row) in dist.iter().enumerate().take(3) {
3660            for (j, &cell) in row.iter().enumerate().take(3) {
3661                if i == j {
3662                    assert_eq!(cell, Some(0));
3663                } else {
3664                    assert_eq!(cell, Some(1));
3665                }
3666            }
3667        }
3668    }
3669
3670    #[test]
3671    fn test_apsp_star_graph() {
3672        // Star graph: 0 connected to 1, 2, 3
3673        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3674        let dist = g.all_pairs_shortest_paths();
3675
3676        // Center to leaves: distance 1
3677        assert_eq!(dist[0][1], Some(1));
3678        assert_eq!(dist[0][2], Some(1));
3679        assert_eq!(dist[0][3], Some(1));
3680
3681        // Leaf to leaf through center: distance 2
3682        assert_eq!(dist[1][2], Some(2));
3683        assert_eq!(dist[1][3], Some(2));
3684        assert_eq!(dist[2][3], Some(2));
3685    }
3686
3687    #[test]
3688    fn test_apsp_empty_graph() {
3689        let g = Graph::new(false);
3690        let dist = g.all_pairs_shortest_paths();
3691        assert_eq!(dist.len(), 0);
3692    }
3693
3694    #[test]
3695    fn test_apsp_single_node() {
3696        let g = Graph::from_edges(&[(0, 0)], false);
3697        let dist = g.all_pairs_shortest_paths();
3698
3699        assert_eq!(dist.len(), 1);
3700        assert_eq!(dist[0][0], Some(0));
3701    }
3702
3703    #[test]
3704    fn test_apsp_cycle() {
3705        // Cycle: 0 -> 1 -> 2 -> 3 -> 0
3706        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], true);
3707        let dist = g.all_pairs_shortest_paths();
3708
3709        // Along cycle direction
3710        assert_eq!(dist[0][1], Some(1));
3711        assert_eq!(dist[0][2], Some(2));
3712        assert_eq!(dist[0][3], Some(3));
3713        assert_eq!(dist[1][3], Some(2));
3714
3715        // All nodes reachable in directed cycle
3716        for row in dist.iter().take(4) {
3717            for &cell in row.iter().take(4) {
3718                assert!(cell.is_some());
3719            }
3720        }
3721    }
3722
3723    #[test]
3724    fn test_apsp_matrix_size() {
3725        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3726        let dist = g.all_pairs_shortest_paths();
3727
3728        // Matrix should be n×n
3729        assert_eq!(dist.len(), 4);
3730        for row in &dist {
3731            assert_eq!(row.len(), 4);
3732        }
3733    }
3734
3735    // ========================================================================
3736    // A* Search Algorithm Tests
3737    // ========================================================================
3738
3739    #[test]
3740    fn test_astar_linear_chain() {
3741        // Linear chain: 0 -- 1 -- 2 -- 3
3742        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3743
3744        // Simple distance heuristic
3745        let heuristic = |node: usize| (3 - node) as f64;
3746
3747        let path = g.a_star(0, 3, heuristic).expect("path should exist");
3748        assert_eq!(path, vec![0, 1, 2, 3]);
3749    }
3750
3751    #[test]
3752    fn test_astar_same_node() {
3753        let g = Graph::from_edges(&[(0, 1)], false);
3754        let heuristic = |_: usize| 0.0;
3755
3756        let path = g.a_star(0, 0, heuristic).expect("path should exist");
3757        assert_eq!(path, vec![0]);
3758    }
3759
3760    #[test]
3761    fn test_astar_disconnected() {
3762        // Two disconnected components
3763        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
3764        let heuristic = |_: usize| 0.0;
3765
3766        assert!(g.a_star(0, 3, heuristic).is_none());
3767    }
3768
3769    #[test]
3770    fn test_astar_zero_heuristic() {
3771        // With h(n) = 0, A* behaves like Dijkstra
3772        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3773        let heuristic = |_: usize| 0.0;
3774
3775        let path = g.a_star(0, 3, heuristic).expect("path should exist");
3776        assert_eq!(path, vec![0, 1, 2, 3]);
3777    }
3778
3779    #[test]
3780    fn test_astar_admissible_heuristic() {
3781        // Graph with shortcut
3782        // 0 -- 1 -- 2
3783        // |         |
3784        // +----3----+
3785        let g = Graph::from_weighted_edges(
3786            &[(0, 1, 1.0), (1, 2, 1.0), (0, 3, 0.5), (3, 2, 0.5)],
3787            false,
3788        );
3789
3790        // Admissible heuristic (straight-line distance estimate)
3791        let heuristic = |node: usize| match node {
3792            0 | 1 => 1.0, // Estimate to reach 2
3793            3 => 0.5,
3794            _ => 0.0, // At target (2) or other
3795        };
3796
3797        let path = g.a_star(0, 2, heuristic).expect("path should exist");
3798        // Should find shortest path via 3
3799        assert!(path.contains(&3)); // Must use the shortcut
3800    }
3801
3802    #[test]
3803    fn test_astar_directed() {
3804        // Directed graph: 0 -> 1 -> 2
3805        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
3806        let heuristic = |node: usize| (2 - node) as f64;
3807
3808        let path = g
3809            .a_star(0, 2, heuristic)
3810            .expect("forward path should exist");
3811        assert_eq!(path, vec![0, 1, 2]);
3812
3813        // Backward path doesn't exist
3814        assert!(g.a_star(2, 0, |_| 0.0).is_none());
3815    }
3816
3817    #[test]
3818    fn test_astar_triangle() {
3819        // Triangle graph
3820        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
3821        let heuristic = |_: usize| 0.0;
3822
3823        let path = g.a_star(0, 2, heuristic).expect("path should exist");
3824        assert_eq!(path.len(), 2); // Direct edge 0-2
3825        assert_eq!(path[0], 0);
3826        assert_eq!(path[1], 2);
3827    }
3828
3829    #[test]
3830    fn test_astar_weighted_graph() {
3831        // Weighted graph with better heuristic guidance
3832        let g = Graph::from_weighted_edges(&[(0, 1, 1.0), (1, 2, 2.0), (0, 2, 5.0)], false);
3833
3834        // Heuristic guides toward node 2
3835        let heuristic = |node: usize| match node {
3836            0 => 3.0,
3837            1 => 2.0,
3838            _ => 0.0, // At target (2) or other
3839        };
3840
3841        let path = g.a_star(0, 2, heuristic).expect("path should exist");
3842        // Should find path 0->1->2 (cost 3) instead of 0->2 (cost 5)
3843        assert_eq!(path, vec![0, 1, 2]);
3844    }
3845
3846    #[test]
3847    fn test_astar_complete_graph() {
3848        // Complete graph K4
3849        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
3850        let heuristic = |_: usize| 0.0;
3851
3852        // All nodes directly connected
3853        let path = g.a_star(0, 3, heuristic).expect("path should exist");
3854        assert_eq!(path.len(), 2); // Direct path
3855        assert_eq!(path[0], 0);
3856        assert_eq!(path[1], 3);
3857    }
3858
3859    #[test]
3860    fn test_astar_invalid_nodes() {
3861        let g = Graph::from_edges(&[(0, 1)], false);
3862        let heuristic = |_: usize| 0.0;
3863
3864        assert!(g.a_star(0, 10, heuristic).is_none());
3865        assert!(g.a_star(10, 0, heuristic).is_none());
3866    }
3867
3868    #[test]
3869    fn test_astar_vs_shortest_path() {
3870        // On unweighted graph with zero heuristic, A* should match shortest_path
3871        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3872
3873        let sp_path = g
3874            .shortest_path(0, 3)
3875            .expect("shortest_path should find path");
3876        let astar_path = g.a_star(0, 3, |_| 0.0).expect("astar should find path");
3877
3878        assert_eq!(sp_path.len(), astar_path.len());
3879        assert_eq!(sp_path, astar_path);
3880    }
3881
3882    #[test]
3883    fn test_astar_star_graph() {
3884        // Star graph: 0 connected to 1, 2, 3
3885        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3886
3887        // Heuristic that guides toward target 3
3888        let heuristic = |node: usize| if node == 3 { 0.0 } else { 1.0 };
3889
3890        let path = g.a_star(1, 3, heuristic).expect("path should exist");
3891        assert_eq!(path.len(), 3); // Must go through center
3892        assert_eq!(path[0], 1);
3893        assert_eq!(path[1], 0);
3894        assert_eq!(path[2], 3);
3895    }
3896
3897    #[test]
3898    fn test_astar_perfect_heuristic() {
3899        // Perfect heuristic (exact distance to target)
3900        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3901
3902        // Perfect heuristic = exact remaining distance
3903        let heuristic = |node: usize| (3 - node) as f64;
3904
3905        let path = g.a_star(0, 3, heuristic).expect("path should exist");
3906        assert_eq!(path, vec![0, 1, 2, 3]);
3907    }
3908
3909    #[test]
3910    fn test_astar_complex_graph() {
3911        // More complex graph to test heuristic efficiency
3912        //     1
3913        //    / \
3914        //   0   3 - 4
3915        //    \ /
3916        //     2
3917        let g = Graph::from_weighted_edges(
3918            &[
3919                (0, 1, 1.0),
3920                (0, 2, 1.0),
3921                (1, 3, 1.0),
3922                (2, 3, 1.0),
3923                (3, 4, 1.0),
3924            ],
3925            false,
3926        );
3927
3928        // Distance-based heuristic
3929        let heuristic = |node: usize| (4 - node) as f64;
3930
3931        let path = g.a_star(0, 4, heuristic).expect("path should exist");
3932        assert_eq!(path.len(), 4); // 0->1->3->4 or 0->2->3->4
3933        assert_eq!(path[0], 0);
3934        assert_eq!(path[3], 4);
3935    }
3936
3937    // DFS Tests
3938
3939    #[test]
3940    fn test_dfs_linear_chain() {
3941        // Linear chain: 0 -- 1 -- 2 -- 3
3942        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
3943        let visited = g.dfs(0).expect("valid source");
3944
3945        assert_eq!(visited.len(), 4);
3946        assert_eq!(visited[0], 0); // Starts at source
3947        assert!(visited.contains(&1));
3948        assert!(visited.contains(&2));
3949        assert!(visited.contains(&3));
3950    }
3951
3952    #[test]
3953    fn test_dfs_tree() {
3954        // Tree: 0 connected to 1, 2, 3
3955        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
3956        let visited = g.dfs(0).expect("valid source");
3957
3958        assert_eq!(visited.len(), 4);
3959        assert_eq!(visited[0], 0); // Root first
3960                                   // Children visited in some order
3961        assert!(visited.contains(&1));
3962        assert!(visited.contains(&2));
3963        assert!(visited.contains(&3));
3964    }
3965
3966    #[test]
3967    fn test_dfs_cycle() {
3968        // Cycle: 0 -- 1 -- 2 -- 0
3969        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
3970        let visited = g.dfs(0).expect("valid source");
3971
3972        assert_eq!(visited.len(), 3);
3973        assert_eq!(visited[0], 0);
3974        assert!(visited.contains(&1));
3975        assert!(visited.contains(&2));
3976    }
3977
3978    #[test]
3979    fn test_dfs_disconnected() {
3980        // Two components: (0, 1) and (2, 3)
3981        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
3982
3983        // DFS from 0 only visits component containing 0
3984        let visited = g.dfs(0).expect("valid source");
3985        assert_eq!(visited.len(), 2);
3986        assert!(visited.contains(&0));
3987        assert!(visited.contains(&1));
3988        assert!(!visited.contains(&2));
3989        assert!(!visited.contains(&3));
3990
3991        // DFS from 2 only visits component containing 2
3992        let visited2 = g.dfs(2).expect("valid source");
3993        assert_eq!(visited2.len(), 2);
3994        assert!(visited2.contains(&2));
3995        assert!(visited2.contains(&3));
3996    }
3997
3998    #[test]
3999    fn test_dfs_directed() {
4000        // Directed: 0 -> 1 -> 2
4001        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
4002
4003        // Forward traversal
4004        let visited = g.dfs(0).expect("valid source");
4005        assert_eq!(visited.len(), 3);
4006        assert_eq!(visited[0], 0);
4007
4008        // Backward traversal (node 2 has no outgoing edges)
4009        let visited2 = g.dfs(2).expect("valid source");
4010        assert_eq!(visited2.len(), 1);
4011        assert_eq!(visited2[0], 2);
4012    }
4013
4014    #[test]
4015    fn test_dfs_single_node() {
4016        // Single node with self-loop
4017        let g = Graph::from_edges(&[(0, 0)], false);
4018
4019        let visited = g.dfs(0).expect("valid source");
4020        assert_eq!(visited.len(), 1);
4021        assert_eq!(visited[0], 0);
4022    }
4023
4024    #[test]
4025    fn test_dfs_invalid_source() {
4026        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
4027
4028        // Invalid source node
4029        assert!(g.dfs(10).is_none());
4030        assert!(g.dfs(100).is_none());
4031    }
4032
4033    #[test]
4034    fn test_dfs_complete_graph() {
4035        // Complete graph K4
4036        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
4037        let visited = g.dfs(0).expect("valid source");
4038
4039        assert_eq!(visited.len(), 4);
4040        assert_eq!(visited[0], 0);
4041        // All other nodes reachable
4042        assert!(visited.contains(&1));
4043        assert!(visited.contains(&2));
4044        assert!(visited.contains(&3));
4045    }
4046
4047    #[test]
4048    fn test_dfs_dag() {
4049        // DAG: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
4050        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (2, 3)], true);
4051        let visited = g.dfs(0).expect("valid source");
4052
4053        assert_eq!(visited.len(), 4);
4054        assert_eq!(visited[0], 0);
4055        // All nodes reachable from 0
4056        assert!(visited.contains(&1));
4057        assert!(visited.contains(&2));
4058        assert!(visited.contains(&3));
4059
4060        // Node 3 is a sink (no outgoing edges)
4061        let visited3 = g.dfs(3).expect("valid source");
4062        assert_eq!(visited3.len(), 1);
4063        assert_eq!(visited3[0], 3);
4064    }
4065
4066    #[test]
4067    fn test_dfs_empty_graph() {
4068        let g = Graph::new(false);
4069        // No nodes, so any DFS should return None
4070        assert!(g.dfs(0).is_none());
4071    }
4072
4073    // Connected Components Tests
4074
4075    #[test]
4076    fn test_connected_components_single() {
4077        // Single connected component
4078        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
4079        let components = g.connected_components();
4080
4081        assert_eq!(components.len(), 4);
4082        // All nodes in same component
4083        assert_eq!(components[0], components[1]);
4084        assert_eq!(components[1], components[2]);
4085        assert_eq!(components[2], components[3]);
4086    }
4087
4088    #[test]
4089    fn test_connected_components_two() {
4090        // Two disconnected components: (0,1) and (2,3)
4091        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
4092        let components = g.connected_components();
4093
4094        assert_eq!(components.len(), 4);
4095        // Component 1: nodes 0 and 1
4096        assert_eq!(components[0], components[1]);
4097        // Component 2: nodes 2 and 3
4098        assert_eq!(components[2], components[3]);
4099        // Different components
4100        assert_ne!(components[0], components[2]);
4101    }
4102
4103    #[test]
4104    fn test_connected_components_three() {
4105        // Three components: (0,1), (2,3), (4)
4106        let g = Graph::from_edges(&[(0, 1), (2, 3), (4, 4)], false);
4107        let components = g.connected_components();
4108
4109        assert_eq!(components.len(), 5);
4110        // Three distinct components
4111        assert_eq!(components[0], components[1]);
4112        assert_eq!(components[2], components[3]);
4113        assert_ne!(components[0], components[2]);
4114        assert_ne!(components[0], components[4]);
4115        assert_ne!(components[2], components[4]);
4116    }
4117
4118    #[test]
4119    fn test_connected_components_complete() {
4120        // Complete graph K4 - all in one component
4121        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
4122        let components = g.connected_components();
4123
4124        assert_eq!(components.len(), 4);
4125        let first = components[0];
4126        assert!(components.iter().all(|&c| c == first));
4127    }
4128
4129    #[test]
4130    fn test_connected_components_star() {
4131        // Star graph - all connected through center
4132        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
4133        let components = g.connected_components();
4134
4135        assert_eq!(components.len(), 4);
4136        // All in same component
4137        assert_eq!(components[0], components[1]);
4138        assert_eq!(components[0], components[2]);
4139        assert_eq!(components[0], components[3]);
4140    }
4141
4142    #[test]
4143    fn test_connected_components_directed_weak() {
4144        // Directed graph: 0 -> 1 -> 2 (weakly connected)
4145        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
4146        let components = g.connected_components();
4147
4148        assert_eq!(components.len(), 3);
4149        // Weakly connected (ignores direction)
4150        assert_eq!(components[0], components[1]);
4151        assert_eq!(components[1], components[2]);
4152    }
4153
4154    #[test]
4155    fn test_connected_components_cycle() {
4156        // Cycle graph
4157        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
4158        let components = g.connected_components();
4159
4160        assert_eq!(components.len(), 3);
4161        // All in same component
4162        assert_eq!(components[0], components[1]);
4163        assert_eq!(components[1], components[2]);
4164    }
4165
4166    #[test]
4167    fn test_connected_components_empty() {
4168        let g = Graph::new(false);
4169        let components = g.connected_components();
4170        assert!(components.is_empty());
4171    }
4172
4173    #[test]
4174    fn test_connected_components_isolated_nodes() {
4175        // Graph with some isolated nodes
4176        let g = Graph::from_edges(&[(0, 1), (3, 4)], false);
4177        // Node 2 is isolated (no edges)
4178        // But we only have nodes that appear in edges
4179        let components = g.connected_components();
4180
4181        assert_eq!(components.len(), 5);
4182        // Two components: (0,1) and (3,4), and isolated 2
4183        assert_eq!(components[0], components[1]);
4184        assert_eq!(components[3], components[4]);
4185        assert_ne!(components[0], components[3]);
4186        // Node 2 is in its own component
4187        assert_ne!(components[2], components[0]);
4188        assert_ne!(components[2], components[3]);
4189    }
4190
4191    #[test]
4192    fn test_connected_components_count() {
4193        // Helper to count unique components
4194        fn count_components(components: &[usize]) -> usize {
4195            use std::collections::HashSet;
4196            components.iter().copied().collect::<HashSet<_>>().len()
4197        }
4198
4199        // Single component
4200        let g1 = Graph::from_edges(&[(0, 1), (1, 2)], false);
4201        assert_eq!(count_components(&g1.connected_components()), 1);
4202
4203        // Two components
4204        let g2 = Graph::from_edges(&[(0, 1), (2, 3)], false);
4205        assert_eq!(count_components(&g2.connected_components()), 2);
4206
4207        // Three components
4208        let g3 = Graph::from_edges(&[(0, 1), (2, 3), (4, 5)], false);
4209        assert_eq!(count_components(&g3.connected_components()), 3);
4210    }
4211
4212    // Strongly Connected Components Tests
4213
4214    #[test]
4215    fn test_scc_single_cycle() {
4216        // Single SCC: 0 -> 1 -> 2 -> 0
4217        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
4218        let sccs = g.strongly_connected_components();
4219
4220        assert_eq!(sccs.len(), 3);
4221        // All nodes in same SCC
4222        assert_eq!(sccs[0], sccs[1]);
4223        assert_eq!(sccs[1], sccs[2]);
4224    }
4225
4226    #[test]
4227    fn test_scc_dag() {
4228        // DAG: 0 -> 1 -> 2 (each node is its own SCC)
4229        let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
4230        let sccs = g.strongly_connected_components();
4231
4232        assert_eq!(sccs.len(), 3);
4233        // Each node is its own SCC
4234        assert_ne!(sccs[0], sccs[1]);
4235        assert_ne!(sccs[1], sccs[2]);
4236        assert_ne!(sccs[0], sccs[2]);
4237    }
4238
4239    #[test]
4240    fn test_scc_two_components() {
4241        // Two SCCs: (0->1->0) and (2->3->2)
4242        let g = Graph::from_edges(&[(0, 1), (1, 0), (2, 3), (3, 2)], true);
4243        let sccs = g.strongly_connected_components();
4244
4245        assert_eq!(sccs.len(), 4);
4246        // SCC 1: nodes 0 and 1
4247        assert_eq!(sccs[0], sccs[1]);
4248        // SCC 2: nodes 2 and 3
4249        assert_eq!(sccs[2], sccs[3]);
4250        // Different SCCs
4251        assert_ne!(sccs[0], sccs[2]);
4252    }
4253
4254    #[test]
4255    fn test_scc_complex() {
4256        // Complex graph with multiple SCCs
4257        // SCC 1: 0 -> 1 -> 0
4258        // SCC 2: 2 -> 3 -> 4 -> 2
4259        // Edge from SCC1 to SCC2: 1 -> 2
4260        let g = Graph::from_edges(&[(0, 1), (1, 0), (1, 2), (2, 3), (3, 4), (4, 2)], true);
4261        let sccs = g.strongly_connected_components();
4262
4263        assert_eq!(sccs.len(), 5);
4264        // SCC 1: nodes 0 and 1
4265        assert_eq!(sccs[0], sccs[1]);
4266        // SCC 2: nodes 2, 3, 4
4267        assert_eq!(sccs[2], sccs[3]);
4268        assert_eq!(sccs[3], sccs[4]);
4269        // Different SCCs
4270        assert_ne!(sccs[0], sccs[2]);
4271    }
4272
4273    #[test]
4274    fn test_scc_self_loop() {
4275        // Single node with self-loop is an SCC
4276        let g = Graph::from_edges(&[(0, 0)], true);
4277        let sccs = g.strongly_connected_components();
4278
4279        assert_eq!(sccs.len(), 1);
4280        assert_eq!(sccs[0], 0);
4281    }
4282
4283    #[test]
4284    fn test_scc_disconnected() {
4285        // Two disconnected cycles
4286        let g = Graph::from_edges(&[(0, 1), (1, 0), (2, 3), (3, 2)], true);
4287        let sccs = g.strongly_connected_components();
4288
4289        assert_eq!(sccs.len(), 4);
4290        // Two separate SCCs
4291        assert_eq!(sccs[0], sccs[1]);
4292        assert_eq!(sccs[2], sccs[3]);
4293        assert_ne!(sccs[0], sccs[2]);
4294    }
4295
4296    #[test]
4297    fn test_scc_empty() {
4298        let g = Graph::new(true);
4299        let sccs = g.strongly_connected_components();
4300        assert!(sccs.is_empty());
4301    }
4302
4303    #[test]
4304    fn test_scc_linear_dag() {
4305        // Linear DAG: 0 -> 1 -> 2 -> 3
4306        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], true);
4307        let sccs = g.strongly_connected_components();
4308
4309        assert_eq!(sccs.len(), 4);
4310        // Each node is its own SCC in a DAG
4311        use std::collections::HashSet;
4312        let unique_sccs: HashSet<_> = sccs.iter().copied().collect();
4313        assert_eq!(unique_sccs.len(), 4);
4314    }
4315
4316    #[test]
4317    fn test_scc_complete_graph() {
4318        // Complete directed graph (all nodes reachable from all)
4319        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)], true);
4320        let sccs = g.strongly_connected_components();
4321
4322        assert_eq!(sccs.len(), 3);
4323        // All in same SCC
4324        assert_eq!(sccs[0], sccs[1]);
4325        assert_eq!(sccs[1], sccs[2]);
4326    }
4327
4328    #[test]
4329    fn test_scc_count() {
4330        // Helper to count unique SCCs
4331        fn count_sccs(sccs: &[usize]) -> usize {
4332            use std::collections::HashSet;
4333            sccs.iter().copied().collect::<HashSet<_>>().len()
4334        }
4335
4336        // Single SCC
4337        let g1 = Graph::from_edges(&[(0, 1), (1, 0)], true);
4338        assert_eq!(count_sccs(&g1.strongly_connected_components()), 1);
4339
4340        // Two SCCs
4341        let g2 = Graph::from_edges(&[(0, 1)], true);
4342        assert_eq!(count_sccs(&g2.strongly_connected_components()), 2);
4343
4344        // Three SCCs
4345        let g3 = Graph::from_edges(&[(0, 1), (2, 3), (3, 2)], true);
4346        assert_eq!(count_sccs(&g3.strongly_connected_components()), 3);
4347    }
4348
4349    // Topological Sort Tests
4350
4351    #[test]
4352    fn test_topo_linear_dag() {
4353        // Linear DAG: 0 -> 1 -> 2 -> 3
4354        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], true);
4355        let order = g
4356            .topological_sort()
4357            .expect("DAG should have topological order");
4358
4359        assert_eq!(order.len(), 4);
4360        // Check ordering constraints
4361        assert!(order.iter().position(|&x| x == 0) < order.iter().position(|&x| x == 1));
4362        assert!(order.iter().position(|&x| x == 1) < order.iter().position(|&x| x == 2));
4363        assert!(order.iter().position(|&x| x == 2) < order.iter().position(|&x| x == 3));
4364    }
4365
4366    #[test]
4367    fn test_topo_cycle() {
4368        // Cycle: 0 -> 1 -> 2 -> 0
4369        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
4370        assert!(g.topological_sort().is_none()); // Should detect cycle
4371    }
4372
4373    #[test]
4374    fn test_topo_diamond() {
4375        // Diamond DAG: 0 -> {1,2} -> 3
4376        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (2, 3)], true);
4377        let order = g
4378            .topological_sort()
4379            .expect("DAG should have topological order");
4380
4381        assert_eq!(order.len(), 4);
4382        // 0 must come before 1, 2, 3
4383        let pos_0 = order
4384            .iter()
4385            .position(|&x| x == 0)
4386            .expect("0 should be in order");
4387        assert!(
4388            pos_0
4389                < order
4390                    .iter()
4391                    .position(|&x| x == 1)
4392                    .expect("1 should be in order")
4393        );
4394        assert!(
4395            pos_0
4396                < order
4397                    .iter()
4398                    .position(|&x| x == 2)
4399                    .expect("2 should be in order")
4400        );
4401        assert!(
4402            pos_0
4403                < order
4404                    .iter()
4405                    .position(|&x| x == 3)
4406                    .expect("3 should be in order")
4407        );
4408
4409        // 3 must come after 1 and 2
4410        let pos_3 = order
4411            .iter()
4412            .position(|&x| x == 3)
4413            .expect("3 should be in order");
4414        assert!(
4415            order
4416                .iter()
4417                .position(|&x| x == 1)
4418                .expect("1 should be in order")
4419                < pos_3
4420        );
4421        assert!(
4422            order
4423                .iter()
4424                .position(|&x| x == 2)
4425                .expect("2 should be in order")
4426                < pos_3
4427        );
4428    }
4429
4430    #[test]
4431    fn test_topo_empty() {
4432        let g = Graph::new(true);
4433        let order = g
4434            .topological_sort()
4435            .expect("Empty graph has topological order");
4436        assert!(order.is_empty());
4437    }
4438
4439    #[test]
4440    fn test_topo_single_node() {
4441        // Single node with self-loop creates cycle
4442        let g = Graph::from_edges(&[(0, 0)], true);
4443        assert!(g.topological_sort().is_none()); // Self-loop is a cycle
4444    }
4445
4446    #[test]
4447    fn test_topo_disconnected_dag() {
4448        // Two disconnected chains: 0->1 and 2->3
4449        let g = Graph::from_edges(&[(0, 1), (2, 3)], true);
4450        let order = g
4451            .topological_sort()
4452            .expect("Disconnected DAG has topological order");
4453
4454        assert_eq!(order.len(), 4);
4455        // Within each chain, ordering is preserved
4456        assert!(order.iter().position(|&x| x == 0) < order.iter().position(|&x| x == 1));
4457        assert!(order.iter().position(|&x| x == 2) < order.iter().position(|&x| x == 3));
4458    }
4459
4460    #[test]
4461    fn test_topo_tree() {
4462        // Tree: 0 -> {1, 2}, 1 -> {3, 4}
4463        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (1, 4)], true);
4464        let order = g.topological_sort().expect("Tree is a DAG");
4465
4466        assert_eq!(order.len(), 5);
4467        // 0 must come first
4468        assert_eq!(order.iter().position(|&x| x == 0), Some(0));
4469        // 1 before 3 and 4
4470        let pos_1 = order
4471            .iter()
4472            .position(|&x| x == 1)
4473            .expect("1 should be in order");
4474        assert!(
4475            pos_1
4476                < order
4477                    .iter()
4478                    .position(|&x| x == 3)
4479                    .expect("3 should be in order")
4480        );
4481        assert!(
4482            pos_1
4483                < order
4484                    .iter()
4485                    .position(|&x| x == 4)
4486                    .expect("4 should be in order")
4487        );
4488    }
4489
4490    #[test]
4491    fn test_topo_self_loop() {
4492        // Self-loop is a cycle
4493        let g = Graph::from_edges(&[(0, 0)], true);
4494        assert!(g.topological_sort().is_none());
4495    }
4496
4497    #[test]
4498    fn test_topo_complete_dag() {
4499        // Complete DAG: 0 -> {1,2,3}, 1 -> {2,3}, 2 -> 3
4500        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], true);
4501        let order = g
4502            .topological_sort()
4503            .expect("Complete DAG has topological order");
4504
4505        assert_eq!(order.len(), 4);
4506        // Check all ordering constraints
4507        let positions: Vec<_> = (0..4)
4508            .map(|i| {
4509                order
4510                    .iter()
4511                    .position(|&x| x == i)
4512                    .expect("node should be in order")
4513            })
4514            .collect();
4515
4516        assert!(positions[0] < positions[1]);
4517        assert!(positions[0] < positions[2]);
4518        assert!(positions[0] < positions[3]);
4519        assert!(positions[1] < positions[2]);
4520        assert!(positions[1] < positions[3]);
4521        assert!(positions[2] < positions[3]);
4522    }
4523
4524    #[test]
4525    fn test_topo_undirected() {
4526        // Undirected graph is treated as bidirectional (always has cycles unless tree)
4527        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
4528        // Undirected edges create cycles (0->1 and 1->0)
4529        assert!(g.topological_sort().is_none());
4530    }
4531
4532    // Common Neighbors Tests
4533
4534    #[test]
4535    fn test_common_neighbors_triangle() {
4536        // Triangle: 0-1, 0-2, 1-2
4537        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], false);
4538
4539        // Nodes 1 and 2 share neighbor 0
4540        assert_eq!(g.common_neighbors(1, 2), Some(1));
4541        // Nodes 0 and 1 share neighbor 2
4542        assert_eq!(g.common_neighbors(0, 1), Some(1));
4543        // Nodes 0 and 2 share neighbor 1
4544        assert_eq!(g.common_neighbors(0, 2), Some(1));
4545    }
4546
4547    #[test]
4548    fn test_common_neighbors_no_overlap() {
4549        // Two stars: 0-{1,2}, 3-{4,5}
4550        let g = Graph::from_edges(&[(0, 1), (0, 2), (3, 4), (3, 5)], false);
4551
4552        // Nodes 1 and 2 share neighbor 0
4553        assert_eq!(g.common_neighbors(1, 2), Some(1));
4554        // Nodes from different components have no common neighbors
4555        assert_eq!(g.common_neighbors(1, 4), Some(0));
4556        assert_eq!(g.common_neighbors(0, 3), Some(0));
4557    }
4558
4559    #[test]
4560    fn test_common_neighbors_complete() {
4561        // Complete graph K4
4562        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
4563
4564        // Any two nodes share 2 common neighbors
4565        assert_eq!(g.common_neighbors(0, 1), Some(2)); // Share 2, 3
4566        assert_eq!(g.common_neighbors(0, 2), Some(2)); // Share 1, 3
4567        assert_eq!(g.common_neighbors(1, 2), Some(2)); // Share 0, 3
4568    }
4569
4570    #[test]
4571    fn test_common_neighbors_directed() {
4572        // Directed: 0->1, 0->2, 1->2
4573        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], true);
4574
4575        // 0 has out-neighbors {1, 2}
4576        // 1 has out-neighbors {2}
4577        assert_eq!(g.common_neighbors(0, 1), Some(1)); // Share out-neighbor 2
4578    }
4579
4580    #[test]
4581    fn test_common_neighbors_invalid() {
4582        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
4583
4584        // Invalid nodes
4585        assert!(g.common_neighbors(0, 10).is_none());
4586        assert!(g.common_neighbors(10, 0).is_none());
4587        assert!(g.common_neighbors(10, 20).is_none());
4588    }
4589
4590    #[test]
4591    fn test_common_neighbors_self() {
4592        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], false);
4593
4594        // Node with itself shares all neighbors
4595        assert_eq!(g.common_neighbors(0, 0), Some(2)); // Shares {1, 2}
4596    }
4597
4598    #[test]
4599    fn test_common_neighbors_empty() {
4600        let g = Graph::new(false);
4601        assert!(g.common_neighbors(0, 1).is_none());
4602    }
4603
4604    #[test]
4605    fn test_common_neighbors_star() {
4606        // Star: 0 connected to {1, 2, 3, 4}
4607        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false);
4608
4609        // Leaves share center as common neighbor
4610        assert_eq!(g.common_neighbors(1, 2), Some(1)); // Share 0
4611        assert_eq!(g.common_neighbors(1, 3), Some(1)); // Share 0
4612        assert_eq!(g.common_neighbors(2, 4), Some(1)); // Share 0
4613    }
4614
4615    // Adamic-Adar Index Tests
4616
4617    #[test]
4618    fn test_adamic_adar_triangle() {
4619        // Triangle: 0-1, 0-2, 1-2
4620        let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], false);
4621
4622        // Nodes 1 and 2 share neighbor 0 (degree 2)
4623        let aa = g.adamic_adar_index(1, 2).expect("valid nodes");
4624        // Score should be 1/ln(2) ≈ 1.44
4625        assert!((aa - 1.0 / 2.0_f64.ln()).abs() < 1e-10);
4626    }
4627
4628    #[test]
4629    fn test_adamic_adar_no_common() {
4630        // Two disconnected edges
4631        let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
4632
4633        // No common neighbors
4634        assert_eq!(g.adamic_adar_index(0, 2).expect("valid nodes"), 0.0);
4635        assert_eq!(g.adamic_adar_index(1, 3).expect("valid nodes"), 0.0);
4636    }
4637
4638    #[test]
4639    fn test_adamic_adar_star() {
4640        // Star: 0-{1,2,3,4}
4641        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false);
4642
4643        // Leaves share center (degree 4)
4644        let aa = g.adamic_adar_index(1, 2).expect("valid nodes");
4645        // Score should be 1/ln(4) ≈ 0.72
4646        assert!((aa - 1.0 / 4.0_f64.ln()).abs() < 1e-10);
4647    }
4648
4649    #[test]
4650    fn test_adamic_adar_multiple_common() {
4651        // Graph where nodes 0 and 1 share multiple neighbors
4652        // 0-{2,3,4}, 1-{2,3,4}
4653        let g = Graph::from_edges(&[(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)], false);
4654
4655        let aa = g.adamic_adar_index(0, 1).expect("valid nodes");
4656        // Three common neighbors (2, 3, 4), each with degree 2
4657        // Score = 3 * 1/ln(2) = 3/ln(2) ≈ 4.33
4658        let expected = 3.0 / 2.0_f64.ln();
4659        assert!((aa - expected).abs() < 1e-10);
4660    }
4661
4662    #[test]
4663    fn test_adamic_adar_degree_one() {
4664        // Linear chain: 0-1-2
4665        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
4666
4667        // Nodes 0 and 2 share neighbor 1 (degree 2)
4668        let aa = g.adamic_adar_index(0, 2).expect("valid nodes");
4669        assert!((aa - 1.0 / 2.0_f64.ln()).abs() < 1e-10);
4670    }
4671
4672    #[test]
4673    fn test_adamic_adar_invalid() {
4674        let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
4675
4676        assert!(g.adamic_adar_index(0, 10).is_none());
4677        assert!(g.adamic_adar_index(10, 0).is_none());
4678    }
4679
4680    #[test]
4681    fn test_adamic_adar_directed() {
4682        // Directed: 0->2, 1->2, 2->3
4683        let g = Graph::from_edges(&[(0, 2), (1, 2), (2, 3)], true);
4684
4685        // 0 and 1 both point to 2 (share out-neighbor)
4686        let aa = g.adamic_adar_index(0, 1).expect("valid nodes");
4687        // Node 2 has degree 1 (out-degree), but we use total neighbor count
4688        // which is 2 (1 in + 1 out in directed graph becomes bidirectional in CSR)
4689        // Actually in CSR for directed graphs, degree is out-degree
4690        assert!(aa >= 0.0);
4691    }
4692
4693    #[test]
4694    fn test_adamic_adar_empty() {
4695        let g = Graph::new(false);
4696        assert!(g.adamic_adar_index(0, 1).is_none());
4697    }
4698
4699    // Label Propagation Tests
4700
4701    #[test]
4702    fn test_label_propagation_two_triangles() {
4703        // Two triangles connected by single edge
4704        // Triangle 1: 0-1-2-0, Triangle 2: 3-4-5-3, Bridge: 2-3
4705        let g = Graph::from_edges(
4706            &[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)],
4707            false,
4708        );
4709
4710        let communities = g.label_propagation(100, Some(42));
4711
4712        // Nodes within same triangle should likely have same label
4713        // This is probabilistic, but with seed should be deterministic
4714        assert_eq!(communities.len(), 6);
4715
4716        // Count unique communities
4717        use std::collections::HashSet;
4718        let unique: HashSet<_> = communities.iter().copied().collect();
4719        // Should have 2-3 communities depending on convergence
4720        assert!(!unique.is_empty() && unique.len() <= 6);
4721    }
4722
4723    #[test]
4724    fn test_label_propagation_complete_graph() {
4725        // Complete graph K4 - all nodes fully connected
4726        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
4727
4728        let communities = g.label_propagation(100, Some(42));
4729
4730        assert_eq!(communities.len(), 4);
4731        // In complete graph, all nodes should converge to same label
4732        let first_label = communities[0];
4733        assert!(communities.iter().all(|&c| c == first_label));
4734    }
4735
4736    #[test]
4737    fn test_label_propagation_disconnected() {
4738        // Two disconnected triangles
4739        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)], false);
4740
4741        let communities = g.label_propagation(100, Some(42));
4742
4743        assert_eq!(communities.len(), 6);
4744
4745        // Each triangle should form its own community
4746        // Nodes 0, 1, 2 should have same label
4747        assert_eq!(communities[0], communities[1]);
4748        assert_eq!(communities[1], communities[2]);
4749
4750        // Nodes 3, 4, 5 should have same label
4751        assert_eq!(communities[3], communities[4]);
4752        assert_eq!(communities[4], communities[5]);
4753
4754        // Different triangles should have different labels
4755        assert_ne!(communities[0], communities[3]);
4756    }
4757
4758    #[test]
4759    fn test_label_propagation_star() {
4760        // Star graph: center connected to all leaves
4761        let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false);
4762
4763        let communities = g.label_propagation(100, Some(42));
4764
4765        assert_eq!(communities.len(), 5);
4766        // All nodes should eventually have same label
4767        // (leaves adopt center's label or center adopts majority)
4768        use std::collections::HashSet;
4769        let unique: HashSet<_> = communities.iter().copied().collect();
4770        assert_eq!(unique.len(), 1);
4771    }
4772
4773    #[test]
4774    fn test_label_propagation_linear() {
4775        // Linear chain: 0-1-2-3-4
4776        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false);
4777
4778        let communities = g.label_propagation(100, Some(42));
4779
4780        assert_eq!(communities.len(), 5);
4781        // Linear graph may converge to 1-2 communities
4782        use std::collections::HashSet;
4783        let unique: HashSet<_> = communities.iter().copied().collect();
4784        assert!(!unique.is_empty() && unique.len() <= 5);
4785    }
4786
4787    #[test]
4788    fn test_label_propagation_empty() {
4789        let g = Graph::new(false);
4790        let communities = g.label_propagation(100, Some(42));
4791        assert!(communities.is_empty());
4792    }
4793
4794    #[test]
4795    fn test_label_propagation_single_node() {
4796        // Single isolated node (created via self-loop)
4797        let g = Graph::from_edges(&[(0, 0)], false);
4798        let communities = g.label_propagation(100, Some(42));
4799
4800        assert_eq!(communities.len(), 1);
4801        assert_eq!(communities[0], 0); // Keeps its own label
4802    }
4803
4804    #[test]
4805    fn test_label_propagation_convergence() {
4806        // Small graph that should converge quickly
4807        let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
4808
4809        let communities = g.label_propagation(100, Some(42));
4810
4811        assert_eq!(communities.len(), 3);
4812        // Triangle should converge to single community
4813        assert_eq!(communities[0], communities[1]);
4814        assert_eq!(communities[1], communities[2]);
4815    }
4816
4817    #[test]
4818    fn test_label_propagation_directed() {
4819        // Directed graph with mutual edges forms strongly connected component
4820        // 0<->1<->2 (bidirectional edges)
4821        let g = Graph::from_edges(&[(0, 1), (1, 0), (1, 2), (2, 1), (0, 2), (2, 0)], true);
4822
4823        let communities = g.label_propagation(100, Some(42));
4824
4825        assert_eq!(communities.len(), 3);
4826        // Strongly connected component should form single community
4827        assert_eq!(communities[0], communities[1]);
4828        assert_eq!(communities[1], communities[2]);
4829    }
4830
4831    #[test]
4832    fn test_label_propagation_barbell() {
4833        // Barbell graph: two cliques connected by bridge
4834        // Clique 1: 0-1-2 (complete), Clique 2: 3-4-5 (complete), Bridge: 2-3
4835        let g = Graph::from_edges(
4836            &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
4837            false,
4838        );
4839
4840        let communities = g.label_propagation(100, Some(42));
4841
4842        assert_eq!(communities.len(), 6);
4843
4844        // Should detect 1-2 communities
4845        use std::collections::HashSet;
4846        let unique: HashSet<_> = communities.iter().copied().collect();
4847        assert!(!unique.is_empty() && unique.len() <= 3);
4848    }
4849}