scirs2_graph/algorithms/community/
louvain.rs

1//! Louvain method for community detection
2
3use super::types::{CommunityResult, CommunityStructure};
4use crate::base::{EdgeWeight, Graph, Node};
5use petgraph::visit::EdgeRef;
6use std::collections::HashMap;
7use std::hash::Hash;
8
9/// Detects communities in a graph using the Louvain method (modern API)
10///
11/// This function returns the standardized `CommunityResult` type that provides
12/// multiple ways to access the community structure.
13///
14/// # Arguments
15/// * `graph` - The undirected graph to analyze
16///
17/// # Returns
18/// * A `CommunityResult` with comprehensive community information
19///
20/// # Time Complexity
21/// O(m * log n) where m is the number of edges and n is the number of nodes
22/// in typical cases. Can be O(m * n) in worst case with many iterations.
23/// The algorithm usually converges quickly in practice.
24///
25/// # Space Complexity
26/// O(n) for storing community assignments and node degrees.
27///
28/// # Example
29/// ```rust
30/// use scirs2_graph::{Graph, louvain_communities_result};
31///
32/// let mut graph: Graph<i32, f64> = Graph::new();
33/// // ... add nodes and edges ...
34/// let result = louvain_communities_result(&graph);
35///
36/// println!("Found {} communities", result.num_communities);
37/// for (i, community) in result.communities.iter().enumerate() {
38///     println!("Community {}: {} members", i, community.len());
39/// }
40/// ```
41#[allow(dead_code)]
42pub fn louvain_communities_result<N, E, Ix>(graph: &Graph<N, E, Ix>) -> CommunityResult<N>
43where
44    N: Node + Clone + Hash + Eq + std::fmt::Debug,
45    E: EdgeWeight + Into<f64> + scirs2_core::numeric::Zero + Copy,
46    Ix: petgraph::graph::IndexType,
47{
48    let structure = louvain_communities_legacy(graph);
49    CommunityResult::from_community_structure(structure)
50}
51
52/// Detects communities in a graph using the Louvain method (legacy API)
53///
54/// **Note**: This function is deprecated in favor of `louvain_communities_result`.
55/// It will be removed in version 2.0.
56#[deprecated(
57    since = "0.1.0-beta.1",
58    note = "Use `louvain_communities_result` instead"
59)]
60#[allow(dead_code)]
61pub fn louvain_communities<N, E, Ix>(graph: &Graph<N, E, Ix>) -> CommunityStructure<N>
62where
63    N: Node + std::fmt::Debug,
64    E: EdgeWeight + Into<f64> + scirs2_core::numeric::Zero + Copy,
65    Ix: petgraph::graph::IndexType,
66{
67    louvain_communities_legacy(graph)
68}
69
70/// Internal implementation of Louvain method
71#[allow(dead_code)]
72fn louvain_communities_legacy<N, E, Ix>(graph: &Graph<N, E, Ix>) -> CommunityStructure<N>
73where
74    N: Node + std::fmt::Debug,
75    E: EdgeWeight + Into<f64> + scirs2_core::numeric::Zero + Copy,
76    Ix: petgraph::graph::IndexType,
77{
78    let n = graph.node_count();
79    if n == 0 {
80        return CommunityStructure {
81            node_communities: HashMap::new(),
82            modularity: 0.0,
83        };
84    }
85
86    // Initialize each node in its own community
87    let mut communities: HashMap<petgraph::graph::NodeIndex<Ix>, usize> = HashMap::new();
88    let mut node_degrees: HashMap<petgraph::graph::NodeIndex<Ix>, f64> = HashMap::new();
89
90    // Calculate node degrees and total weight
91    let mut m = 0.0; // Total weight of edges (sum of all edge weights)
92    for edge in graph.inner().edge_references() {
93        m += (*edge.weight()).into();
94    }
95
96    // Handle edge case
97    if m == 0.0 {
98        m = 1.0;
99    }
100
101    // Calculate node degrees
102    for node_idx in graph.inner().node_indices() {
103        let mut degree = 0.0;
104        for edge in graph.inner().edges(node_idx) {
105            degree += (*edge.weight()).into();
106        }
107        node_degrees.insert(node_idx, degree);
108        communities.insert(node_idx, node_idx.index());
109    }
110
111    // Optimization phase
112    let mut improved = true;
113    let mut iterations = 0;
114    const MAX_ITERATIONS: usize = 100;
115
116    while improved && iterations < MAX_ITERATIONS {
117        improved = false;
118        iterations += 1;
119
120        // For each node, try to find a better community
121        for node_idx in graph.inner().node_indices() {
122            let current_community = communities[&node_idx];
123            let k_i = node_degrees[&node_idx]; // Degree of node i
124
125            // Remove node from its community first
126            communities.insert(node_idx, node_idx.index());
127
128            // Calculate sum of weights to each neighboring community
129            let mut community_weights: HashMap<usize, f64> = HashMap::new();
130            for edge in graph.inner().edges(node_idx) {
131                let neighbor_idx = edge.target();
132                let neighbor_community = communities[&neighbor_idx];
133                let edge_weight: f64 = (*edge.weight()).into();
134                *community_weights.entry(neighbor_community).or_insert(0.0) += edge_weight;
135            }
136
137            // Add current node as a possible community
138            community_weights.entry(node_idx.index()).or_insert(0.0);
139
140            // Find best community
141            let mut best_community = node_idx.index();
142            let mut best_delta_q = 0.0;
143
144            for (&community, &k_i_in) in &community_weights {
145                // Calculate sum of degrees of nodes in this community
146                let mut sigma_tot = 0.0;
147                for (&other_node, &other_community) in &communities {
148                    if other_community == community && other_node != node_idx {
149                        sigma_tot += node_degrees[&other_node];
150                    }
151                }
152
153                // Calculate modularity gain
154                let delta_q = k_i_in / m - (sigma_tot * k_i) / (2.0 * m * m);
155
156                if delta_q > best_delta_q {
157                    best_delta_q = delta_q;
158                    best_community = community;
159                }
160            }
161
162            // Move node to best community
163            if best_community != current_community {
164                improved = true;
165            }
166            communities.insert(node_idx, best_community);
167        }
168    }
169
170    // Renumber communities to be consecutive
171    let mut community_map: HashMap<usize, usize> = HashMap::new();
172    let mut next_id = 0;
173    for &comm in communities.values() {
174        if let std::collections::hash_map::Entry::Vacant(e) = community_map.entry(comm) {
175            e.insert(next_id);
176            next_id += 1;
177        }
178    }
179
180    // Apply renumbering
181    for comm in communities.values_mut() {
182        *comm = community_map[comm];
183    }
184
185    // Calculate final modularity
186    let modularity = calculate_modularity(graph, &communities, m);
187
188    // Convert to final result
189    let node_communities: HashMap<N, usize> = communities
190        .into_iter()
191        .map(|(idx, comm)| (graph.inner()[idx].clone(), comm))
192        .collect();
193
194    CommunityStructure {
195        node_communities,
196        modularity,
197    }
198}
199
200/// Calculate modularity for a given partition
201#[allow(dead_code)]
202pub fn calculate_modularity<N, E, Ix>(
203    graph: &Graph<N, E, Ix>,
204    communities: &HashMap<petgraph::graph::NodeIndex<Ix>, usize>,
205    m: f64,
206) -> f64
207where
208    N: Node + std::fmt::Debug,
209    E: EdgeWeight + Into<f64> + Copy,
210    Ix: petgraph::graph::IndexType,
211{
212    let mut modularity = 0.0;
213
214    // Calculate node degrees
215    let mut node_degrees: HashMap<petgraph::graph::NodeIndex<Ix>, f64> = HashMap::new();
216    for node_idx in graph.inner().node_indices() {
217        let degree: f64 = graph
218            .inner()
219            .edges(node_idx)
220            .map(|e| (*e.weight()).into())
221            .sum();
222        node_degrees.insert(node_idx, degree);
223    }
224
225    // Sum over all pairs of nodes
226    for node_i in graph.inner().node_indices() {
227        for node_j in graph.inner().node_indices() {
228            if communities[&node_i] == communities[&node_j] {
229                // Check if edge exists between i and j
230                let mut a_ij = 0.0;
231                for edge in graph.inner().edges(node_i) {
232                    if edge.target() == node_j {
233                        a_ij = (*edge.weight()).into();
234                        break;
235                    }
236                }
237
238                let k_i = node_degrees[&node_i];
239                let k_j = node_degrees[&node_j];
240
241                modularity += a_ij - (k_i * k_j) / (2.0 * m);
242            }
243        }
244    }
245
246    modularity / (2.0 * m)
247}