scirs2_graph/algorithms/community/
fluid.rs

1//! Fluid communities algorithm for community detection
2
3use super::types::{CommunityResult, CommunityStructure};
4use crate::base::{EdgeWeight, Graph, IndexType, Node};
5use std::collections::HashMap;
6use std::hash::Hash;
7
8/// Fluid communities algorithm (legacy API)
9///
10/// **Note**: This function is deprecated in favor of `fluid_communities_result`.
11/// It will be removed in version 2.0.
12///
13/// Fluid communities is a density-based algorithm where communities are formed
14/// by propagating "fluids" through the network. Each community starts with a seed
15/// node and expands by including neighboring nodes based on density.
16///
17/// # Arguments
18/// * `graph` - The graph to analyze
19/// * `num_communities` - Target number of communities to find
20/// * `max_iterations` - Maximum number of iterations
21///
22/// # Returns
23/// * A community structure with node assignments and modularity
24#[deprecated(
25    since = "0.1.0-beta.2",
26    note = "Use `fluid_communities_result` instead"
27)]
28#[allow(dead_code)]
29pub fn fluid_communities<N, E, Ix>(
30    graph: &Graph<N, E, Ix>,
31    num_communities: usize,
32    max_iterations: usize,
33) -> CommunityStructure<N>
34where
35    N: Node + Clone + Hash + Eq + std::fmt::Debug,
36    E: EdgeWeight + Into<f64> + Copy,
37    Ix: IndexType,
38{
39    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
40    let n = nodes.len();
41
42    if n == 0 || num_communities == 0 {
43        return CommunityStructure {
44            node_communities: HashMap::new(),
45            modularity: 0.0,
46        };
47    }
48
49    let num_communities = num_communities.min(n);
50    let mut rng = scirs2_core::random::rng();
51
52    // Initialize fluids - each node starts with a random fluid
53    let mut node_fluids: HashMap<N, Vec<f64>> = HashMap::new();
54    for node in &nodes {
55        let mut fluids = vec![0.0; num_communities];
56        // Assign random initial fluid
57        use scirs2_core::random::Rng;
58        let initial_fluid = rng.gen_range(0..num_communities);
59        fluids[initial_fluid] = 1.0;
60        node_fluids.insert(node.clone(), fluids);
61    }
62
63    // Fluid propagation iterations
64    for _iteration in 0..max_iterations {
65        let mut new_fluids: HashMap<N, Vec<f64>> = HashMap::new();
66
67        for node in &nodes {
68            let mut fluid_sums = vec![0.0; num_communities];
69
70            // Aggregate fluids from neighbors
71            if let Ok(neighbors) = graph.neighbors(node) {
72                let neighbor_count = neighbors.len();
73                if neighbor_count > 0 {
74                    for neighbor in neighbors {
75                        if let Some(neighbor_fluids) = node_fluids.get(&neighbor) {
76                            for (i, &fluid_amount) in neighbor_fluids.iter().enumerate() {
77                                fluid_sums[i] += fluid_amount;
78                            }
79                        }
80                    }
81
82                    // Normalize by number of neighbors
83                    for fluid_sum in fluid_sums.iter_mut() {
84                        *fluid_sum /= neighbor_count as f64;
85                    }
86                } else {
87                    // Isolated nodes keep their current fluids
88                    if let Some(current_fluids) = node_fluids.get(node) {
89                        fluid_sums = current_fluids.clone();
90                    }
91                }
92            } else {
93                // No neighbors, keep current fluids
94                if let Some(current_fluids) = node_fluids.get(node) {
95                    fluid_sums = current_fluids.clone();
96                }
97            }
98
99            // Normalize fluids to sum to 1
100            let total: f64 = fluid_sums.iter().sum();
101            if total > 0.0 {
102                for fluid in fluid_sums.iter_mut() {
103                    *fluid /= total;
104                }
105            } else {
106                // If all fluids are zero, assign random fluid
107                use scirs2_core::random::Rng;
108                let random_fluid = rng.gen_range(0..num_communities);
109                fluid_sums[random_fluid] = 1.0;
110            }
111
112            new_fluids.insert(node.clone(), fluid_sums);
113        }
114
115        // Update fluids
116        node_fluids = new_fluids;
117    }
118
119    // Assign nodes to communities based on dominant fluid
120    let mut communities: HashMap<N, usize> = HashMap::new();
121    for (node, fluids) in &node_fluids {
122        let max_fluid_idx = fluids
123            .iter()
124            .enumerate()
125            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
126            .map(|(i, _)| i)
127            .unwrap_or(0);
128        communities.insert(node.clone(), max_fluid_idx);
129    }
130
131    // Renumber communities to be consecutive
132    let mut community_map: HashMap<usize, usize> = HashMap::new();
133    let mut next_id = 0;
134    for &comm in communities.values() {
135        if let std::collections::hash_map::Entry::Vacant(e) = community_map.entry(comm) {
136            e.insert(next_id);
137            next_id += 1;
138        }
139    }
140
141    // Apply renumbering
142    for (_, comm) in communities.iter_mut() {
143        *comm = community_map[comm];
144    }
145
146    // Calculate modularity
147    let mod_score = super::modularity(graph, &communities);
148
149    CommunityStructure {
150        node_communities: communities,
151        modularity: mod_score,
152    }
153}
154
155/// Fluid communities algorithm with standardized CommunityResult return type
156///
157/// This function provides the same functionality as `fluid_communities` but returns
158/// a standardized `CommunityResult` type that provides multiple ways to access
159/// the community structure.
160///
161/// Fluid communities is a density-based algorithm where communities are formed
162/// by propagating "fluids" through the network. Each community starts with a seed
163/// node and expands by including neighboring nodes based on density.
164///
165/// # Arguments
166/// * `graph` - The graph to analyze
167/// * `num_communities` - Target number of communities to find
168/// * `max_iterations` - Maximum number of iterations
169///
170/// # Returns
171/// * A `CommunityResult` with comprehensive community information
172///
173/// # Time Complexity
174/// O(k * m * c) where k is the number of iterations, m is the number of edges,
175/// and c is the target number of communities. Each iteration involves fluid
176/// propagation across all edges and community density calculations.
177///
178/// # Space Complexity
179/// O(n + c) for storing node assignments, fluid densities per community,
180/// and tracking community membership changes.
181///
182/// # Example
183/// ```rust
184/// use scirs2_graph::{Graph, fluid_communities_result};
185///
186/// let mut graph: Graph<i32, f64> = Graph::new();
187/// // ... add nodes and edges ...
188/// let result = fluid_communities_result(&graph, 5, 100);
189///
190/// println!("Found {} communities", result.num_communities);
191/// for (i, community) in result.communities.iter().enumerate() {
192///     println!("Community {}: {} members", i, community.len());
193/// }
194/// ```
195#[allow(dead_code)]
196pub fn fluid_communities_result<N, E, Ix>(
197    graph: &Graph<N, E, Ix>,
198    num_communities: usize,
199    max_iterations: usize,
200) -> CommunityResult<N>
201where
202    N: Node + Clone + Hash + Eq + std::fmt::Debug,
203    E: EdgeWeight + Into<f64> + Copy,
204    Ix: IndexType,
205{
206    #[allow(deprecated)]
207    let structure = fluid_communities(graph, num_communities, max_iterations);
208    let mut result = CommunityResult::from_node_map(structure.node_communities.clone());
209
210    // Calculate and set the quality score (modularity)
211    let mod_score = super::modularity(graph, &structure.node_communities);
212    result.quality_score = Some(mod_score);
213    result
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::error::Result as GraphResult;
220    use crate::generators::create_graph;
221
222    #[test]
223    fn test_fluid_communities() -> GraphResult<()> {
224        // Create a graph with clear community structure
225        let mut graph = create_graph::<i32, f64>();
226
227        // Community 1: triangle
228        graph.add_edge(0, 1, 1.0)?;
229        graph.add_edge(1, 2, 1.0)?;
230        graph.add_edge(2, 0, 1.0)?;
231
232        // Community 2: triangle
233        graph.add_edge(3, 4, 1.0)?;
234        graph.add_edge(4, 5, 1.0)?;
235        graph.add_edge(5, 3, 1.0)?;
236
237        // Weak connection between communities
238        graph.add_edge(2, 3, 0.1)?;
239
240        let result = fluid_communities_result(&graph, 2, 30);
241
242        // Check that all nodes are assigned to communities
243        assert_eq!(result.node_communities.len(), 6);
244
245        // Check that we found the expected number of communities (should be <= 2)
246        assert!(result.num_communities <= 2);
247        assert!(result.num_communities > 0);
248
249        // Check that quality score was calculated
250        assert!(result.quality_score.is_some());
251
252        Ok(())
253    }
254
255    #[test]
256    fn test_fluid_communities_empty_graph() {
257        let graph = create_graph::<i32, f64>();
258        let result = fluid_communities_result(&graph, 2, 10);
259
260        assert!(result.node_communities.is_empty());
261        assert_eq!(result.quality_score.unwrap_or(0.0), 0.0);
262    }
263
264    #[test]
265    fn test_fluid_communities_single_node() -> GraphResult<()> {
266        let mut graph = create_graph::<&str, f64>();
267        graph.add_node("A");
268
269        let result = fluid_communities_result(&graph, 1, 10);
270
271        assert_eq!(result.node_communities.len(), 1);
272        assert!(result.node_communities.contains_key(&"A"));
273        assert_eq!(result.node_communities[&"A"], 0);
274
275        Ok(())
276    }
277}