graphmind_graph_algorithms/
lcc.rs1use super::common::{GraphView, NodeId};
13use std::collections::{HashMap, HashSet};
14
15#[derive(Debug, Clone)]
17pub struct LccResult {
18 pub coefficients: HashMap<NodeId, f64>,
20 pub average: f64,
22}
23
24pub fn local_clustering_coefficient(view: &GraphView) -> LccResult {
29 local_clustering_coefficient_directed(view, false)
30}
31
32pub fn local_clustering_coefficient_directed(view: &GraphView, directed: bool) -> LccResult {
42 let n = view.node_count;
43 if n == 0 {
44 return LccResult {
45 coefficients: HashMap::new(),
46 average: 0.0,
47 };
48 }
49
50 let mut neighbors: Vec<HashSet<usize>> = Vec::with_capacity(n);
52 for idx in 0..n {
53 let mut set = HashSet::new();
54 for &s in view.successors(idx) {
55 if s != idx {
56 set.insert(s);
57 }
58 }
59 for &p in view.predecessors(idx) {
60 if p != idx {
61 set.insert(p);
62 }
63 }
64 neighbors.push(set);
65 }
66
67 let successor_sets: Option<Vec<HashSet<usize>>> = if directed {
69 let mut sets = Vec::with_capacity(n);
70 for idx in 0..n {
71 let set: HashSet<usize> = view.successors(idx).iter().copied().collect();
72 sets.push(set);
73 }
74 Some(sets)
75 } else {
76 None
77 };
78
79 let mut coefficients = HashMap::with_capacity(n);
80 let mut sum = 0.0;
81
82 for idx in 0..n {
83 let deg = neighbors[idx].len();
84 if deg < 2 {
85 coefficients.insert(view.index_to_node[idx], 0.0);
86 continue;
87 }
88
89 let neighbor_vec: Vec<usize> = neighbors[idx].iter().cloned().collect();
90
91 if directed {
92 let succ_sets = successor_sets.as_ref().unwrap();
94 let mut directed_edges = 0usize;
95 for i in 0..neighbor_vec.len() {
96 for j in 0..neighbor_vec.len() {
97 if i != j && succ_sets[neighbor_vec[i]].contains(&neighbor_vec[j]) {
98 directed_edges += 1;
99 }
100 }
101 }
102 let max_edges = deg * (deg - 1); let cc = directed_edges as f64 / max_edges as f64;
104 coefficients.insert(view.index_to_node[idx], cc);
105 sum += cc;
106 } else {
107 let mut triangle_edges = 0usize;
109 for i in 0..neighbor_vec.len() {
110 for j in (i + 1)..neighbor_vec.len() {
111 if neighbors[neighbor_vec[i]].contains(&neighbor_vec[j]) {
112 triangle_edges += 1;
113 }
114 }
115 }
116 let max_edges = deg * (deg - 1) / 2; let cc = triangle_edges as f64 / max_edges as f64;
118 coefficients.insert(view.index_to_node[idx], cc);
119 sum += cc;
120 }
121 }
122
123 let average = if n > 0 { sum / n as f64 } else { 0.0 };
124
125 LccResult {
126 coefficients,
127 average,
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::common::GraphView;
135 use std::collections::HashMap;
136
137 #[test]
138 fn test_lcc_triangle() {
139 let index_to_node = vec![1, 2, 3];
141 let mut node_to_index = HashMap::new();
142 node_to_index.insert(1, 0);
143 node_to_index.insert(2, 1);
144 node_to_index.insert(3, 2);
145
146 let outgoing = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
147 let incoming = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
148
149 let view = GraphView::from_adjacency_list(
150 3,
151 index_to_node,
152 node_to_index,
153 outgoing,
154 incoming,
155 None,
156 );
157 let result = local_clustering_coefficient(&view);
158
159 for (_node, cc) in &result.coefficients {
161 assert!(
162 (cc - 1.0).abs() < 1e-10,
163 "Complete triangle LCC should be 1.0, got {}",
164 cc
165 );
166 }
167 assert!((result.average - 1.0).abs() < 1e-10);
168 }
169
170 #[test]
171 fn test_lcc_star() {
172 let index_to_node = vec![1, 2, 3, 4];
174 let mut node_to_index = HashMap::new();
175 for (i, &id) in index_to_node.iter().enumerate() {
176 node_to_index.insert(id, i);
177 }
178
179 let outgoing = vec![vec![1, 2, 3], vec![0], vec![0], vec![0]];
180 let incoming = vec![vec![1, 2, 3], vec![0], vec![0], vec![0]];
181
182 let view = GraphView::from_adjacency_list(
183 4,
184 index_to_node,
185 node_to_index,
186 outgoing,
187 incoming,
188 None,
189 );
190 let result = local_clustering_coefficient(&view);
191
192 assert!((result.coefficients[&1] - 0.0).abs() < 1e-10);
194 assert!((result.coefficients[&2] - 0.0).abs() < 1e-10);
196 }
197
198 #[test]
199 fn test_lcc_empty() {
200 let view = GraphView::from_adjacency_list(0, vec![], HashMap::new(), vec![], vec![], None);
201 let result = local_clustering_coefficient(&view);
202 assert!(result.coefficients.is_empty());
203 assert!((result.average - 0.0).abs() < 1e-10);
204 }
205
206 #[test]
207 fn test_lcc_directed_triangle() {
208 let index_to_node = vec![1, 2, 3];
213 let mut node_to_index = HashMap::new();
214 node_to_index.insert(1, 0);
215 node_to_index.insert(2, 1);
216 node_to_index.insert(3, 2);
217
218 let outgoing = vec![vec![1], vec![2], vec![0]];
220 let incoming = vec![vec![2], vec![0], vec![1]];
221
222 let view = GraphView::from_adjacency_list(
223 3,
224 index_to_node,
225 node_to_index,
226 outgoing,
227 incoming,
228 None,
229 );
230 let result = local_clustering_coefficient_directed(&view, true);
231
232 for (&_node, &cc) in &result.coefficients {
235 assert!(
236 (cc - 0.5).abs() < 1e-10,
237 "Directed cycle triangle LCC should be 0.5, got {}",
238 cc
239 );
240 }
241 }
242
243 #[test]
244 fn test_lcc_directed_complete_triangle() {
245 let index_to_node = vec![1, 2, 3];
249 let mut node_to_index = HashMap::new();
250 node_to_index.insert(1, 0);
251 node_to_index.insert(2, 1);
252 node_to_index.insert(3, 2);
253
254 let outgoing = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
255 let incoming = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
256
257 let view = GraphView::from_adjacency_list(
258 3,
259 index_to_node,
260 node_to_index,
261 outgoing,
262 incoming,
263 None,
264 );
265 let result = local_clustering_coefficient_directed(&view, true);
266
267 for (&_node, &cc) in &result.coefficients {
268 assert!(
269 (cc - 1.0).abs() < 1e-10,
270 "Fully connected directed triangle LCC should be 1.0, got {}",
271 cc
272 );
273 }
274 }
275}