1#![allow(
30 clippy::cast_possible_truncation,
31 clippy::cast_possible_wrap,
32 clippy::cast_precision_loss,
33 clippy::cast_sign_loss,
34 clippy::float_cmp,
35 clippy::items_after_statements,
36 clippy::many_single_char_names,
37 clippy::needless_range_loop,
38 clippy::too_many_lines
39)]
40
41use std::cmp::Ordering;
42use std::collections::BinaryHeap;
43
44use crate::algorithms::community::edge_betweenness_community::EdgeBetweennessResult;
45use crate::algorithms::community::modularity::{modularity_weighted, modularity_weighted_directed};
46use crate::core::graph::EdgeId;
47use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
48
49#[derive(Copy, Clone)]
53struct Frontier(f64, VertexId);
54
55impl PartialEq for Frontier {
56 fn eq(&self, other: &Self) -> bool {
57 self.0 == other.0 && self.1 == other.1
58 }
59}
60impl Eq for Frontier {}
61impl Ord for Frontier {
62 fn cmp(&self, other: &Self) -> Ordering {
63 other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
64 }
65}
66impl PartialOrd for Frontier {
67 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
68 Some(self.cmp(other))
69 }
70}
71
72pub fn edge_betweenness_community_weighted(
111 graph: &Graph,
112 weights: &[f64],
113) -> IgraphResult<EdgeBetweennessResult> {
114 let directed = graph.is_directed();
115 let n = graph.vcount();
116 let m_us = graph.ecount();
117 let n_us = n as usize;
118
119 if n == 0 {
122 return Ok(EdgeBetweennessResult {
123 membership: Vec::new(),
124 nb_clusters: 0,
125 removed_edges: Vec::new(),
126 edge_betweenness: Vec::new(),
127 merges: Vec::new(),
128 bridges: Vec::new(),
129 modularity: Vec::new(),
130 });
131 }
132 if m_us == 0 {
133 if !weights.is_empty() {
134 return Err(IgraphError::InvalidArgument(format!(
135 "weights vector size ({}) differs from edge count (0)",
136 weights.len(),
137 )));
138 }
139 return Ok(EdgeBetweennessResult {
140 membership: (0..n).collect(),
141 nb_clusters: n,
142 removed_edges: Vec::new(),
143 edge_betweenness: Vec::new(),
144 merges: Vec::new(),
145 bridges: Vec::new(),
146 modularity: vec![0.0],
147 });
148 }
149
150 if weights.len() != m_us {
153 return Err(IgraphError::InvalidArgument(format!(
154 "weights vector size ({}) differs from edge count ({})",
155 weights.len(),
156 m_us
157 )));
158 }
159 for (e, &w) in weights.iter().enumerate() {
160 if w.is_nan() || !w.is_finite() || w < 0.0 {
161 return Err(IgraphError::InvalidArgument(format!(
162 "weight at edge {e} must be non-negative and finite (got {w})"
163 )));
164 }
165 }
166
167 let mut inc_out: Vec<Vec<EdgeId>> = (0..n)
175 .map(|v| graph.incident(v))
176 .collect::<IgraphResult<Vec<_>>>()?;
177 let mut inc_in: Vec<Vec<EdgeId>> = if directed {
178 (0..n)
179 .map(|v| graph.incident_in(v))
180 .collect::<IgraphResult<Vec<_>>>()?
181 } else {
182 Vec::new()
183 };
184 let mut passive: Vec<bool> = vec![false; m_us];
185
186 let mut removed_edges: Vec<EdgeId> = Vec::with_capacity(m_us);
187 let mut edge_betweenness_history: Vec<f64> = Vec::with_capacity(m_us);
188
189 let mut sigma = vec![0.0_f64; n_us];
191 let mut dist = vec![f64::INFINITY; n_us];
192 let mut visited = vec![false; n_us];
193 let mut pred: Vec<Vec<(VertexId, EdgeId)>> = vec![Vec::new(); n_us];
194 let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
195 let mut delta_v = vec![0.0_f64; n_us];
196 let mut eb_now = vec![0.0_f64; m_us];
197
198 for _ in 0..m_us {
199 eb_now.fill(0.0);
200
201 for s in 0..n {
203 sigma.fill(0.0);
204 dist.fill(f64::INFINITY);
205 visited.fill(false);
206 for slot in &mut pred {
207 slot.clear();
208 }
209 stack.clear();
210 delta_v.fill(0.0);
211
212 sigma[s as usize] = 1.0;
213 dist[s as usize] = 0.0;
214 let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
215 heap.push(Frontier(0.0, s));
216
217 while let Some(Frontier(d, v)) = heap.pop() {
218 let v_us = v as usize;
219 if visited[v_us] {
220 continue;
221 }
222 visited[v_us] = true;
223 stack.push(v);
224
225 for &eid in &inc_out[v_us] {
226 let w_edge = weights[eid as usize];
227 let other = if directed {
228 let (_from, to) = graph.edge(eid)?;
229 to
230 } else {
231 graph.edge_other(eid, v)?
232 };
233 let other_us = other as usize;
234 let nd = d + w_edge;
235 match nd.partial_cmp(&dist[other_us]) {
236 Some(Ordering::Less) => {
237 dist[other_us] = nd;
238 sigma[other_us] = sigma[v_us];
239 pred[other_us].clear();
240 pred[other_us].push((v, eid));
241 heap.push(Frontier(nd, other));
242 }
243 Some(Ordering::Equal) => {
244 sigma[other_us] += sigma[v_us];
245 pred[other_us].push((v, eid));
246 }
247 _ => {}
248 }
249 }
250 }
251
252 while let Some(w) = stack.pop() {
253 let w_us = w as usize;
254 for &(v, e) in &pred[w_us] {
255 let c = (sigma[v as usize] / sigma[w_us]) * (1.0 + delta_v[w_us]);
256 delta_v[v as usize] += c;
257 eb_now[e as usize] += c;
258 }
259 }
260 }
261
262 let mut max_eid: Option<EdgeId> = None;
265 let mut max_val = f64::NEG_INFINITY;
266 for e in 0..m_us {
267 if passive[e] {
268 continue;
269 }
270 let val = eb_now[e];
271 if val > max_val {
272 max_val = val;
273 max_eid = Some(e as EdgeId);
274 }
275 }
276 let eid = max_eid.ok_or(IgraphError::Internal(
277 "edge_betweenness_community_weighted: no active edge to remove",
278 ))?;
279 removed_edges.push(eid);
280 edge_betweenness_history.push(if directed { max_val } else { max_val / 2.0 });
283 passive[eid as usize] = true;
284
285 let (from, to) = graph.edge(eid)?;
286 if directed {
287 inc_out[from as usize].retain(|&e| e != eid);
288 inc_in[to as usize].retain(|&e| e != eid);
289 } else {
290 for endpoint in [from, to] {
291 inc_out[endpoint as usize].retain(|&e| e != eid);
292 }
293 }
294 }
295
296 let mut membership_now: Vec<u32> = (0..n).collect();
299 let mut merges: Vec<[u32; 2]> = Vec::new();
300 let mut bridges: Vec<u32> = Vec::new();
301 let mut modularity_levels: Vec<f64> = Vec::new();
302
303 let level_q = |mem: &[u32]| -> IgraphResult<f64> {
304 let opt = if directed {
305 modularity_weighted_directed(graph, mem, 1.0, weights)?
306 } else {
307 modularity_weighted(graph, mem, 1.0, weights)?
308 };
309 Ok(opt.unwrap_or(0.0))
310 };
311 let q0 = level_q(&membership_now)?;
312 modularity_levels.push(q0);
313 let mut max_mod = q0;
314 let mut best_membership: Vec<u32> = membership_now.clone();
315
316 for (step, &eid) in removed_edges.iter().enumerate().rev() {
317 let (from, to) = graph.edge(eid)?;
318 let c1 = membership_now[from as usize];
319 let c2 = membership_now[to as usize];
320 if c1 == c2 {
321 continue;
322 }
323
324 let merge_index = merges.len();
325 let new_cluster = n
326 .checked_add(merge_index as u32)
327 .ok_or(IgraphError::Internal(
328 "edge_betweenness_community_weighted: merge index overflow",
329 ))?;
330 merges.push([c1, c2]);
331 bridges.push(step as u32);
332
333 for slot in &mut membership_now {
334 if *slot == c1 || *slot == c2 {
335 *slot = new_cluster;
336 }
337 }
338
339 let q = level_q(&membership_now)?;
340 modularity_levels.push(q);
341 if q > max_mod {
342 max_mod = q;
343 best_membership.clone_from(&membership_now);
344 }
345 }
346
347 let (membership_dense, nb_clusters) = densify_labels(&best_membership);
348
349 Ok(EdgeBetweennessResult {
350 membership: membership_dense,
351 nb_clusters,
352 removed_edges,
353 edge_betweenness: edge_betweenness_history,
354 merges,
355 bridges,
356 modularity: modularity_levels,
357 })
358}
359
360fn densify_labels(labels: &[u32]) -> (Vec<u32>, u32) {
365 let mut remap: Vec<(u32, u32)> = Vec::new();
366 let mut out: Vec<u32> = Vec::with_capacity(labels.len());
367 for &lbl in labels {
368 let dense = if let Some(&(_, d)) = remap.iter().find(|(orig, _)| *orig == lbl) {
369 d
370 } else {
371 let d = remap.len() as u32;
372 remap.push((lbl, d));
373 d
374 };
375 out.push(dense);
376 }
377 let n_clusters = remap.len() as u32;
378 (out, n_clusters)
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::algorithms::community::edge_betweenness_community::edge_betweenness_community;
385
386 fn well_formed(r: &EdgeBetweennessResult, n: u32, m: usize) {
387 assert_eq!(r.membership.len() as u32, n, "membership length");
388 assert_eq!(r.removed_edges.len(), m, "removed_edges length");
389 assert_eq!(r.edge_betweenness.len(), m, "history length");
390 assert_eq!(r.merges.len(), r.bridges.len(), "merges/bridges");
391 assert_eq!(
392 r.modularity.len(),
393 r.merges.len() + 1,
394 "modularity = merges + 1"
395 );
396 for &lbl in &r.membership {
397 assert!(lbl < r.nb_clusters, "dense label in range");
398 }
399 }
400
401 #[test]
402 fn empty_graph_returns_empty_result() {
403 let g = Graph::with_vertices(0);
404 let r = edge_betweenness_community_weighted(&g, &[]).unwrap();
405 assert_eq!(r.nb_clusters, 0);
406 assert!(r.removed_edges.is_empty());
407 assert!(r.modularity.is_empty());
408 }
409
410 #[test]
411 fn edgeless_graph_returns_singletons() {
412 let g = Graph::with_vertices(4);
413 let r = edge_betweenness_community_weighted(&g, &[]).unwrap();
414 assert_eq!(r.nb_clusters, 4);
415 for v in 0..4 {
416 assert_eq!(r.membership[v as usize], v);
417 }
418 assert!(r.removed_edges.is_empty());
419 assert_eq!(r.modularity, vec![0.0]);
420 }
421
422 #[test]
423 fn two_triangles_bridge_unit_weights_split_into_two() {
424 let mut g = Graph::with_vertices(6);
425 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
426 g.add_edge(u, v).unwrap();
427 }
428 let w = vec![1.0_f64; g.ecount()];
429 let r = edge_betweenness_community_weighted(&g, &w).unwrap();
430 well_formed(&r, 6, 7);
431 assert_eq!(r.nb_clusters, 2);
432 let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
433 assert!(
434 (from0, to0) == (2, 3) || (from0, to0) == (3, 2),
435 "first removed must be the bridge, got ({from0}, {to0})"
436 );
437 }
438
439 #[test]
440 fn unit_weights_match_unweighted_path_5() {
441 let mut g = Graph::with_vertices(5);
442 for i in 0..4u32 {
443 g.add_edge(i, i + 1).unwrap();
444 }
445 let w = vec![1.0_f64; g.ecount()];
446 let rw = edge_betweenness_community_weighted(&g, &w).unwrap();
447 let ru = edge_betweenness_community(&g).unwrap();
448 assert_eq!(rw.nb_clusters, ru.nb_clusters);
449 assert_eq!(rw.merges, ru.merges);
450 assert_eq!(rw.removed_edges, ru.removed_edges);
451 for (a, b) in rw.modularity.iter().zip(ru.modularity.iter()) {
453 assert!((a - b).abs() < 1e-9, "modularity mismatch: {a} vs {b}");
454 }
455 }
456
457 #[test]
458 fn cheap_bridge_still_removed_first() {
459 let mut g = Graph::with_vertices(6);
464 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
465 g.add_edge(u, v).unwrap();
466 }
467 let weights = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.1];
468 let r = edge_betweenness_community_weighted(&g, &weights).unwrap();
469 well_formed(&r, 6, 7);
470 let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
471 assert!(
472 (from0, to0) == (2, 3) || (from0, to0) == (3, 2),
473 "weighted-first-removed must be the bridge, got ({from0}, {to0})"
474 );
475 }
476
477 #[test]
478 fn directed_unit_weights_match_unweighted_path_6() {
479 let mut g = Graph::new(6, true).unwrap();
482 for i in 0..5u32 {
483 g.add_edge(i, i + 1).unwrap();
484 }
485 let w = vec![1.0_f64; g.ecount()];
486 let rw = edge_betweenness_community_weighted(&g, &w).unwrap();
487 let ru = edge_betweenness_community(&g).unwrap();
488 assert_eq!(rw.nb_clusters, ru.nb_clusters);
489 assert_eq!(rw.removed_edges, ru.removed_edges);
490 assert_eq!(rw.merges, ru.merges);
491 for (a, b) in rw.modularity.iter().zip(ru.modularity.iter()) {
492 assert!((a - b).abs() < 1e-9, "modularity mismatch: {a} vs {b}");
493 }
494 }
495
496 #[test]
497 fn directed_weighted_betweenness_is_not_halved() {
498 let mut g = Graph::new(4, true).unwrap();
502 for i in 0..3u32 {
503 g.add_edge(i, i + 1).unwrap();
504 }
505 let r = edge_betweenness_community_weighted(&g, &[1.0; 3]).unwrap();
506 let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
507 assert_eq!((from0, to0), (1, 2));
508 assert!(
509 (r.edge_betweenness[0] - 4.0).abs() < 1e-9,
510 "expected unhalved eb=4.0, got {}",
511 r.edge_betweenness[0]
512 );
513 }
514
515 #[test]
516 fn weights_size_mismatch_errors() {
517 let mut g = Graph::with_vertices(2);
518 g.add_edge(0, 1).unwrap();
519 assert!(edge_betweenness_community_weighted(&g, &[]).is_err());
520 }
521
522 #[test]
523 fn negative_weight_errors() {
524 let mut g = Graph::with_vertices(2);
525 g.add_edge(0, 1).unwrap();
526 let err = edge_betweenness_community_weighted(&g, &[-1.0]).unwrap_err();
527 assert!(matches!(err, IgraphError::InvalidArgument(_)));
528 }
529
530 #[test]
531 fn nan_weight_errors() {
532 let mut g = Graph::with_vertices(2);
533 g.add_edge(0, 1).unwrap();
534 let err = edge_betweenness_community_weighted(&g, &[f64::NAN]).unwrap_err();
535 assert!(matches!(err, IgraphError::InvalidArgument(_)));
536 }
537
538 #[test]
539 fn dendrogram_at_most_n_minus_components() {
540 let mut g = Graph::with_vertices(4);
541 for i in 0..4u32 {
542 g.add_edge(i, (i + 1) % 4).unwrap();
543 }
544 let r = edge_betweenness_community_weighted(&g, &[1.0; 4]).unwrap();
545 assert!(r.merges.len() <= 3);
546 }
547
548 #[test]
549 fn densify_labels_preserves_first_appearance_order() {
550 let (dense, n) = densify_labels(&[7, 7, 4, 4, 7, 9, 4, 9]);
551 assert_eq!(n, 3);
552 assert_eq!(dense, vec![0, 0, 1, 1, 0, 2, 1, 2]);
553 }
554}