1#![allow(
40 clippy::cast_possible_truncation,
41 clippy::cast_possible_wrap,
42 clippy::cast_sign_loss
43)]
44
45use crate::core::graph::EdgeId;
46use crate::core::{Graph, IgraphError, IgraphResult};
47
48#[derive(Debug, Clone)]
50pub struct Vf2Isomorphism {
51 pub iso: bool,
53 pub map12: Vec<u32>,
56 pub map21: Vec<u32>,
59}
60
61pub(crate) fn adjacency(graph: &Graph, incoming: bool) -> IgraphResult<Vec<Vec<u32>>> {
68 let n = graph.vcount();
69 let mut adj: Vec<Vec<u32>> = Vec::with_capacity(n as usize);
70 for v in 0..n {
71 let mut neis: Vec<u32> = if !graph.is_directed() {
72 graph.neighbors(v)?
74 } else if incoming {
75 let mut s = Vec::new();
77 for eid in graph.incident_in(v)? {
78 s.push(graph.edge_source(eid)?);
79 }
80 s
81 } else {
82 graph.neighbors(v)?
84 };
85 neis.retain(|&u| u != v); neis.sort_unstable();
87 neis.dedup(); adj.push(neis);
89 }
90 Ok(adj)
91}
92
93pub(crate) fn out_degree(graph: &Graph, v: u32) -> IgraphResult<i64> {
96 if graph.is_directed() {
97 Ok(graph.incident(v)?.len() as i64)
98 } else {
99 Ok(graph.degree(v)? as i64)
100 }
101}
102
103pub(crate) fn in_degree(graph: &Graph, v: u32) -> IgraphResult<i64> {
105 if graph.is_directed() {
106 Ok(graph.incident_in(v)?.len() as i64)
107 } else {
108 Ok(graph.degree(v)? as i64)
109 }
110}
111
112pub(crate) fn contains_sorted(slice: &[u32], x: u32) -> bool {
114 slice.binary_search(&x).is_ok()
115}
116
117fn same_color_distribution(a: &[u32], b: &[u32]) -> bool {
120 let mut a = a.to_vec();
121 let mut b = b.to_vec();
122 a.sort_unstable();
123 b.sort_unstable();
124 a == b
125}
126
127pub(crate) enum Flow {
129 Continue,
131 Stop,
133}
134
135pub(crate) fn perform_pre_checks(graph1: &Graph, graph2: &Graph) -> IgraphResult<()> {
136 if graph1.is_directed() != graph2.is_directed() {
137 return Err(IgraphError::InvalidArgument(
138 "cannot compare directed and undirected graphs".into(),
139 ));
140 }
141 let has_loops = graph_has_loop(graph1)? || graph_has_loop(graph2)?;
142 if has_loops {
143 return Err(IgraphError::InvalidArgument(
144 "the VF2 algorithm does not support graphs with loop edges".into(),
145 ));
146 }
147 Ok(())
148}
149
150fn graph_has_loop(graph: &Graph) -> IgraphResult<bool> {
151 for e in 0..graph.ecount() {
152 let eid =
153 EdgeId::try_from(e).map_err(|_| IgraphError::Internal("vf2: edge id overflow"))?;
154 let (from, to) = graph.edge(eid)?;
155 if from == to {
156 return Ok(true);
157 }
158 }
159 Ok(false)
160}
161
162#[allow(clippy::too_many_arguments)]
169#[allow(clippy::too_many_lines)]
170fn vf2_engine(
171 graph1: &Graph,
172 graph2: &Graph,
173 mut vertex_color1: Option<&[u32]>,
174 mut vertex_color2: Option<&[u32]>,
175 mut edge_color1: Option<&[u32]>,
176 mut edge_color2: Option<&[u32]>,
177 mut isohandler: impl FnMut(&[i64], &[i64]) -> Flow,
178) -> IgraphResult<()> {
179 perform_pre_checks(graph1, graph2)?;
180
181 let no_of_nodes = i64::from(graph1.vcount());
182 let no_of_edges = graph1.ecount() as i64;
183
184 if vertex_color1.is_some() != vertex_color2.is_some() {
186 vertex_color1 = None;
187 vertex_color2 = None;
188 }
189 if edge_color1.is_some() != edge_color2.is_some() {
190 edge_color1 = None;
191 edge_color2 = None;
192 }
193
194 if no_of_nodes != i64::from(graph2.vcount()) || no_of_edges != graph2.ecount() as i64 {
196 return Ok(());
197 }
198
199 if let (Some(c1), Some(c2)) = (vertex_color1, vertex_color2) {
200 if c1.len() as i64 != no_of_nodes || c2.len() as i64 != no_of_nodes {
201 return Err(IgraphError::InvalidArgument(
202 "invalid vertex color vector length".into(),
203 ));
204 }
205 if !same_color_distribution(c1, c2) {
206 return Ok(());
207 }
208 }
209 if let (Some(c1), Some(c2)) = (edge_color1, edge_color2) {
210 if c1.len() as i64 != no_of_edges || c2.len() as i64 != no_of_edges {
211 return Err(IgraphError::InvalidArgument(
212 "invalid edge color vector length".into(),
213 ));
214 }
215 if !same_color_distribution(c1, c2) {
216 return Ok(());
217 }
218 }
219
220 let n = no_of_nodes as usize;
221
222 let inneis_1 = adjacency(graph1, true)?;
223 let outneis_1 = adjacency(graph1, false)?;
224 let inneis_2 = adjacency(graph2, true)?;
225 let outneis_2 = adjacency(graph2, false)?;
226
227 let mut indeg1 = vec![0i64; n];
228 let mut indeg2 = vec![0i64; n];
229 let mut outdeg1 = vec![0i64; n];
230 let mut outdeg2 = vec![0i64; n];
231 for v in 0..no_of_nodes {
232 let vu = v as usize;
233 indeg1[vu] = in_degree(graph1, v as u32)?;
234 indeg2[vu] = in_degree(graph2, v as u32)?;
235 outdeg1[vu] = out_degree(graph1, v as u32)?;
236 outdeg2[vu] = out_degree(graph2, v as u32)?;
237 }
238
239 let mut core_1 = vec![-1i64; n];
241 let mut core_2 = vec![-1i64; n];
242 let mut in_1 = vec![0i64; n];
244 let mut in_2 = vec![0i64; n];
245 let mut out_1 = vec![0i64; n];
246 let mut out_2 = vec![0i64; n];
247 let mut in_1_size = 0i64;
248 let mut in_2_size = 0i64;
249 let mut out_1_size = 0i64;
250 let mut out_2_size = 0i64;
251
252 let mut path: Vec<i64> = Vec::with_capacity(2 * n);
254 let mut matched_nodes = 0i64;
255 let mut depth = 0i64;
256 let mut last1 = -1i64;
257 let mut last2 = -1i64;
258
259 while depth >= 0 {
260 let mut cand1 = -1i64;
261 let mut cand2 = -1i64;
262
263 if in_1_size != in_2_size || out_1_size != out_2_size {
265 } else if out_1_size > 0 && out_2_size > 0 {
267 if last2 >= 0 {
268 cand2 = last2;
269 } else {
270 let mut i = 0i64;
271 while cand2 < 0 && i < no_of_nodes {
272 if out_2[i as usize] > 0 && core_2[i as usize] < 0 {
273 cand2 = i;
274 }
275 i += 1;
276 }
277 }
278 let mut i = last1 + 1;
279 while cand1 < 0 && i < no_of_nodes {
280 if out_1[i as usize] > 0 && core_1[i as usize] < 0 {
281 cand1 = i;
282 }
283 i += 1;
284 }
285 } else if in_1_size > 0 && in_2_size > 0 {
286 if last2 >= 0 {
287 cand2 = last2;
288 } else {
289 let mut i = 0i64;
290 while cand2 < 0 && i < no_of_nodes {
291 if in_2[i as usize] > 0 && core_2[i as usize] < 0 {
292 cand2 = i;
293 }
294 i += 1;
295 }
296 }
297 let mut i = last1 + 1;
298 while cand1 < 0 && i < no_of_nodes {
299 if in_1[i as usize] > 0 && core_1[i as usize] < 0 {
300 cand1 = i;
301 }
302 i += 1;
303 }
304 } else {
305 if last2 >= 0 {
306 cand2 = last2;
307 } else {
308 let mut i = 0i64;
309 while cand2 < 0 && i < no_of_nodes {
310 if core_2[i as usize] < 0 {
311 cand2 = i;
312 }
313 i += 1;
314 }
315 }
316 let mut i = last1 + 1;
317 while cand1 < 0 && i < no_of_nodes {
318 if core_1[i as usize] < 0 {
319 cand1 = i;
320 }
321 i += 1;
322 }
323 }
324
325 if cand1 < 0 || cand2 < 0 {
326 if depth >= 1 {
328 last2 = path.pop().ok_or(IgraphError::Internal("vf2: empty path"))?;
329 last1 = path.pop().ok_or(IgraphError::Internal("vf2: empty path"))?;
330 let l1 = last1 as usize;
331 let l2 = last2 as usize;
332 matched_nodes -= 1;
333 core_1[l1] = -1;
334 core_2[l2] = -1;
335
336 if in_1[l1] != 0 {
337 in_1_size += 1;
338 }
339 if out_1[l1] != 0 {
340 out_1_size += 1;
341 }
342 if in_2[l2] != 0 {
343 in_2_size += 1;
344 }
345 if out_2[l2] != 0 {
346 out_2_size += 1;
347 }
348
349 for &node in &inneis_1[l1] {
350 if in_1[node as usize] == depth {
351 in_1[node as usize] = 0;
352 in_1_size -= 1;
353 }
354 }
355 for &node in &outneis_1[l1] {
356 if out_1[node as usize] == depth {
357 out_1[node as usize] = 0;
358 out_1_size -= 1;
359 }
360 }
361 for &node in &inneis_2[l2] {
362 if in_2[node as usize] == depth {
363 in_2[node as usize] = 0;
364 in_2_size -= 1;
365 }
366 }
367 for &node in &outneis_2[l2] {
368 if out_2[node as usize] == depth {
369 out_2[node as usize] = 0;
370 out_2_size -= 1;
371 }
372 }
373 }
374 depth -= 1;
375 } else {
376 let c1 = cand1 as usize;
378 let c2 = cand2 as usize;
379 let mut xin1 = 0i64;
380 let mut xin2 = 0i64;
381 let mut xout1 = 0i64;
382 let mut xout2 = 0i64;
383 let mut end = false;
384
385 if indeg1[c1] != indeg2[c2] || outdeg1[c1] != outdeg2[c2] {
386 end = true;
387 }
388 if let (Some(vc1), Some(vc2)) = (vertex_color1, vertex_color2) {
389 if vc1[c1] != vc2[c2] {
390 end = true;
391 }
392 }
393
394 for &node in &inneis_1[c1] {
396 if end {
397 break;
398 }
399 let nu = node as usize;
400 if core_1[nu] >= 0 {
401 let node2 = core_1[nu];
402 if !contains_sorted(&inneis_2[c2], node2 as u32) {
403 end = true;
404 } else if edge_color1.is_some() {
405 let eid1 = graph1.get_eid(node, cand1 as u32)? as usize;
406 let eid2 = graph2.get_eid(node2 as u32, cand2 as u32)? as usize;
407 if let (Some(ec1), Some(ec2)) = (edge_color1, edge_color2) {
408 if ec1[eid1] != ec2[eid2] {
409 end = true;
410 }
411 }
412 }
413 } else {
414 if in_1[nu] != 0 {
415 xin1 += 1;
416 }
417 if out_1[nu] != 0 {
418 xout1 += 1;
419 }
420 }
421 }
422 for &node in &outneis_1[c1] {
424 if end {
425 break;
426 }
427 let nu = node as usize;
428 if core_1[nu] >= 0 {
429 let node2 = core_1[nu];
430 if !contains_sorted(&outneis_2[c2], node2 as u32) {
431 end = true;
432 } else if edge_color1.is_some() {
433 let eid1 = graph1.get_eid(cand1 as u32, node)? as usize;
434 let eid2 = graph2.get_eid(cand2 as u32, node2 as u32)? as usize;
435 if let (Some(ec1), Some(ec2)) = (edge_color1, edge_color2) {
436 if ec1[eid1] != ec2[eid2] {
437 end = true;
438 }
439 }
440 }
441 } else {
442 if in_1[nu] != 0 {
443 xin1 += 1;
444 }
445 if out_1[nu] != 0 {
446 xout1 += 1;
447 }
448 }
449 }
450 for &node in &inneis_2[c2] {
452 if end {
453 break;
454 }
455 let nu = node as usize;
456 if core_2[nu] >= 0 {
457 let node2 = core_2[nu];
458 if !contains_sorted(&inneis_1[c1], node2 as u32) {
459 end = true;
460 } else if edge_color1.is_some() {
461 let eid1 = graph1.get_eid(node2 as u32, cand1 as u32)? as usize;
462 let eid2 = graph2.get_eid(node, cand2 as u32)? as usize;
463 if let (Some(ec1), Some(ec2)) = (edge_color1, edge_color2) {
464 if ec1[eid1] != ec2[eid2] {
465 end = true;
466 }
467 }
468 }
469 } else {
470 if in_2[nu] != 0 {
471 xin2 += 1;
472 }
473 if out_2[nu] != 0 {
474 xout2 += 1;
475 }
476 }
477 }
478 for &node in &outneis_2[c2] {
480 if end {
481 break;
482 }
483 let nu = node as usize;
484 if core_2[nu] >= 0 {
485 let node2 = core_2[nu];
486 if !contains_sorted(&outneis_1[c1], node2 as u32) {
487 end = true;
488 } else if edge_color1.is_some() {
489 let eid1 = graph1.get_eid(cand1 as u32, node2 as u32)? as usize;
490 let eid2 = graph2.get_eid(cand2 as u32, node)? as usize;
491 if let (Some(ec1), Some(ec2)) = (edge_color1, edge_color2) {
492 if ec1[eid1] != ec2[eid2] {
493 end = true;
494 }
495 }
496 }
497 } else {
498 if in_2[nu] != 0 {
499 xin2 += 1;
500 }
501 if out_2[nu] != 0 {
502 xout2 += 1;
503 }
504 }
505 }
506
507 if !end && xin1 == xin2 && xout1 == xout2 {
508 depth += 1;
510 path.push(cand1);
511 path.push(cand2);
512 matched_nodes += 1;
513 core_1[c1] = cand2;
514 core_2[c2] = cand1;
515
516 if in_1[c1] != 0 {
517 in_1_size -= 1;
518 }
519 if out_1[c1] != 0 {
520 out_1_size -= 1;
521 }
522 if in_2[c2] != 0 {
523 in_2_size -= 1;
524 }
525 if out_2[c2] != 0 {
526 out_2_size -= 1;
527 }
528
529 for &node in &inneis_1[c1] {
530 let nu = node as usize;
531 if in_1[nu] == 0 && core_1[nu] < 0 {
532 in_1[nu] = depth;
533 in_1_size += 1;
534 }
535 }
536 for &node in &outneis_1[c1] {
537 let nu = node as usize;
538 if out_1[nu] == 0 && core_1[nu] < 0 {
539 out_1[nu] = depth;
540 out_1_size += 1;
541 }
542 }
543 for &node in &inneis_2[c2] {
544 let nu = node as usize;
545 if in_2[nu] == 0 && core_2[nu] < 0 {
546 in_2[nu] = depth;
547 in_2_size += 1;
548 }
549 }
550 for &node in &outneis_2[c2] {
551 let nu = node as usize;
552 if out_2[nu] == 0 && core_2[nu] < 0 {
553 out_2[nu] = depth;
554 out_2_size += 1;
555 }
556 }
557
558 last1 = -1;
559 last2 = -1;
560 } else {
561 last1 = cand1;
562 last2 = cand2;
563 }
564 }
565
566 if matched_nodes == no_of_nodes {
567 if let Flow::Stop = isohandler(&core_1, &core_2) {
568 break;
569 }
570 }
571 }
572
573 Ok(())
574}
575
576#[allow(clippy::too_many_arguments)]
612pub fn isomorphic_vf2(
613 graph1: &Graph,
614 graph2: &Graph,
615 vertex_color1: Option<&[u32]>,
616 vertex_color2: Option<&[u32]>,
617 edge_color1: Option<&[u32]>,
618 edge_color2: Option<&[u32]>,
619) -> IgraphResult<Vf2Isomorphism> {
620 let mut map12: Vec<u32> = Vec::new();
621 let mut map21: Vec<u32> = Vec::new();
622 let mut iso = false;
623
624 vf2_engine(
625 graph1,
626 graph2,
627 vertex_color1,
628 vertex_color2,
629 edge_color1,
630 edge_color2,
631 |core_1, core_2| {
632 iso = true;
633 map12 = core_1.iter().map(|&x| x as u32).collect();
634 map21 = core_2.iter().map(|&x| x as u32).collect();
635 Flow::Stop
636 },
637 )?;
638
639 if !iso {
640 map12.clear();
641 map21.clear();
642 }
643 Ok(Vf2Isomorphism { iso, map12, map21 })
644}
645
646#[allow(clippy::too_many_arguments)]
670pub fn count_isomorphisms_vf2(
671 graph1: &Graph,
672 graph2: &Graph,
673 vertex_color1: Option<&[u32]>,
674 vertex_color2: Option<&[u32]>,
675 edge_color1: Option<&[u32]>,
676 edge_color2: Option<&[u32]>,
677) -> IgraphResult<u64> {
678 let mut count = 0u64;
679 vf2_engine(
680 graph1,
681 graph2,
682 vertex_color1,
683 vertex_color2,
684 edge_color1,
685 edge_color2,
686 |_core_1, _core_2| {
687 count += 1;
688 Flow::Continue
689 },
690 )?;
691 Ok(count)
692}
693
694#[allow(clippy::too_many_arguments)]
717pub fn get_isomorphisms_vf2(
718 graph1: &Graph,
719 graph2: &Graph,
720 vertex_color1: Option<&[u32]>,
721 vertex_color2: Option<&[u32]>,
722 edge_color1: Option<&[u32]>,
723 edge_color2: Option<&[u32]>,
724) -> IgraphResult<Vec<Vec<u32>>> {
725 let mut maps: Vec<Vec<u32>> = Vec::new();
726 vf2_engine(
727 graph1,
728 graph2,
729 vertex_color1,
730 vertex_color2,
731 edge_color1,
732 edge_color2,
733 |_core_1, core_2| {
734 maps.push(core_2.iter().map(|&x| x as u32).collect());
735 Flow::Continue
736 },
737 )?;
738 Ok(maps)
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 fn ring(n: u32, directed: bool) -> Graph {
747 let mut g = Graph::new(n, directed).expect("graph");
748 for i in 0..n {
749 g.add_edge(i, (i + 1) % n).expect("edge");
750 }
751 g
752 }
753
754 fn graph_from(n: u32, directed: bool, edges: &[(u32, u32)]) -> Graph {
756 let mut g = Graph::new(n, directed).expect("graph");
757 for &(u, v) in edges {
758 g.add_edge(u, v).expect("edge");
759 }
760 g
761 }
762
763 #[test]
764 fn empty_graphs_are_isomorphic() {
765 let a = Graph::new(0, false).expect("graph");
766 let b = Graph::new(0, false).expect("graph");
767 let r = isomorphic_vf2(&a, &b, None, None, None, None).expect("ok");
768 assert!(r.iso);
769 assert!(r.map12.is_empty());
770 let c = count_isomorphisms_vf2(&a, &b, None, None, None, None).expect("ok");
772 assert_eq!(c, 1);
773 }
774
775 #[test]
776 fn single_vertex_one_automorphism() {
777 let g = Graph::new(1, false).expect("graph");
778 let c = count_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
779 assert_eq!(c, 1);
780 let r = isomorphic_vf2(&g, &g, None, None, None, None).expect("ok");
781 assert!(r.iso);
782 assert_eq!(r.map12, vec![0]);
783 assert_eq!(r.map21, vec![0]);
784 }
785
786 #[test]
787 fn different_vertex_counts_not_isomorphic() {
788 let a = ring(4, false);
789 let b = ring(5, false);
790 let r = isomorphic_vf2(&a, &b, None, None, None, None).expect("ok");
791 assert!(!r.iso);
792 assert!(r.map12.is_empty());
793 let c = count_isomorphisms_vf2(&a, &b, None, None, None, None).expect("ok");
794 assert_eq!(c, 0);
795 }
796
797 #[test]
798 fn same_vertices_different_edge_counts_not_isomorphic() {
799 let path = graph_from(4, false, &[(0, 1), (1, 2), (2, 3)]);
800 let star = graph_from(4, false, &[(0, 1), (0, 2), (0, 3)]);
801 let r = isomorphic_vf2(&path, &star, None, None, None, None).expect("ok");
803 assert!(!r.iso);
804 }
805
806 #[test]
807 fn triangle_relabelled_is_isomorphic_with_consistent_maps() {
808 let a = graph_from(3, false, &[(0, 1), (1, 2), (2, 0)]);
809 let b = graph_from(3, false, &[(0, 2), (2, 1), (1, 0)]);
810 let r = isomorphic_vf2(&a, &b, None, None, None, None).expect("ok");
811 assert!(r.iso);
812 for i in 0..3usize {
814 assert_eq!(r.map21[r.map12[i] as usize] as usize, i);
815 assert_eq!(r.map12[r.map21[i] as usize] as usize, i);
816 }
817 }
818
819 #[test]
820 fn undirected_ring_automorphisms() {
821 assert_eq!(
823 count_isomorphisms_vf2(&ring(4, false), &ring(4, false), None, None, None, None)
824 .expect("ok"),
825 8
826 );
827 assert_eq!(
828 count_isomorphisms_vf2(&ring(6, false), &ring(6, false), None, None, None, None)
829 .expect("ok"),
830 12
831 );
832 assert_eq!(
834 count_isomorphisms_vf2(&ring(100, false), &ring(100, false), None, None, None, None)
835 .expect("ok"),
836 200
837 );
838 }
839
840 #[test]
841 fn directed_ring_has_only_rotations() {
842 assert_eq!(
844 count_isomorphisms_vf2(&ring(4, true), &ring(4, true), None, None, None, None)
845 .expect("ok"),
846 4
847 );
848 assert_eq!(
849 count_isomorphisms_vf2(&ring(100, true), &ring(100, true), None, None, None, None)
850 .expect("ok"),
851 100
852 );
853 }
854
855 #[test]
856 fn single_edge_has_two_automorphisms() {
857 let g = graph_from(2, false, &[(0, 1)]);
858 let maps = get_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
859 assert_eq!(maps.len(), 2);
860 assert!(maps.contains(&vec![0, 1]));
862 assert!(maps.contains(&vec![1, 0]));
863 }
864
865 #[test]
866 fn distinct_vertex_colors_pin_the_mapping() {
867 let g = ring(6, false);
868 let colors: Vec<u32> = (0..6).collect();
869 let c =
871 count_isomorphisms_vf2(&g, &g, Some(&colors), Some(&colors), None, None).expect("ok");
872 assert_eq!(c, 1);
873 }
874
875 #[test]
876 fn mismatched_color_distribution_blocks_isomorphism() {
877 let g = ring(4, false);
878 let c1 = [0u32, 0, 0, 0];
879 let c2 = [0u32, 0, 0, 1];
880 let r = isomorphic_vf2(&g, &g, Some(&c1), Some(&c2), None, None).expect("ok");
881 assert!(!r.iso);
882 }
883
884 #[test]
885 fn vertex_colors_can_reduce_symmetry() {
886 let g = ring(4, false);
891 let colors = [0u32, 1, 0, 1];
892 let c =
893 count_isomorphisms_vf2(&g, &g, Some(&colors), Some(&colors), None, None).expect("ok");
894 assert_eq!(c, 4);
895 }
896
897 #[test]
898 fn only_one_side_colored_ignores_colors() {
899 let g = ring(4, false);
900 let colors = [0u32, 1, 2, 3];
901 let c = count_isomorphisms_vf2(&g, &g, Some(&colors), None, None, None).expect("ok");
903 assert_eq!(c, 8);
904 }
905
906 #[test]
907 fn edge_colors_constrain_matching() {
908 let g = graph_from(3, false, &[(0, 1), (1, 2), (2, 0)]);
911 let mut ec = vec![0u32; g.ecount()];
914 for (e, slot) in ec.iter_mut().enumerate() {
916 let eid = u32::try_from(e).expect("fits");
917 let (a, b) = g.edge(eid).expect("edge");
918 if (a, b) == (1, 2) || (a, b) == (2, 1) {
919 *slot = 1;
920 }
921 }
922 let c = count_isomorphisms_vf2(&g, &g, None, None, Some(&ec), Some(&ec)).expect("ok");
923 assert_eq!(c, 2);
924 }
925
926 #[test]
927 fn self_loops_are_rejected() {
928 let g = graph_from(2, false, &[(0, 0), (0, 1)]);
929 let h = graph_from(2, false, &[(0, 1)]);
930 assert!(isomorphic_vf2(&g, &h, None, None, None, None).is_err());
931 assert!(isomorphic_vf2(&h, &g, None, None, None, None).is_err());
932 }
933
934 #[test]
935 fn directedness_mismatch_is_rejected() {
936 let u = ring(3, false);
937 let d = ring(3, true);
938 assert!(isomorphic_vf2(&u, &d, None, None, None, None).is_err());
939 }
940
941 #[test]
942 fn wrong_color_vector_length_errors() {
943 let g = ring(3, false);
944 let short = [0u32, 0];
945 assert!(isomorphic_vf2(&g, &g, Some(&short), Some(&short), None, None).is_err());
946 }
947
948 #[test]
949 fn get_and_count_agree() {
950 let g = ring(6, false);
951 let maps = get_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
952 let c = count_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
953 assert_eq!(maps.len() as u64, c);
954 }
955
956 #[test]
957 fn returned_mapping_preserves_adjacency() {
958 let src = graph_from(5, false, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]);
960 let dst = graph_from(5, false, &[(0, 2), (2, 4), (4, 1), (1, 3), (3, 0)]);
961 let r = isomorphic_vf2(&src, &dst, None, None, None, None).expect("ok");
962 assert!(r.iso);
963 for e in 0..src.ecount() {
965 let eid = u32::try_from(e).expect("fits");
966 let (u, v) = src.edge(eid).expect("edge");
967 let mu = r.map12[u as usize];
968 let mv = r.map12[v as usize];
969 assert!(
970 dst.find_eid(mu, mv).expect("lookup").is_some(),
971 "edge {u}-{v} not preserved"
972 );
973 }
974 }
975}
976
977#[cfg(all(test, feature = "proptest-harness"))]
978mod proptests {
979 use super::*;
980 use proptest::prelude::*;
981 use std::collections::HashSet;
982
983 fn arb_simple_graph(max_v: u32) -> impl Strategy<Value = (u32, Vec<(u32, u32)>, bool)> {
986 (1..=max_v, any::<bool>()).prop_flat_map(|(n, directed)| {
987 proptest::collection::vec((0..n, 0..n), 0..=12).prop_map(move |raw| {
988 let mut seen: HashSet<(u32, u32)> = HashSet::new();
989 let mut edges = Vec::new();
990 for (u, v) in raw {
991 if u == v {
992 continue; }
994 let key = if directed {
995 (u, v)
996 } else {
997 (u.min(v), u.max(v))
998 };
999 if seen.insert(key) {
1000 edges.push((u, v));
1001 }
1002 }
1003 (n, edges, directed)
1004 })
1005 })
1006 }
1007
1008 fn relabel(edges: &[(u32, u32)], perm: &[u32]) -> Vec<(u32, u32)> {
1010 edges
1011 .iter()
1012 .map(|&(u, v)| (perm[u as usize], perm[v as usize]))
1013 .collect()
1014 }
1015
1016 fn build(n: u32, directed: bool, edges: &[(u32, u32)]) -> Graph {
1017 let mut g = Graph::new(n, directed).expect("graph");
1018 for &(u, v) in edges {
1019 g.add_edge(u, v).expect("edge");
1020 }
1021 g
1022 }
1023
1024 proptest! {
1025 #[test]
1028 fn self_isomorphic((n, edges, directed) in arb_simple_graph(7)) {
1029 let g = build(n, directed, &edges);
1030 let r = isomorphic_vf2(&g, &g, None, None, None, None).expect("ok");
1031 prop_assert!(r.iso);
1032 prop_assert_eq!(r.map12.len(), n as usize);
1033 for i in 0..n as usize {
1034 prop_assert_eq!(r.map21[r.map12[i] as usize] as usize, i);
1035 }
1036 }
1037
1038 #[test]
1041 fn isomorphic_under_relabelling(
1042 (n, edges, directed) in arb_simple_graph(6),
1043 seed in any::<u64>(),
1044 ) {
1045 let mut perm: Vec<u32> = (0..n).collect();
1047 let mut state = seed;
1048 for i in (1..n as usize).rev() {
1049 state = state
1050 .wrapping_mul(6_364_136_223_846_793_005)
1051 .wrapping_add(1_442_695_040_888_963_407);
1052 let j = (state >> 33) as usize % (i + 1);
1053 perm.swap(i, j);
1054 }
1055 let g = build(n, directed, &edges);
1056 let h = build(n, directed, &relabel(&edges, &perm));
1057
1058 let r = isomorphic_vf2(&g, &h, None, None, None, None).expect("ok");
1059 prop_assert!(r.iso, "relabelled graph must be isomorphic");
1060
1061 for e in 0..g.ecount() {
1063 let eid = u32::try_from(e).expect("fits");
1064 let (u, v) = g.edge(eid).expect("edge");
1065 let mu = r.map12[u as usize];
1066 let mv = r.map12[v as usize];
1067 prop_assert!(h.find_eid(mu, mv).expect("lookup").is_some());
1068 }
1069
1070 let cg = count_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
1072 let ch = count_isomorphisms_vf2(&h, &h, None, None, None, None).expect("ok");
1073 prop_assert_eq!(cg, ch);
1074 }
1075
1076 #[test]
1079 fn get_count_consistency((n, edges, directed) in arb_simple_graph(6)) {
1080 let g = build(n, directed, &edges);
1081 let maps = get_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
1082 let c = count_isomorphisms_vf2(&g, &g, None, None, None, None).expect("ok");
1083 prop_assert_eq!(maps.len() as u64, c);
1084 prop_assert!(c >= 1, "identity automorphism always exists");
1085 for m in &maps {
1087 prop_assert_eq!(m.len(), n as usize);
1088 let mut sorted = m.clone();
1089 sorted.sort_unstable();
1090 let expected: Vec<u32> = (0..n).collect();
1091 prop_assert_eq!(sorted, expected);
1092 }
1093 }
1094
1095 #[test]
1098 fn distinct_colors_force_unique_self_map(
1099 (n, edges, directed) in arb_simple_graph(6),
1100 ) {
1101 let g = build(n, directed, &edges);
1102 let colors: Vec<u32> = (0..n).collect();
1103 let c = count_isomorphisms_vf2(&g, &g, Some(&colors), Some(&colors), None, None)
1104 .expect("ok");
1105 prop_assert_eq!(c, 1);
1106 }
1107 }
1108}