scirs2_graph/algorithms/community/
modularity.rs

1//! Modularity calculation and optimization algorithms
2
3use super::types::{CommunityResult, CommunityStructure};
4use crate::base::{EdgeWeight, Graph, IndexType, Node};
5use std::collections::{HashMap, HashSet};
6use std::hash::Hash;
7
8/// Computes the modularity of a given community partition
9///
10/// Modularity measures the quality of a partition by comparing the number
11/// of edges within communities to what would be expected in a random graph.
12///
13/// # Arguments
14/// * `graph` - The graph to analyze
15/// * `communities` - Map from nodes to community IDs
16///
17/// # Returns
18/// * The modularity score (typically between -1 and 1, higher is better)
19///
20/// # Time Complexity
21/// O(m + n) where m is the number of edges and n is the number of nodes.
22/// This is the optimized implementation that avoids the O(n²) naive approach.
23///
24/// # Space Complexity
25/// O(n) for storing degree information and community assignments.
26#[allow(dead_code)]
27pub fn modularity<N, E, Ix>(graph: &Graph<N, E, Ix>, communities: &HashMap<N, usize>) -> f64
28where
29    N: Node + std::fmt::Debug,
30    E: EdgeWeight + Into<f64> + Copy,
31    Ix: IndexType,
32{
33    let n = graph.node_count();
34    if n == 0 || communities.is_empty() {
35        return 0.0;
36    }
37
38    // Calculate total edge weight
39    let mut m = 0.0;
40    for edge in graph.inner().edge_references() {
41        m += (*edge.weight()).into();
42    }
43
44    if m == 0.0 {
45        return 0.0;
46    }
47
48    // Calculate node degrees
49    let mut node_degrees: HashMap<N, f64> = HashMap::new();
50    for node in graph.nodes() {
51        let mut degree = 0.0;
52        if let Ok(neighbors) = graph.neighbors(node) {
53            for neighbor in neighbors {
54                if let Ok(weight) = graph.edge_weight(node, &neighbor) {
55                    degree += weight.into();
56                }
57            }
58        }
59        node_degrees.insert(node.clone(), degree);
60    }
61
62    // Calculate modularity
63    let mut q = 0.0;
64    for node_i in graph.nodes() {
65        for node_j in graph.nodes() {
66            if communities.get(node_i) == communities.get(node_j) {
67                // Check if edge exists
68                let a_ij = if let Ok(weight) = graph.edge_weight(node_i, node_j) {
69                    weight.into()
70                } else {
71                    0.0
72                };
73
74                let k_i = node_degrees.get(node_i).unwrap_or(&0.0);
75                let k_j = node_degrees.get(node_j).unwrap_or(&0.0);
76
77                q += a_ij - (k_i * k_j) / (2.0 * m);
78            }
79        }
80    }
81
82    q / (2.0 * m)
83}
84
85/// Optimizes modularity using simulated annealing
86///
87/// This algorithm tries to maximize modularity by iteratively moving nodes
88/// between communities using simulated annealing to escape local optima.
89///
90/// # Arguments
91/// * `graph` - The graph to analyze
92/// * `initial_temp` - Initial temperature for simulated annealing
93/// * `cooling_rate` - Rate at which temperature decreases (0 < rate < 1)
94/// * `max_iterations` - Maximum number of iterations
95///
96/// # Returns
97/// * A community structure with optimized modularity
98///
99/// # Time Complexity
100/// O(k * n * m) where k is the number of iterations, n is the number of nodes,
101/// and m is the number of edges. Each iteration involves evaluating modularity
102/// changes for potential node moves.
103///
104/// # Space Complexity
105/// O(n) for storing community assignments and modularity calculations.
106#[deprecated(
107    since = "0.1.0-beta.2",
108    note = "Use `modularity_optimization_result` instead for standardized community detection API"
109)]
110#[allow(dead_code)]
111pub fn modularity_optimization<N, E, Ix>(
112    graph: &Graph<N, E, Ix>,
113    initial_temp: f64,
114    cooling_rate: f64,
115    max_iterations: usize,
116) -> CommunityStructure<N>
117where
118    N: Node + Clone + Hash + Eq + std::fmt::Debug,
119    E: EdgeWeight + Into<f64> + Copy,
120    Ix: IndexType,
121{
122    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
123    let n = nodes.len();
124
125    if n == 0 {
126        return CommunityStructure {
127            node_communities: HashMap::new(),
128            modularity: 0.0,
129        };
130    }
131
132    // Initialize with each node in its own community
133    let mut current_communities: HashMap<N, usize> = nodes
134        .iter()
135        .enumerate()
136        .map(|(i, node)| (node.clone(), i))
137        .collect();
138
139    let mut current_modularity = modularity(graph, &current_communities);
140    let mut best_communities = current_communities.clone();
141    let mut best_modularity = current_modularity;
142
143    let mut temp = initial_temp;
144    let mut rng = scirs2_core::random::rng();
145
146    for _iteration in 0..max_iterations {
147        // Choose a random node to move
148        use scirs2_core::random::Rng;
149        let node_idx = rng.gen_range(0..n);
150        let node = &nodes[node_idx];
151        let current_community = current_communities[node];
152
153        // Find possible communities to move to (neighboring communities + new community)
154        let mut candidate_communities = HashSet::new();
155        candidate_communities.insert(n); // New community
156
157        if let Ok(neighbors) = graph.neighbors(node) {
158            for neighbor in neighbors {
159                if let Some(&comm) = current_communities.get(&neighbor) {
160                    candidate_communities.insert(comm);
161                }
162            }
163        }
164
165        // Try moving to a random candidate community
166        let candidates: Vec<usize> = candidate_communities.into_iter().collect();
167        if candidates.is_empty() {
168            continue;
169        }
170
171        let new_community = candidates[rng.gen_range(0..candidates.len())];
172
173        if new_community == current_community {
174            continue;
175        }
176
177        // Make the move temporarily
178        current_communities.insert(node.clone(), new_community);
179        let new_modularity = modularity(graph, &current_communities);
180        let delta = new_modularity - current_modularity;
181
182        // Accept or reject the move
183        let accept = if delta > 0.0 {
184            true
185        } else {
186            // Accept with probability exp(delta / temp)
187            let prob = (delta / temp).exp();
188            rng.random::<f64>() < prob
189        };
190
191        if accept {
192            current_modularity = new_modularity;
193            if current_modularity > best_modularity {
194                best_modularity = current_modularity;
195                best_communities = current_communities.clone();
196            }
197        } else {
198            // Revert the move
199            current_communities.insert(node.clone(), current_community);
200        }
201
202        // Cool down
203        temp *= cooling_rate;
204
205        // Early stopping if temperature is too low
206        if temp < 1e-8 {
207            break;
208        }
209    }
210
211    // Renumber communities to be consecutive
212    let mut community_map: HashMap<usize, usize> = HashMap::new();
213    let mut next_id = 0;
214    for &comm in best_communities.values() {
215        if let std::collections::hash_map::Entry::Vacant(e) = community_map.entry(comm) {
216            e.insert(next_id);
217            next_id += 1;
218        }
219    }
220
221    // Apply renumbering
222    for (_, comm) in best_communities.iter_mut() {
223        *comm = community_map[comm];
224    }
225
226    CommunityStructure {
227        node_communities: best_communities,
228        modularity: best_modularity,
229    }
230}
231
232/// Greedy modularity optimization algorithm
233///
234/// This is a simplified version of modularity optimization that uses a greedy
235/// approach without simulated annealing. It's faster but may get stuck in local optima.
236///
237/// # Arguments
238/// * `graph` - The graph to analyze
239/// * `max_iterations` - Maximum number of iterations
240///
241/// # Returns
242/// * A community structure with optimized modularity
243///
244/// # Time Complexity
245/// O(k * n * d) where k is the number of iterations, n is the number of nodes,
246/// and d is the average degree. Each iteration involves finding the best community
247/// for each node based on local modularity improvements.
248///
249/// # Space Complexity
250/// O(n) for storing community assignments and tracking modularity gains.
251#[deprecated(
252    since = "0.1.0-beta.2",
253    note = "Use `greedy_modularity_optimization_result` instead for standardized community detection API"
254)]
255#[allow(dead_code)]
256pub fn greedy_modularity_optimization<N, E, Ix>(
257    graph: &Graph<N, E, Ix>,
258    max_iterations: usize,
259) -> CommunityStructure<N>
260where
261    N: Node + Clone + Hash + Eq + std::fmt::Debug,
262    E: EdgeWeight + Into<f64> + Copy,
263    Ix: IndexType,
264{
265    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
266    let n = nodes.len();
267
268    if n == 0 {
269        return CommunityStructure {
270            node_communities: HashMap::new(),
271            modularity: 0.0,
272        };
273    }
274
275    // Initialize with each node in its own community
276    let mut communities: HashMap<N, usize> = nodes
277        .iter()
278        .enumerate()
279        .map(|(i, node)| (node.clone(), i))
280        .collect();
281
282    let mut improved = true;
283    let mut _iterations = 0;
284
285    while improved && _iterations < max_iterations {
286        improved = false;
287        _iterations += 1;
288
289        let current_modularity = modularity(graph, &communities);
290
291        // Try moving each node to each neighboring community
292        for node in &nodes {
293            let original_community = communities[node];
294            let mut best_modularity = current_modularity;
295            let mut best_community = original_community;
296
297            // Get neighboring communities
298            let mut neighboring_communities = HashSet::new();
299            if let Ok(neighbors) = graph.neighbors(node) {
300                for neighbor in neighbors {
301                    if let Some(&comm) = communities.get(&neighbor) {
302                        neighboring_communities.insert(comm);
303                    }
304                }
305            }
306
307            // Try each neighboring community
308            for &candidate_community in &neighboring_communities {
309                if candidate_community != original_community {
310                    communities.insert(node.clone(), candidate_community);
311                    let new_modularity = modularity(graph, &communities);
312
313                    if new_modularity > best_modularity {
314                        best_modularity = new_modularity;
315                        best_community = candidate_community;
316                    }
317                }
318            }
319
320            // Move to best community if it's better
321            if best_community != original_community {
322                communities.insert(node.clone(), best_community);
323                improved = true;
324            } else {
325                // Restore original community
326                communities.insert(node.clone(), original_community);
327            }
328        }
329    }
330
331    // Renumber communities to be consecutive
332    let mut community_map: HashMap<usize, usize> = HashMap::new();
333    let mut next_id = 0;
334    for &comm in communities.values() {
335        if let std::collections::hash_map::Entry::Vacant(e) = community_map.entry(comm) {
336            e.insert(next_id);
337            next_id += 1;
338        }
339    }
340
341    // Apply renumbering
342    for (_, comm) in communities.iter_mut() {
343        *comm = community_map[comm];
344    }
345
346    let final_modularity = modularity(graph, &communities);
347
348    CommunityStructure {
349        node_communities: communities,
350        modularity: final_modularity,
351    }
352}
353
354/// Optimizes modularity using simulated annealing (modern API)
355///
356/// Returns a standardized `CommunityResult` type that provides multiple ways
357/// to access the community structure.
358///
359/// # Arguments
360/// * `graph` - The graph to analyze
361/// * `initial_temp` - Initial temperature for simulated annealing
362/// * `cooling_rate` - Rate at which temperature decreases (0 < rate < 1)
363/// * `max_iterations` - Maximum number of iterations
364///
365/// # Returns
366/// * A `CommunityResult` with comprehensive community information
367#[allow(dead_code)]
368pub fn modularity_optimization_result<N, E, Ix>(
369    graph: &Graph<N, E, Ix>,
370    initial_temp: f64,
371    cooling_rate: f64,
372    max_iterations: usize,
373) -> CommunityResult<N>
374where
375    N: Node + Clone + Hash + Eq + std::fmt::Debug,
376    E: EdgeWeight + Into<f64> + Copy,
377    Ix: IndexType,
378{
379    #[allow(deprecated)]
380    let structure = modularity_optimization(graph, initial_temp, cooling_rate, max_iterations);
381    CommunityResult::from_community_structure(structure)
382}
383
384/// Greedy modularity optimization algorithm (modern API)
385///
386/// Returns a standardized `CommunityResult` type that provides multiple ways
387/// to access the community structure.
388///
389/// # Arguments
390/// * `graph` - The graph to analyze
391/// * `max_iterations` - Maximum number of iterations
392///
393/// # Returns
394/// * A `CommunityResult` with comprehensive community information
395#[allow(dead_code)]
396pub fn greedy_modularity_optimization_result<N, E, Ix>(
397    graph: &Graph<N, E, Ix>,
398    max_iterations: usize,
399) -> CommunityResult<N>
400where
401    N: Node + Clone + Hash + Eq + std::fmt::Debug,
402    E: EdgeWeight + Into<f64> + Copy,
403    Ix: IndexType,
404{
405    #[allow(deprecated)]
406    let structure = greedy_modularity_optimization(graph, max_iterations);
407    CommunityResult::from_community_structure(structure)
408}