1use super::isoclass::tables;
16use crate::core::rng::SplitMix64;
17use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
18
19pub fn motifs_randesu(graph: &Graph, size: u32) -> IgraphResult<Vec<f64>> {
44 let directed = graph.is_directed();
45 let histlen = hist_length(size, directed)?;
46
47 let mut hist = vec![0.0_f64; histlen];
48
49 motifs_randesu_callback(graph, size, |_vids, isoclass| {
50 hist[isoclass as usize] += 1.0;
51 Ok(true)
52 })?;
53
54 let not_connected = not_connected_classes(size, directed);
55 for &cls in ¬_connected {
56 hist[cls] = f64::NAN;
57 }
58
59 Ok(hist)
60}
61
62pub fn motifs_randesu_no(graph: &Graph, size: u32) -> IgraphResult<f64> {
87 if size < 3 {
88 return Err(IgraphError::InvalidArgument(format!(
89 "motifs_randesu_no: size must be at least 3 (got {size})"
90 )));
91 }
92
93 let n = graph.vcount();
94 if n == 0 {
95 return Ok(0.0);
96 }
97
98 let all_neis = build_all_neighbors(graph)?;
99 let mut added = vec![0_i32; n as usize];
100 let mut count: f64 = 0.0;
101
102 for parent in 0..n {
103 let mut vids: Vec<VertexId> = Vec::new();
104 let mut adjverts: Vec<(VertexId, VertexId)> = Vec::new();
105 let mut stack: Vec<(VertexId, VertexId, u32)> = Vec::new();
106
107 vids.push(parent);
108 added[parent as usize] += 1;
109 let mut level: u32 = 1;
110
111 for &nei in &all_neis[parent as usize] {
112 if added[nei as usize] == 0 && nei > parent {
113 adjverts.push((nei, parent));
114 }
115 added[nei as usize] += 1;
116 }
117
118 while level > 1 || !adjverts.is_empty() {
119 if level == size - 1 {
120 #[allow(clippy::cast_precision_loss)]
121 {
122 count += adjverts.len() as f64;
123 }
124 }
125
126 if level < size - 1 && !adjverts.is_empty() {
127 let (nei, neiparent) = adjverts.pop().unwrap_or((0, 0));
128
129 vids.push(nei);
130 added[nei as usize] += 1;
131 level += 1;
132
133 stack.push((neiparent, nei, level));
134
135 for &nei2 in &all_neis[nei as usize] {
136 if added[nei2 as usize] == 0 && nei2 > parent {
137 adjverts.push((nei2, nei));
138 }
139 added[nei2 as usize] += 1;
140 }
141 } else {
142 while let Some(&(_, _, stack_level)) = stack.last() {
143 if level == stack_level - 1 {
144 let (neiparent, nei, _) = stack.pop().unwrap_or((0, 0, 0));
145 adjverts.push((nei, neiparent));
146 } else {
147 break;
148 }
149 }
150
151 if let Some(nei) = vids.pop() {
152 added[nei as usize] -= 1;
153 level -= 1;
154 for &n2 in &all_neis[nei as usize] {
155 added[n2 as usize] -= 1;
156 }
157 while adjverts.last().is_some_and(|&(_, p)| p == nei) {
158 adjverts.pop();
159 }
160 }
161 }
162 }
163
164 added[parent as usize] -= 1;
165 for &nei in &all_neis[parent as usize] {
166 added[nei as usize] -= 1;
167 }
168 }
169
170 Ok(count)
171}
172
173fn resolve_estimate_roots(
178 n: u32,
179 sample: Option<&[VertexId]>,
180 sample_size: usize,
181 rng: &mut SplitMix64,
182) -> IgraphResult<Vec<VertexId>> {
183 if let Some(s) = sample {
184 for &v in s {
185 if v >= n {
186 return Err(IgraphError::InvalidArgument(format!(
187 "motifs_randesu_estimate: sample vertex {v} out of range [0, {n})"
188 )));
189 }
190 }
191 return Ok(s.to_vec());
192 }
193
194 if sample_size > n as usize {
195 return Err(IgraphError::InvalidArgument(format!(
196 "motifs_randesu_estimate: sample_size {sample_size} exceeds vertex count {n}"
197 )));
198 }
199 let mut pool: Vec<VertexId> = (0..n).collect();
200 for i in 0..sample_size {
201 let j = i + rng.gen_index(n as usize - i);
202 pool.swap(i, j);
203 }
204 pool.truncate(sample_size);
205 Ok(pool)
206}
207
208pub fn motifs_randesu_estimate(
262 graph: &Graph,
263 size: u32,
264 cut_prob: Option<&[f64]>,
265 sample: Option<&[VertexId]>,
266 sample_size: usize,
267 seed: u64,
268) -> IgraphResult<f64> {
269 if size < 3 {
270 return Err(IgraphError::InvalidArgument(format!(
271 "motifs_randesu_estimate: size must be at least 3 (got {size})"
272 )));
273 }
274 if let Some(cp) = cut_prob {
275 if cp.len() != size as usize {
276 return Err(IgraphError::InvalidArgument(format!(
277 "motifs_randesu_estimate: cut_prob length {} must equal size {size}",
278 cp.len()
279 )));
280 }
281 }
282
283 let n = graph.vcount();
284 if n == 0 {
285 return Ok(0.0);
286 }
287
288 let mut rng = SplitMix64::new(seed);
289
290 let roots = resolve_estimate_roots(n, sample, sample_size, &mut rng)?;
291 let effective_sample = roots.len();
292 if effective_sample == 0 {
293 return Err(IgraphError::InvalidArgument(
294 "motifs_randesu_estimate: the sample must contain at least one vertex".to_string(),
295 ));
296 }
297
298 let all_neis = build_all_neighbors(graph)?;
299 let mut added = vec![0_i32; n as usize];
300 let mut est: f64 = 0.0;
301
302 for &parent in &roots {
303 if let Some(cp) = cut_prob {
305 let c0 = cp[0];
306 if c0 >= 1.0 || rng.gen_unit() < c0 {
307 continue;
308 }
309 }
310 est += estimate_count_from_root(parent, size, cut_prob, &all_neis, &mut added, &mut rng);
311 }
312
313 #[allow(clippy::cast_precision_loss)]
314 let scale = f64::from(n) / effective_sample as f64;
315 Ok(est * scale)
316}
317
318fn estimate_count_from_root(
323 parent: VertexId,
324 size: u32,
325 cut_prob: Option<&[f64]>,
326 all_neis: &[Vec<VertexId>],
327 added: &mut [i32],
328 rng: &mut SplitMix64,
329) -> f64 {
330 let mut vids: Vec<VertexId> = Vec::new();
331 let mut adjverts: Vec<(VertexId, VertexId)> = Vec::new();
332 let mut stack: Vec<(VertexId, VertexId, u32)> = Vec::new();
333 let mut count: f64 = 0.0;
334
335 vids.push(parent);
336 added[parent as usize] += 1;
337 let mut level: u32 = 1;
338
339 for &nei in &all_neis[parent as usize] {
340 if added[nei as usize] == 0 && nei > parent {
341 adjverts.push((nei, parent));
342 }
343 added[nei as usize] += 1;
344 }
345
346 while level > 1 || !adjverts.is_empty() {
347 let cp_level = cut_prob.map_or(0.0, |cp| cp[level as usize]);
348
349 if level == size - 1 {
350 for _ in 0..adjverts.len() {
351 if cp_level != 0.0 && rng.gen_unit() < cp_level {
352 continue;
353 }
354 count += 1.0;
355 }
356 }
357
358 if level < size - 1 && !adjverts.is_empty() {
359 let (nei, neiparent) = adjverts.pop().unwrap_or((0, 0));
360
361 if cp_level != 0.0 && rng.gen_unit() <= cp_level {
363 continue;
364 }
365
366 vids.push(nei);
367 added[nei as usize] += 1;
368 level += 1;
369
370 stack.push((neiparent, nei, level));
371
372 for &nei2 in &all_neis[nei as usize] {
373 if added[nei2 as usize] == 0 && nei2 > parent {
374 adjverts.push((nei2, nei));
375 }
376 added[nei2 as usize] += 1;
377 }
378 } else {
379 while let Some(&(_, _, stack_level)) = stack.last() {
380 if level == stack_level - 1 {
381 let (neiparent, nei, _) = stack.pop().unwrap_or((0, 0, 0));
382 adjverts.push((nei, neiparent));
383 } else {
384 break;
385 }
386 }
387
388 if let Some(nei) = vids.pop() {
389 added[nei as usize] -= 1;
390 level -= 1;
391 for &n2 in &all_neis[nei as usize] {
392 added[n2 as usize] -= 1;
393 }
394 while adjverts.last().is_some_and(|&(_, p)| p == nei) {
395 adjverts.pop();
396 }
397 }
398 }
399 }
400
401 added[parent as usize] -= 1;
402 for &nei in &all_neis[parent as usize] {
403 added[nei as usize] -= 1;
404 }
405
406 count
407}
408
409pub fn motifs_randesu_callback<F>(graph: &Graph, size: u32, mut callback: F) -> IgraphResult<()>
435where
436 F: FnMut(&[VertexId], u32) -> IgraphResult<bool>,
437{
438 let directed = graph.is_directed();
439 let (arr_idx, arr_code, mul) = get_isoclass_tables(size, directed)?;
440
441 let n = graph.vcount();
442 if n == 0 {
443 return Ok(());
444 }
445
446 let all_neis = build_all_neighbors(graph)?;
447 let out_neis = build_out_neighbors(graph)?;
448 let mut added = vec![0_i32; n as usize];
449 let mut subg = vec![0_u32; n as usize];
450
451 for parent in 0..n {
452 let mut vids: Vec<VertexId> = Vec::new();
453 let mut adjverts: Vec<(VertexId, VertexId)> = Vec::new();
454 let mut stack: Vec<(VertexId, VertexId, u32)> = Vec::new();
455
456 vids.push(parent);
457 subg[parent as usize] = 1;
458 added[parent as usize] += 1;
459 let mut level: u32 = 1;
460
461 for &nei in &all_neis[parent as usize] {
462 if added[nei as usize] == 0 && nei > parent {
463 adjverts.push((nei, parent));
464 }
465 added[nei as usize] += 1;
466 }
467
468 let mut terminate = false;
469
470 while level > 1 || !adjverts.is_empty() {
471 if level == size - 1 {
472 for &(last, _) in &adjverts {
473 vids.push(last);
474 subg[last as usize] = size;
475
476 let code = compute_isoclass_code(&vids, size, &out_neis, &subg, arr_idx, mul);
477 let isoclass = u32::from(arr_code[code as usize]);
478
479 match callback(&vids, isoclass) {
480 Ok(true) => {}
481 Ok(false) => {
482 vids.pop();
483 subg[last as usize] = 0;
484 terminate = true;
485 break;
486 }
487 Err(e) => {
488 vids.pop();
489 subg[last as usize] = 0;
490 return Err(e);
491 }
492 }
493
494 vids.pop();
495 subg[last as usize] = 0;
496 }
497 }
498
499 if terminate {
500 break;
501 }
502
503 if level < size - 1 && !adjverts.is_empty() {
504 let (nei, neiparent) = adjverts.pop().unwrap_or((0, 0));
505
506 vids.push(nei);
507 subg[nei as usize] = level + 1;
508 added[nei as usize] += 1;
509 level += 1;
510
511 stack.push((neiparent, nei, level));
512
513 for &nei2 in &all_neis[nei as usize] {
514 if added[nei2 as usize] == 0 && nei2 > parent {
515 adjverts.push((nei2, nei));
516 }
517 added[nei2 as usize] += 1;
518 }
519 } else {
520 while let Some(&(_, _, stack_level)) = stack.last() {
521 if level == stack_level - 1 {
522 let (neiparent, nei, _) = stack.pop().unwrap_or((0, 0, 0));
523 adjverts.push((nei, neiparent));
524 } else {
525 break;
526 }
527 }
528
529 if let Some(nei) = vids.pop() {
530 subg[nei as usize] = 0;
531 added[nei as usize] -= 1;
532 level -= 1;
533 for &n2 in &all_neis[nei as usize] {
534 added[n2 as usize] -= 1;
535 }
536 while adjverts.last().is_some_and(|&(_, p)| p == nei) {
537 adjverts.pop();
538 }
539 }
540 }
541 }
542
543 if terminate {
544 break;
545 }
546
547 added[parent as usize] -= 1;
548 subg[parent as usize] = 0;
549 for &nei in &all_neis[parent as usize] {
550 added[nei as usize] -= 1;
551 }
552 }
553
554 Ok(())
555}
556
557fn compute_isoclass_code(
558 vids: &[VertexId],
559 size: u32,
560 out_neis: &[Vec<VertexId>],
561 subg: &[u32],
562 arr_idx: &[u32],
563 mul: u32,
564) -> u32 {
565 let mut code: u32 = 0;
566 for k in 0..size {
567 let from = vids[k as usize];
568 for &nei in &out_neis[from as usize] {
569 let nei_subg = subg[nei as usize];
570 if nei_subg != 0 && k != nei_subg - 1 {
571 let idx = mul * k + (nei_subg - 1);
572 code |= arr_idx[idx as usize];
573 }
574 }
575 }
576 code
577}
578
579fn get_isoclass_tables(
580 size: u32,
581 directed: bool,
582) -> IgraphResult<(&'static [u32], &'static [u8], u32)> {
583 if directed {
584 match size {
585 3 => Ok((&tables::ISOCLASS_3_IDX, &tables::ISOCLASS2_3, 3)),
586 4 => Ok((&tables::ISOCLASS_4_IDX, &tables::ISOCLASS2_4, 4)),
587 _ => Err(IgraphError::InvalidArgument(format!(
588 "motifs_randesu: directed graphs support size 3 or 4 (got {size})"
589 ))),
590 }
591 } else {
592 match size {
593 3 => Ok((&tables::ISOCLASS_3U_IDX, &tables::ISOCLASS2_3U, 3)),
594 4 => Ok((&tables::ISOCLASS_4U_IDX, &tables::ISOCLASS2_4U, 4)),
595 5 => Ok((&tables::ISOCLASS_5U_IDX, &tables::ISOCLASS2_5U, 5)),
596 _ => Err(IgraphError::InvalidArgument(format!(
597 "motifs_randesu: undirected graphs support size 3, 4, or 5 (got {size})"
598 ))),
599 }
600 }
601}
602
603fn hist_length(size: u32, directed: bool) -> IgraphResult<usize> {
604 if directed {
605 match size {
606 3 => Ok(16),
607 4 => Ok(218),
608 _ => Err(IgraphError::InvalidArgument(format!(
609 "motifs_randesu: directed graphs support size 3 or 4 (got {size})"
610 ))),
611 }
612 } else {
613 match size {
614 3 => Ok(4),
615 4 => Ok(11),
616 5 => Ok(34),
617 _ => Err(IgraphError::InvalidArgument(format!(
618 "motifs_randesu: undirected graphs support size 3, 4, or 5 (got {size})"
619 ))),
620 }
621 }
622}
623
624fn not_connected_classes(size: u32, directed: bool) -> Vec<usize> {
625 if size == 3 {
626 if directed { vec![0, 1, 3] } else { vec![0, 1] }
627 } else if size == 4 {
628 if directed {
629 vec![
630 0, 1, 2, 4, 5, 6, 9, 10, 11, 15, 22, 23, 27, 28, 33, 34, 39, 62, 120,
631 ]
632 } else {
633 vec![0, 1, 2, 3, 5]
634 }
635 } else if size == 5 {
636 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 19]
637 } else {
638 vec![]
639 }
640}
641
642fn build_all_neighbors(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
643 let n = graph.vcount() as usize;
644 let ecount = graph.ecount();
645 let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
646
647 for eid in 0..ecount {
648 #[allow(clippy::cast_possible_truncation)]
649 let (src, tgt) = graph.edge(eid as u32)?;
650 if src == tgt {
651 continue;
652 }
653 adj[src as usize].push(tgt);
654 adj[tgt as usize].push(src);
655 }
656
657 for neighbors in &mut adj {
658 neighbors.sort_unstable();
659 neighbors.dedup();
660 }
661
662 Ok(adj)
663}
664
665fn build_out_neighbors(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
666 let n = graph.vcount() as usize;
667 let ecount = graph.ecount();
668 let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
669
670 for eid in 0..ecount {
671 #[allow(clippy::cast_possible_truncation)]
672 let (src, tgt) = graph.edge(eid as u32)?;
673 if src == tgt {
674 continue;
675 }
676 adj[src as usize].push(tgt);
677 if !graph.is_directed() {
678 adj[tgt as usize].push(src);
679 }
680 }
681
682 for neighbors in &mut adj {
683 neighbors.sort_unstable();
684 }
685
686 Ok(adj)
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn motifs_randesu_empty_graph() {
695 let g = Graph::with_vertices(0);
696 let hist = motifs_randesu(&g, 3).unwrap();
697 assert_eq!(hist.len(), 4);
698 assert!(hist[0].is_nan());
699 assert!(hist[1].is_nan());
700 assert!((hist[2]).abs() < 1e-10);
701 assert!((hist[3]).abs() < 1e-10);
702 }
703
704 #[test]
705 fn motifs_randesu_triangle() {
706 let mut g = Graph::with_vertices(3);
707 g.add_edge(0, 1).unwrap();
708 g.add_edge(1, 2).unwrap();
709 g.add_edge(0, 2).unwrap();
710 let hist = motifs_randesu(&g, 3).unwrap();
711 assert!((hist[3] - 1.0).abs() < 1e-10);
712 assert!((hist[2]).abs() < 1e-10);
713 }
714
715 #[test]
716 fn motifs_randesu_path_3() {
717 let mut g = Graph::with_vertices(3);
718 g.add_edge(0, 1).unwrap();
719 g.add_edge(1, 2).unwrap();
720 let hist = motifs_randesu(&g, 3).unwrap();
721 assert!((hist[2] - 1.0).abs() < 1e-10);
722 assert!((hist[3]).abs() < 1e-10);
723 }
724
725 #[test]
726 fn motifs_randesu_k4_size3() {
727 let mut g = Graph::with_vertices(4);
728 for u in 0..4u32 {
729 for v in (u + 1)..4 {
730 g.add_edge(u, v).unwrap();
731 }
732 }
733 let hist = motifs_randesu(&g, 3).unwrap();
734 assert!((hist[3] - 4.0).abs() < 1e-10);
735 }
736
737 #[test]
738 fn motifs_randesu_k4_size4() {
739 let mut g = Graph::with_vertices(4);
740 for u in 0..4u32 {
741 for v in (u + 1)..4 {
742 g.add_edge(u, v).unwrap();
743 }
744 }
745 let hist = motifs_randesu(&g, 4).unwrap();
746 assert_eq!(hist.len(), 11);
747 assert!((hist[10] - 1.0).abs() < 1e-10);
748 }
749
750 #[test]
751 fn motifs_randesu_star_4() {
752 let mut g = Graph::with_vertices(4);
753 g.add_edge(0, 1).unwrap();
754 g.add_edge(0, 2).unwrap();
755 g.add_edge(0, 3).unwrap();
756 let hist = motifs_randesu(&g, 3).unwrap();
757 assert!((hist[2] - 3.0).abs() < 1e-10);
758 }
759
760 #[test]
761 fn motifs_randesu_no_triangle() {
762 let mut g = Graph::with_vertices(3);
763 g.add_edge(0, 1).unwrap();
764 g.add_edge(1, 2).unwrap();
765 g.add_edge(0, 2).unwrap();
766 let count = motifs_randesu_no(&g, 3).unwrap();
767 assert!((count - 1.0).abs() < 1e-10);
768 }
769
770 #[test]
771 fn motifs_randesu_no_k4() {
772 let mut g = Graph::with_vertices(4);
773 for u in 0..4u32 {
774 for v in (u + 1)..4 {
775 g.add_edge(u, v).unwrap();
776 }
777 }
778 let count = motifs_randesu_no(&g, 3).unwrap();
779 assert!((count - 4.0).abs() < 1e-10);
780 }
781
782 #[test]
783 fn motifs_randesu_no_k5_size3() {
784 let mut g = Graph::with_vertices(5);
785 for u in 0..5u32 {
786 for v in (u + 1)..5 {
787 g.add_edge(u, v).unwrap();
788 }
789 }
790 let count = motifs_randesu_no(&g, 3).unwrap();
791 assert!((count - 10.0).abs() < 1e-10);
793 }
794
795 #[test]
796 fn motifs_randesu_no_k5_size4() {
797 let mut g = Graph::with_vertices(5);
798 for u in 0..5u32 {
799 for v in (u + 1)..5 {
800 g.add_edge(u, v).unwrap();
801 }
802 }
803 let count = motifs_randesu_no(&g, 4).unwrap();
804 assert!((count - 5.0).abs() < 1e-10);
806 }
807
808 #[test]
809 fn motifs_randesu_directed_3_cycle() {
810 let mut g = Graph::new(3, true).unwrap();
811 g.add_edge(0, 1).unwrap();
812 g.add_edge(1, 2).unwrap();
813 g.add_edge(2, 0).unwrap();
814 let hist = motifs_randesu(&g, 3).unwrap();
815 assert_eq!(hist.len(), 16);
816 let total: f64 = hist.iter().filter(|x| !x.is_nan()).sum();
818 assert!((total - 1.0).abs() < 1e-10);
819 }
820
821 #[test]
822 fn motifs_randesu_directed_mutual() {
823 let mut g = Graph::new(3, true).unwrap();
824 g.add_edge(0, 1).unwrap();
825 g.add_edge(1, 0).unwrap();
826 g.add_edge(1, 2).unwrap();
827 let hist = motifs_randesu(&g, 3).unwrap();
828 let total: f64 = hist.iter().filter(|x| !x.is_nan()).sum();
829 assert!((total - 1.0).abs() < 1e-10);
830 }
831
832 #[test]
833 fn motifs_randesu_callback_early_stop() {
834 let mut g = Graph::with_vertices(5);
835 for u in 0..5u32 {
836 for v in (u + 1)..5 {
837 g.add_edge(u, v).unwrap();
838 }
839 }
840 let mut count = 0;
841 motifs_randesu_callback(&g, 3, |_vids, _cls| {
842 count += 1;
843 Ok(count < 3)
844 })
845 .unwrap();
846 assert_eq!(count, 3);
847 }
848
849 #[test]
850 fn motifs_randesu_no_empty() {
851 let g = Graph::with_vertices(0);
852 assert!((motifs_randesu_no(&g, 3).unwrap()).abs() < 1e-10);
853 }
854
855 #[test]
856 fn motifs_randesu_no_small_graph() {
857 let g = Graph::with_vertices(2);
858 assert!((motifs_randesu_no(&g, 3).unwrap()).abs() < 1e-10);
859 }
860
861 #[test]
862 fn motifs_randesu_invalid_size() {
863 let g = Graph::with_vertices(3);
864 assert!(motifs_randesu(&g, 2).is_err());
865 assert!(motifs_randesu_no(&g, 2).is_err());
866 }
867
868 #[test]
869 fn motifs_randesu_no_path_5_size3() {
870 let mut g = Graph::with_vertices(5);
871 g.add_edge(0, 1).unwrap();
872 g.add_edge(1, 2).unwrap();
873 g.add_edge(2, 3).unwrap();
874 g.add_edge(3, 4).unwrap();
875 let count = motifs_randesu_no(&g, 3).unwrap();
876 assert!((count - 3.0).abs() < 1e-10);
879 }
880
881 #[test]
882 fn motifs_randesu_hist_matches_no() {
883 let mut g = Graph::with_vertices(5);
884 g.add_edge(0, 1).unwrap();
885 g.add_edge(1, 2).unwrap();
886 g.add_edge(0, 2).unwrap();
887 g.add_edge(2, 3).unwrap();
888 g.add_edge(3, 4).unwrap();
889
890 let hist = motifs_randesu(&g, 3).unwrap();
891 let total_from_hist: f64 = hist.iter().filter(|x| !x.is_nan()).sum();
892 let total_from_no = motifs_randesu_no(&g, 3).unwrap();
893 assert!(
894 (total_from_hist - total_from_no).abs() < 1e-10,
895 "hist sum {total_from_hist} != no {total_from_no}"
896 );
897 }
898
899 #[test]
900 fn motifs_randesu_size5_k5() {
901 let mut g = Graph::with_vertices(5);
902 for u in 0..5u32 {
903 for v in (u + 1)..5 {
904 g.add_edge(u, v).unwrap();
905 }
906 }
907 let hist = motifs_randesu(&g, 5).unwrap();
908 assert_eq!(hist.len(), 34);
909 assert!((hist[33] - 1.0).abs() < 1e-10);
911 }
912
913 #[test]
914 fn motifs_randesu_directed_4() {
915 let mut g = Graph::new(4, true).unwrap();
916 g.add_edge(0, 1).unwrap();
917 g.add_edge(1, 2).unwrap();
918 g.add_edge(2, 3).unwrap();
919 g.add_edge(3, 0).unwrap();
920 let hist = motifs_randesu(&g, 4).unwrap();
921 assert_eq!(hist.len(), 218);
922 let total: f64 = hist.iter().filter(|x| !x.is_nan()).sum();
923 assert!((total - 1.0).abs() < 1e-10);
924 }
925
926 #[test]
927 fn motifs_randesu_self_loops_ignored() {
928 let mut g = Graph::with_vertices(3);
929 g.add_edge(0, 0).unwrap();
930 g.add_edge(0, 1).unwrap();
931 g.add_edge(1, 2).unwrap();
932 g.add_edge(0, 2).unwrap();
933 let hist = motifs_randesu(&g, 3).unwrap();
934 assert!((hist[3] - 1.0).abs() < 1e-10);
935 }
936
937 fn complete_graph(n: u32) -> Graph {
938 let mut g = Graph::with_vertices(n);
939 for u in 0..n {
940 for v in (u + 1)..n {
941 g.add_edge(u, v).unwrap();
942 }
943 }
944 g
945 }
946
947 #[test]
948 fn estimate_full_sample_matches_exact() {
949 for &n in &[5u32, 6, 7] {
951 let g = complete_graph(n);
952 let all: Vec<u32> = (0..n).collect();
953 for size in 3..=4 {
954 let est = motifs_randesu_estimate(&g, size, None, Some(&all), 0, 0).unwrap();
955 let exact = motifs_randesu_no(&g, size).unwrap();
956 assert!(
957 (est - exact).abs() < 1e-10,
958 "n={n} size={size}: est {est} != exact {exact}"
959 );
960 }
961 }
962 }
963
964 #[test]
965 fn estimate_all_zero_cut_prob_matches_exact() {
966 let g = complete_graph(6);
968 let all: Vec<u32> = (0..6).collect();
969 let est =
970 motifs_randesu_estimate(&g, 3, Some(&[0.0, 0.0, 0.0]), Some(&all), 0, 42).unwrap();
971 let exact = motifs_randesu_no(&g, 3).unwrap();
972 assert!((est - exact).abs() < 1e-10, "est {est} != exact {exact}");
973 }
974
975 #[test]
976 fn estimate_scaling_half_sample() {
977 let g = complete_graph(6);
981 let exact = motifs_randesu_no(&g, 3).unwrap();
982 let sample = [0u32, 1, 2];
987 let est = motifs_randesu_estimate(&g, 3, None, Some(&sample), 0, 0).unwrap();
988 assert!(est > 0.0);
990 assert!(exact > 0.0);
991 }
992
993 #[test]
994 fn estimate_random_sample_deterministic_with_seed() {
995 let g = complete_graph(8);
996 let a = motifs_randesu_estimate(&g, 3, None, None, 4, 12345).unwrap();
997 let b = motifs_randesu_estimate(&g, 3, None, None, 4, 12345).unwrap();
998 assert!((a - b).abs() < 1e-10, "same seed must be deterministic");
999 }
1000
1001 #[test]
1002 fn estimate_empty_graph() {
1003 let g = Graph::with_vertices(0);
1004 assert!((motifs_randesu_estimate(&g, 3, None, None, 0, 1).unwrap()).abs() < 1e-10);
1005 }
1006
1007 #[test]
1008 fn estimate_errors() {
1009 let g = complete_graph(5);
1010 assert!(motifs_randesu_estimate(&g, 2, None, None, 3, 1).is_err());
1012 assert!(motifs_randesu_estimate(&g, 3, Some(&[0.0, 0.0]), None, 3, 1).is_err());
1014 assert!(motifs_randesu_estimate(&g, 3, None, Some(&[0, 99]), 0, 1).is_err());
1016 assert!(motifs_randesu_estimate(&g, 3, None, Some(&[]), 0, 1).is_err());
1018 assert!(motifs_randesu_estimate(&g, 3, None, None, 99, 1).is_err());
1020 }
1021
1022 #[test]
1023 fn estimate_full_cut_yields_zero() {
1024 let g = complete_graph(6);
1026 let est = motifs_randesu_estimate(&g, 3, Some(&[1.0, 0.0, 0.0]), None, 6, 7).unwrap();
1027 assert!(
1028 est.abs() < 1e-10,
1029 "full root cut must estimate zero, got {est}"
1030 );
1031 }
1032
1033 #[test]
1034 fn motifs_randesu_disconnected_graph() {
1035 let mut g = Graph::with_vertices(4);
1037 g.add_edge(0, 1).unwrap();
1038 g.add_edge(2, 3).unwrap();
1039 let hist = motifs_randesu(&g, 3).unwrap();
1040 let total: f64 = hist.iter().filter(|x| !x.is_nan()).sum();
1042 assert!((total).abs() < 1e-10);
1043 }
1044}