1use crate::algorithms::cliques::maximal_cliques;
15use crate::algorithms::properties::is_simple::is_simple;
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18#[derive(Debug, Clone, PartialEq)]
21pub struct GraphletBasis {
22 pub cliques: Vec<Vec<VertexId>>,
24 pub thresholds: Vec<f64>,
27}
28
29#[derive(Debug, Clone, PartialEq)]
32pub struct GraphletDecomposition {
33 pub cliques: Vec<Vec<VertexId>>,
35 pub mu: Vec<f64>,
37}
38
39pub fn graphlets_candidate_basis(graph: &Graph, weights: &[f64]) -> IgraphResult<GraphletBasis> {
66 validate_graphlets_input(graph, weights)?;
67
68 let n = graph.vcount();
69 let ecount = graph.ecount();
70
71 if ecount == 0 {
72 return Ok(GraphletBasis {
73 cliques: Vec::new(),
74 thresholds: Vec::new(),
75 });
76 }
77
78 let min_thr = weights.iter().copied().fold(f64::INFINITY, f64::min);
79
80 let ids: Vec<VertexId> = (0..n).collect();
81
82 let mut cliques: Vec<Vec<VertexId>> = Vec::new();
83 let mut thresholds: Vec<f64> = Vec::new();
84
85 graphlets_recursive(graph, weights, &ids, min_thr, &mut cliques, &mut thresholds)?;
86
87 graphlets_filter(&mut cliques, &mut thresholds);
88
89 Ok(GraphletBasis {
90 cliques,
91 thresholds,
92 })
93}
94
95pub fn graphlets_project(
124 graph: &Graph,
125 weights: &[f64],
126 cliques: &[Vec<VertexId>],
127 start_mu: Option<&[f64]>,
128 niter: u32,
129) -> IgraphResult<Vec<f64>> {
130 validate_graphlets_input(graph, weights)?;
131
132 let no_cliques = cliques.len();
133 if no_cliques == 0 {
134 return Ok(Vec::new());
135 }
136
137 if let Some(sm) = start_mu {
138 if sm.len() != no_cliques {
139 return Err(IgraphError::InvalidArgument(format!(
140 "start_mu length {} does not match clique count {no_cliques}",
141 sm.len()
142 )));
143 }
144 }
145
146 let mut mu = match start_mu {
147 Some(sm) => sm.to_vec(),
148 None => vec![1.0; no_cliques],
149 };
150
151 project_inner(graph, weights, cliques, &mut mu, niter)?;
152
153 Ok(mu)
154}
155
156pub fn graphlets(
184 graph: &Graph,
185 weights: &[f64],
186 niter: u32,
187) -> IgraphResult<GraphletDecomposition> {
188 let basis = graphlets_candidate_basis(graph, weights)?;
189 let mu = graphlets_project(graph, weights, &basis.cliques, None, niter)?;
190
191 let mut order: Vec<usize> = (0..mu.len()).collect();
192 order.sort_by(|&a, &b| {
193 mu[b]
194 .partial_cmp(&mu[a])
195 .unwrap_or(std::cmp::Ordering::Equal)
196 });
197
198 let cliques: Vec<Vec<VertexId>> = order.iter().map(|&i| basis.cliques[i].clone()).collect();
199 let sorted_mu: Vec<f64> = order.iter().map(|&i| mu[i]).collect();
200
201 Ok(GraphletDecomposition {
202 cliques,
203 mu: sorted_mu,
204 })
205}
206
207fn validate_graphlets_input(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
208 if weights.len() != graph.ecount() {
209 return Err(IgraphError::InvalidArgument(format!(
210 "weights length {} does not match edge count {}",
211 weights.len(),
212 graph.ecount()
213 )));
214 }
215 if !is_simple(graph)? {
216 return Err(IgraphError::InvalidArgument(
217 "graphlets require a simple graph".to_string(),
218 ));
219 }
220 Ok(())
221}
222
223type SubCliqueResult = (Graph, Vec<f64>, Vec<VertexId>);
224
225fn graphlets_recursive(
228 graph: &Graph,
229 weights: &[f64],
230 ids: &[VertexId],
231 start_thr: f64,
232 cliques_out: &mut Vec<Vec<VertexId>>,
233 thresholds_out: &mut Vec<f64>,
234) -> IgraphResult<()> {
235 let n = graph.vcount();
236
237 let mut sub = Graph::with_vertices(n);
241 for (eid, &w) in weights.iter().enumerate() {
242 if w >= start_thr {
243 #[allow(clippy::cast_possible_truncation)]
244 let (src, tgt) = graph.edge(eid as u32)?;
245 sub.add_edge(src, tgt)?;
246 }
247 }
248
249 let my_cliques = maximal_cliques(&sub)?;
250
251 if my_cliques.is_empty() {
252 return Ok(());
253 }
254
255 let no_cliques = my_cliques.len();
259 let mut clique_thrs = Vec::with_capacity(no_cliques);
260 let mut next_thrs = Vec::with_capacity(no_cliques);
261 let mut sub_graphs: Vec<Option<SubCliqueResult>> = Vec::with_capacity(no_cliques);
262
263 for clique in &my_cliques {
264 let (min_w, next_w) = find_clique_thresholds(graph, weights, clique);
265 clique_thrs.push(min_w);
266 next_thrs.push(next_w);
267
268 if next_w.is_finite() {
270 let sub_result = build_subclique_graph(graph, weights, ids, clique, next_w)?;
271 sub_graphs.push(sub_result);
272 } else {
273 sub_graphs.push(None);
274 }
275 }
276
277 for (i, clique) in my_cliques.iter().enumerate() {
279 let mut mapped: Vec<VertexId> = clique.iter().map(|&v| ids[v as usize]).collect();
280 mapped.sort_unstable();
281 cliques_out.push(mapped);
282 thresholds_out.push(clique_thrs[i]);
283 }
284
285 for (i, sub_opt) in sub_graphs.into_iter().enumerate() {
287 if let Some((sub_g, sub_w, sub_ids)) = sub_opt {
288 if sub_g.vcount() > 1 {
289 graphlets_recursive(
290 &sub_g,
291 &sub_w,
292 &sub_ids,
293 next_thrs[i],
294 cliques_out,
295 thresholds_out,
296 )?;
297 }
298 }
299 }
300
301 Ok(())
302}
303
304fn find_clique_thresholds(graph: &Graph, weights: &[f64], clique: &[VertexId]) -> (f64, f64) {
307 let mut min_weight = f64::INFINITY;
308 let mut next_weight = f64::INFINITY;
309
310 let n = clique.len();
311 for i in 0..n {
313 for j in (i + 1)..n {
314 let vi = clique[i];
315 let vj = clique[j];
316 if let Some(w) = edge_weight_between(graph, weights, vi, vj) {
317 if w < min_weight {
318 next_weight = min_weight;
319 min_weight = w;
320 } else if w > min_weight && w < next_weight {
321 next_weight = w;
322 }
323 }
324 }
325 }
326
327 (min_weight, next_weight)
328}
329
330fn edge_weight_between(graph: &Graph, weights: &[f64], v1: VertexId, v2: VertexId) -> Option<f64> {
332 if let Ok(edges) = graph.incident(v1) {
333 for eid in edges {
334 if let Ok(other) = graph.edge_other(eid, v1) {
335 if other == v2 {
336 return Some(weights[eid as usize]);
337 }
338 }
339 }
340 }
341 None
342}
343
344#[allow(clippy::cast_possible_truncation)]
348fn build_subclique_graph(
349 graph: &Graph,
350 weights: &[f64],
351 ids: &[VertexId],
352 clique: &[VertexId],
353 next_thr: f64,
354) -> IgraphResult<Option<SubCliqueResult>> {
355 let n = graph.vcount() as usize;
356 let mut in_clique = vec![false; n];
357 for &v in clique {
358 in_clique[v as usize] = true;
359 }
360
361 let mut edges_within: Vec<(VertexId, VertexId, f64)> = Vec::new();
362 for &v in clique {
363 if let Ok(inc) = graph.incident(v) {
364 for eid in inc {
365 if let Ok(other) = graph.edge_other(eid, v) {
366 if other > v && in_clique[other as usize] {
367 edges_within.push((v, other, weights[eid as usize]));
368 }
369 }
370 }
371 }
372 }
373
374 let mut vertex_map: Vec<Option<u32>> = vec![None; n];
376 let mut sub_ids: Vec<VertexId> = Vec::new();
377 let mut sub_edges: Vec<(u32, u32, f64)> = Vec::new();
378 let mut nov: u32 = 0;
379
380 for &(efrom, eto, ew) in &edges_within {
381 if ew >= next_thr {
382 let from_mapped = *vertex_map[efrom as usize].get_or_insert_with(|| {
383 let m = nov;
384 sub_ids.push(ids[efrom as usize]);
385 nov += 1;
386 m
387 });
388 let to_mapped = *vertex_map[eto as usize].get_or_insert_with(|| {
389 let m = nov;
390 sub_ids.push(ids[eto as usize]);
391 nov += 1;
392 m
393 });
394 sub_edges.push((from_mapped, to_mapped, ew));
395 }
396 }
397
398 if nov <= 1 || sub_edges.is_empty() {
399 return Ok(None);
400 }
401
402 let mut sub_g = Graph::with_vertices(nov);
403 let mut sub_w = Vec::with_capacity(sub_edges.len());
404 for (f, t, w) in sub_edges {
405 sub_g.add_edge(f, t)?;
406 sub_w.push(w);
407 }
408
409 Ok(Some((sub_g, sub_w, sub_ids)))
410}
411
412fn graphlets_filter(cliques: &mut Vec<Vec<VertexId>>, thresholds: &mut Vec<f64>) {
415 let n = cliques.len();
416 if n <= 1 {
417 return;
418 }
419
420 let mut order: Vec<usize> = (0..n).collect();
422 order.sort_by(|&a, &b| {
423 thresholds[a]
424 .partial_cmp(&thresholds[b])
425 .unwrap_or(std::cmp::Ordering::Equal)
426 .then_with(|| cliques[a].len().cmp(&cliques[b].len()))
427 });
428
429 let mut to_remove = vec![false; n];
430
431 for ii in 0..n.saturating_sub(1) {
432 let ri = order[ii];
433 if to_remove[ri] {
434 continue;
435 }
436 let thr_i = thresholds[ri];
437
438 for &rj in &order[(ii + 1)..n] {
439 if to_remove[rj] {
440 continue;
441 }
442 let thr_j = thresholds[rj];
443
444 if (thr_j - thr_i).abs() > f64::EPSILON * thr_i.abs().max(1.0) {
445 break;
446 }
447
448 if cliques[ri].len() > cliques[rj].len() {
449 continue;
450 }
451
452 if is_sorted_subset(&cliques[ri], &cliques[rj]) {
454 to_remove[ri] = true;
455 break;
456 }
457 }
458 }
459
460 let mut write = 0;
462 for (read, &remove) in to_remove.iter().enumerate() {
463 if !remove {
464 if write != read {
465 cliques.swap(write, read);
466 thresholds.swap(write, read);
467 }
468 write += 1;
469 }
470 }
471 cliques.truncate(write);
472 thresholds.truncate(write);
473}
474
475fn is_sorted_subset(needle: &[VertexId], hay: &[VertexId]) -> bool {
477 if needle.len() > hay.len() {
478 return false;
479 }
480 let mut pi = 0;
481 let mut pj = 0;
482 let n_i = needle.len();
483 let n_j = hay.len();
484
485 while pi < n_i && pj < n_j && (n_i - pi) <= (n_j - pj) {
486 match needle[pi].cmp(&hay[pj]) {
487 std::cmp::Ordering::Less => return false,
488 std::cmp::Ordering::Greater => pj += 1,
489 std::cmp::Ordering::Equal => {
490 pi += 1;
491 pj += 1;
492 }
493 }
494 }
495 pi == n_i
496}
497
498#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
501fn project_inner(
502 graph: &Graph,
503 weights: &[f64],
504 cliques: &[Vec<VertexId>],
505 mu: &mut [f64],
506 niter: u32,
507) -> IgraphResult<()> {
508 let no_of_edges = graph.ecount();
509 let no_of_nodes = graph.vcount() as usize;
510 let no_cliques = cliques.len();
511
512 let mut vcl: Vec<Vec<usize>> = vec![Vec::new(); no_of_nodes];
514 for (ci, clique) in cliques.iter().enumerate() {
515 for &v in clique {
516 vcl[v as usize].push(ci);
517 }
518 }
519 for v in &mut vcl {
521 v.sort_unstable();
522 }
523
524 let mut ecl: Vec<Vec<usize>> = Vec::with_capacity(no_of_edges);
526 let mut edge_from_to: Vec<(VertexId, VertexId)> = Vec::with_capacity(no_of_edges);
527 for eid in 0..no_of_edges {
528 let (from, to) = graph.edge(eid as u32)?;
529 edge_from_to.push((from, to));
530 let from_cl = &vcl[from as usize];
531 let to_cl = &vcl[to as usize];
532 ecl.push(sorted_intersection(from_cl, to_cl));
533 }
534
535 let mut cel: Vec<Vec<usize>> = vec![Vec::new(); no_cliques];
537 for (eid, cls) in ecl.iter().enumerate() {
538 for &ci in cls {
539 cel[ci].push(eid);
540 }
541 }
542
543 let normfact: Vec<f64> = cliques
545 .iter()
546 .map(|c| {
547 let n = c.len() as f64;
548 n * (n + 1.0) / 2.0
549 })
550 .collect();
551
552 let mut new_weights = vec![0.0_f64; no_of_edges];
554
555 for _iter in 0..niter {
556 for (eid, cls) in ecl.iter().enumerate() {
558 new_weights[eid] = 0.0001;
559 for &ci in cls {
560 new_weights[eid] += mu[ci];
561 }
562 }
563
564 for (ci, edges) in cel.iter().enumerate() {
566 let mut sum_ratio = 0.0_f64;
567 for &eid in edges {
568 sum_ratio += weights[eid] / new_weights[eid];
569 }
570 mu[ci] *= sum_ratio / normfact[ci];
571 }
572 }
573
574 Ok(())
575}
576
577fn sorted_intersection(a: &[usize], b: &[usize]) -> Vec<usize> {
579 let mut result = Vec::new();
580 let (mut i, mut j) = (0, 0);
581 while i < a.len() && j < b.len() {
582 match a[i].cmp(&b[j]) {
583 std::cmp::Ordering::Equal => {
584 result.push(a[i]);
585 i += 1;
586 j += 1;
587 }
588 std::cmp::Ordering::Less => i += 1,
589 std::cmp::Ordering::Greater => j += 1,
590 }
591 }
592 result
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 fn triangle_with_pendant() -> (Graph, Vec<f64>) {
600 let mut g = Graph::with_vertices(4);
602 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(0, 2).unwrap(); g.add_edge(2, 3).unwrap(); (g, vec![1.0, 2.0, 1.0, 3.0])
607 }
608
609 #[test]
610 fn candidate_basis_nonempty() {
611 let (g, w) = triangle_with_pendant();
612 let basis = graphlets_candidate_basis(&g, &w).unwrap();
613 assert!(!basis.cliques.is_empty());
614 assert_eq!(basis.cliques.len(), basis.thresholds.len());
615 }
616
617 #[test]
618 fn candidate_basis_cliques_sorted() {
619 let (g, w) = triangle_with_pendant();
620 let basis = graphlets_candidate_basis(&g, &w).unwrap();
621 for clique in &basis.cliques {
622 let mut sorted = clique.clone();
623 sorted.sort_unstable();
624 assert_eq!(clique, &sorted);
625 }
626 }
627
628 #[test]
629 fn candidate_basis_empty_graph() {
630 let g = Graph::with_vertices(5);
631 let basis = graphlets_candidate_basis(&g, &[]).unwrap();
632 assert!(basis.cliques.is_empty());
633 assert!(basis.thresholds.is_empty());
634 }
635
636 #[test]
637 fn candidate_basis_single_edge() {
638 let mut g = Graph::with_vertices(2);
639 g.add_edge(0, 1).unwrap();
640 let basis = graphlets_candidate_basis(&g, &[5.0]).unwrap();
641 assert_eq!(basis.cliques.len(), 1);
642 assert_eq!(basis.cliques[0], vec![0, 1]);
643 }
644
645 #[test]
646 fn candidate_basis_wrong_weight_length() {
647 let mut g = Graph::with_vertices(3);
648 g.add_edge(0, 1).unwrap();
649 assert!(graphlets_candidate_basis(&g, &[1.0, 2.0]).is_err());
650 }
651
652 #[test]
653 fn candidate_basis_non_simple_graph_errors() {
654 let mut g = Graph::with_vertices(3);
655 g.add_edge(0, 1).unwrap();
656 g.add_edge(0, 1).unwrap(); assert!(graphlets_candidate_basis(&g, &[1.0, 2.0]).is_err());
658 }
659
660 #[test]
661 fn project_produces_correct_length() {
662 let (g, w) = triangle_with_pendant();
663 let basis = graphlets_candidate_basis(&g, &w).unwrap();
664 let mu = graphlets_project(&g, &w, &basis.cliques, None, 100).unwrap();
665 assert_eq!(mu.len(), basis.cliques.len());
666 }
667
668 #[test]
669 fn project_nonnegative_weights() {
670 let (g, w) = triangle_with_pendant();
671 let basis = graphlets_candidate_basis(&g, &w).unwrap();
672 let mu = graphlets_project(&g, &w, &basis.cliques, None, 100).unwrap();
673 for &m in &mu {
674 assert!(m >= 0.0, "mu should be non-negative, got {m}");
675 }
676 }
677
678 #[test]
679 fn project_with_start_mu() {
680 let (g, w) = triangle_with_pendant();
681 let basis = graphlets_candidate_basis(&g, &w).unwrap();
682 let start: Vec<f64> = vec![2.0; basis.cliques.len()];
683 let mu = graphlets_project(&g, &w, &basis.cliques, Some(&start), 100).unwrap();
684 assert_eq!(mu.len(), basis.cliques.len());
685 }
686
687 #[test]
688 fn project_wrong_start_mu_length() {
689 let (g, w) = triangle_with_pendant();
690 let basis = graphlets_candidate_basis(&g, &w).unwrap();
691 assert!(graphlets_project(&g, &w, &basis.cliques, Some(&[1.0]), 100).is_err());
692 }
693
694 #[test]
695 fn graphlets_full_sorted_decreasing() {
696 let (g, w) = triangle_with_pendant();
697 let result = graphlets(&g, &w, 1000).unwrap();
698 assert_eq!(result.cliques.len(), result.mu.len());
699 for pair in result.mu.windows(2) {
700 assert!(
701 pair[0] >= pair[1],
702 "mu should be sorted decreasing: {} < {}",
703 pair[0],
704 pair[1]
705 );
706 }
707 }
708
709 #[test]
710 fn graphlets_full_empty() {
711 let g = Graph::with_vertices(3);
712 let result = graphlets(&g, &[], 100).unwrap();
713 assert!(result.cliques.is_empty());
714 assert!(result.mu.is_empty());
715 }
716
717 #[test]
718 fn graphlets_complete_graph_uniform_weights() {
719 let mut g = Graph::with_vertices(4);
721 let mut w = Vec::new();
722 for i in 0..4u32 {
723 for j in (i + 1)..4 {
724 g.add_edge(i, j).unwrap();
725 w.push(1.0);
726 }
727 }
728 let basis = graphlets_candidate_basis(&g, &w).unwrap();
729 assert!(basis.cliques.iter().any(|c| c.len() == 4));
731 }
732
733 #[test]
734 fn is_sorted_subset_basic() {
735 assert!(is_sorted_subset(&[1, 3], &[1, 2, 3, 4]));
736 assert!(!is_sorted_subset(&[1, 5], &[1, 2, 3, 4]));
737 assert!(is_sorted_subset(&[], &[1, 2, 3]));
738 assert!(!is_sorted_subset(&[1, 2, 3], &[1, 2]));
739 assert!(is_sorted_subset(&[1, 2], &[1, 2]));
740 }
741
742 #[test]
743 fn filter_removes_subsets() {
744 let mut cliques = vec![vec![0, 1], vec![0, 1, 2], vec![3, 4]];
745 let mut thresholds = vec![1.0, 1.0, 2.0];
746 graphlets_filter(&mut cliques, &mut thresholds);
747 assert!(!cliques.contains(&vec![0, 1]));
749 assert!(cliques.contains(&vec![0, 1, 2]));
750 assert!(cliques.contains(&vec![3, 4]));
751 }
752
753 #[test]
754 fn filter_keeps_different_thresholds() {
755 let mut cliques = vec![vec![0, 1], vec![0, 1, 2]];
756 let mut thresholds = vec![1.0, 2.0];
757 graphlets_filter(&mut cliques, &mut thresholds);
758 assert_eq!(cliques.len(), 2);
760 }
761
762 #[test]
763 fn project_zero_iterations() {
764 let (g, w) = triangle_with_pendant();
765 let basis = graphlets_candidate_basis(&g, &w).unwrap();
766 let mu = graphlets_project(&g, &w, &basis.cliques, None, 0).unwrap();
767 for &m in &mu {
769 assert!((m - 1.0).abs() < 1e-9);
770 }
771 }
772
773 #[test]
774 fn graphlets_path_graph() {
775 let mut g = Graph::with_vertices(4);
777 g.add_edge(0, 1).unwrap();
778 g.add_edge(1, 2).unwrap();
779 g.add_edge(2, 3).unwrap();
780 let w = [1.0, 2.0, 3.0];
781 let result = graphlets(&g, &w, 100).unwrap();
782 assert!(!result.cliques.is_empty());
784 for c in &result.cliques {
785 assert_eq!(c.len(), 2);
786 }
787 }
788}