Skip to main content

rust_igraph/algorithms/isomorphism/
vf2.rs

1//! VF2 graph isomorphism (`ALGO-ISO-001`).
2//!
3//! Port of the VF2 *graph-isomorphism* engine and its three public
4//! wrappers from `references/igraph/src/isomorphism/vf2.c`:
5//!
6//! - the generic backtracking engine `igraph_get_isomorphisms_vf2_callback`
7//!   (lines 137-697),
8//! - [`isomorphic_vf2`] — stop at the first mapping (`igraph_isomorphic_vf2`,
9//!   line 790),
10//! - [`count_isomorphisms_vf2`] — count all mappings
11//!   (`igraph_count_isomorphisms_vf2`, line 872),
12//! - [`get_isomorphisms_vf2`] — collect every mapping
13//!   (`igraph_get_isomorphisms_vf2`, line 947).
14//!
15//! VF2 (Cordella, Foggia, Sansone, Vento) explores a search tree of
16//! candidate vertex pairs `(cand1, cand2)`, maintaining a partial mapping
17//! plus the "terminal sets" `in_*`/`out_*` of not-yet-mapped vertices that
18//! are adjacent to the current partial mapping. At each step it picks the
19//! next candidate pair, runs the feasibility/pruning rules (degree match,
20//! optional vertex/edge colours, edge-consistency in both directions for
21//! directed graphs, and the look-ahead counts `xin`/`xout`), then either
22//! extends the mapping or backtracks. When the mapping is complete it
23//! reports the isomorphism through a callback.
24//!
25//! Like upstream, this engine **does not support self-loops** (rejected up
26//! front) and treats colours via optional per-vertex / per-edge label
27//! slices: two vertices (edges) can match only if their labels are equal.
28//! The optional `node_compat_fn` / `edge_compat_fn` callbacks of the C API
29//! are intentionally not exposed here — colours cover every documented
30//! conformance case and the callback hooks can be added by a later AWU if a
31//! real use case appears.
32
33// The VF2 engine is a faithful port of a pointer/index-heavy C algorithm.
34// It juggles three integer views of the same vertex ids: `usize` for slice
35// indexing, `u32` for the [`Graph`] API, and `i64` for the `-1`-sentinel
36// `core`/`last` slots. Every value is bounded by the vertex/edge count,
37// which fits `u32` by the [`Graph`] contract, so the conversions cannot
38// truncate, wrap, or lose sign in practice.
39#![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/// Result of an exact VF2 graph-isomorphism test ([`isomorphic_vf2`]).
49#[derive(Debug, Clone)]
50pub struct Vf2Isomorphism {
51    /// Whether the two graphs are isomorphic.
52    pub iso: bool,
53    /// The permutation taking `graph1` to `graph2`: `map12[i]` is the image
54    /// of vertex `i` of `graph1`. Empty when the graphs are not isomorphic.
55    pub map12: Vec<u32>,
56    /// The inverse permutation taking `graph2` to `graph1`. Empty when the
57    /// graphs are not isomorphic.
58    pub map21: Vec<u32>,
59}
60
61/// Build deduplicated, ascending neighbour lists with self-loops removed,
62/// reproducing igraph's `lazy_adjlist(mode, NO_LOOPS, NO_MULTIPLE)`.
63///
64/// `incoming = false` yields out-neighbours, `incoming = true` yields
65/// in-neighbours. On undirected graphs both directions return the full
66/// neighbour set (every edge is bidirectional), matching upstream.
67pub(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            // Undirected: all neighbours regardless of requested direction.
73            graph.neighbors(v)?
74        } else if incoming {
75            // Directed in-neighbours: sources of incoming edges.
76            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            // Directed out-neighbours (already ascending from `oi`).
83            graph.neighbors(v)?
84        };
85        neis.retain(|&u| u != v); // NO_LOOPS
86        neis.sort_unstable();
87        neis.dedup(); // NO_MULTIPLE
88        adj.push(neis);
89    }
90    Ok(adj)
91}
92
93/// Out-degree (counting edge multiplicity, loops would count but are
94/// rejected up front), matching `igraph_degree(_, OUT, LOOPS)`.
95pub(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
103/// In-degree counterpart of [`out_degree`].
104pub(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
112/// Whether a sorted slice contains `x` (binary search).
113pub(crate) fn contains_sorted(slice: &[u32], x: u32) -> bool {
114    slice.binary_search(&x).is_ok()
115}
116
117/// Multiset equality of two colour vectors, used for the cheap pre-filter
118/// "the colour distributions must match or no isomorphism can exist".
119fn 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
127/// Action returned by the per-isomorphism callback of [`vf2_engine`].
128pub(crate) enum Flow {
129    /// Keep searching for further isomorphisms.
130    Continue,
131    /// Stop the search immediately (a result has been captured).
132    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/// The generic VF2 backtracking engine for graph isomorphism.
163///
164/// Faithful translation of `igraph_get_isomorphisms_vf2_callback`. For every
165/// complete mapping found, `isohandler` is invoked with `(core_1, core_2)`
166/// where `core_1[i]` is the image in `graph2` of vertex `i` of `graph1` and
167/// `core_2` is its inverse; returning [`Flow::Stop`] ends the search.
168#[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    // Only one graph coloured -> ignore colours, matching upstream.
185    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    // Different size -> cannot be isomorphic.
195    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    // core_1[i] = image of vertex i of graph1 in graph2, or -1.
240    let mut core_1 = vec![-1i64; n];
241    let mut core_2 = vec![-1i64; n];
242    // in_*/out_*[v] = depth at which v entered the terminal set, or 0.
243    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    // path holds (cand1, cand2) pairs pushed on each successful step.
253    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        // Search for the next pair to try.
264        if in_1_size != in_2_size || out_1_size != out_2_size {
265            // step back, nothing to do
266        } 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            // Dead end: step back if possible, otherwise terminate.
327            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            // Step forward if the (cand1, cand2) pair is feasible.
377            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            // cand1's in-neighbours.
395            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            // cand1's out-neighbours.
423            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            // cand2's in-neighbours.
451            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            // cand2's out-neighbours.
479            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                // Add (cand1, cand2) to the mapping.
509                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/// Test whether two graphs are isomorphic using the VF2 algorithm.
577///
578/// Optional `vertex_color*` / `edge_color*` slices restrict the matching:
579/// two vertices (edges) may correspond only if their colours are equal. Pass
580/// `None` for uncoloured graphs; supplying a colour for only one side makes
581/// that colour be ignored (matching upstream).
582///
583/// On success [`Vf2Isomorphism::iso`] tells whether a mapping exists; when it
584/// does, `map12` / `map21` hold the permutation and its inverse, otherwise
585/// they are empty.
586///
587/// # Errors
588///
589/// Returns [`IgraphError::InvalidArgument`] if the two graphs differ in
590/// directedness, if either contains a self-loop (VF2 does not support loops),
591/// or if a supplied colour vector has the wrong length.
592///
593/// # Examples
594///
595/// ```
596/// use rust_igraph::{Graph, isomorphic_vf2};
597///
598/// // Two triangles with relabelled vertices are isomorphic.
599/// let mut a = Graph::new(3, false).unwrap();
600/// a.add_edge(0, 1).unwrap();
601/// a.add_edge(1, 2).unwrap();
602/// a.add_edge(2, 0).unwrap();
603/// let mut b = Graph::new(3, false).unwrap();
604/// b.add_edge(0, 2).unwrap();
605/// b.add_edge(2, 1).unwrap();
606/// b.add_edge(1, 0).unwrap();
607/// let r = isomorphic_vf2(&a, &b, None, None, None, None).unwrap();
608/// assert!(r.iso);
609/// assert_eq!(r.map12.len(), 3);
610/// ```
611#[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/// Count the number of VF2 isomorphic mappings between two graphs.
647///
648/// Calling this with the same graph as both arguments counts the graph's
649/// automorphisms. Colour arguments behave as in [`isomorphic_vf2`].
650///
651/// # Errors
652///
653/// Same conditions as [`isomorphic_vf2`].
654///
655/// # Examples
656///
657/// ```
658/// use rust_igraph::{Graph, count_isomorphisms_vf2};
659///
660/// // A 4-cycle has 8 automorphisms (4 rotations x 2 reflections).
661/// let mut g = Graph::new(4, false).unwrap();
662/// g.add_edge(0, 1).unwrap();
663/// g.add_edge(1, 2).unwrap();
664/// g.add_edge(2, 3).unwrap();
665/// g.add_edge(3, 0).unwrap();
666/// let count = count_isomorphisms_vf2(&g, &g, None, None, None, None).unwrap();
667/// assert_eq!(count, 8);
668/// ```
669#[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/// Collect every VF2 isomorphic mapping between two graphs.
695///
696/// Each returned vector is a `map21` mapping: position `j` holds the vertex
697/// of `graph1` that vertex `j` of `graph2` maps to. The list is empty when
698/// the graphs are not isomorphic. Colour arguments behave as in
699/// [`isomorphic_vf2`].
700///
701/// # Errors
702///
703/// Same conditions as [`isomorphic_vf2`].
704///
705/// # Examples
706///
707/// ```
708/// use rust_igraph::{Graph, get_isomorphisms_vf2};
709///
710/// // A single edge has two automorphisms (identity and the swap).
711/// let mut g = Graph::new(2, false).unwrap();
712/// g.add_edge(0, 1).unwrap();
713/// let maps = get_isomorphisms_vf2(&g, &g, None, None, None, None).unwrap();
714/// assert_eq!(maps.len(), 2);
715/// ```
716#[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    /// Undirected ring on `n` vertices: 0-1-...-(n-1)-0.
746    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    /// Build a graph from an explicit edge list.
755    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        // Exactly one (empty) mapping.
771        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        // Same vcount (4) and ecount (3), but degree sequences differ.
802        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        // map12 and map21 are mutual inverses.
813        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        // ring(n) undirected has 2n automorphisms (n rotations x 2 reflections).
822        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        // The headline oracle value from the upstream unit test.
833        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        // A directed cycle has exactly n automorphisms (rotations; no reflection).
843        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        // Identity and the swap.
861        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        // Every vertex has a unique colour -> only the identity survives.
870        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        // Colour two opposite vertices of a 4-cycle one colour, the other
887        // two another colour. Automorphisms preserving the colouring: the
888        // identity, the half-turn, and the two reflections through the
889        // coloured pairs -> 4 of the 8 uncoloured automorphisms.
890        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        // graph2 has no colours -> colours ignored, full symmetry restored.
902        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        // Triangle with one distinctly coloured edge: automorphisms must fix
909        // that edge, leaving identity + the reflection swapping its endpoints.
910        let g = graph_from(3, false, &[(0, 1), (1, 2), (2, 0)]);
911        // Edge ids follow insertion/canonical order; colour edge (1,2)
912        // differently from the other two.
913        let mut ec = vec![0u32; g.ecount()];
914        // Find the edge between 1 and 2 and colour it 1.
915        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        // Two different drawings of the same 5-cycle.
959        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        // Every edge (u,v) in src must map to an edge in dst.
964        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    /// Generate a simple, loopless graph (VF2 precondition): `n` vertices and
984    /// a deduplicated, self-loop-free edge set, with a directedness flag.
985    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; // no self-loops
993                    }
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    /// Apply a vertex permutation `perm` (perm[old] = new) to an edge list.
1009    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        /// A graph is always isomorphic to itself, with a self-consistent
1026        /// (mutually inverse) pair of mapping permutations.
1027        #[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        /// Relabelling a graph by a random permutation yields an isomorphic
1039        /// graph, and the automorphism count is preserved under relabelling.
1040        #[test]
1041        fn isomorphic_under_relabelling(
1042            (n, edges, directed) in arb_simple_graph(6),
1043            seed in any::<u64>(),
1044        ) {
1045            // Build a permutation from the seed via a Fisher-Yates shuffle.
1046            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            // Every edge of g maps to an edge of h.
1062            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            // Automorphism count is a graph invariant.
1071            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        /// `get_isomorphisms_vf2` returns exactly as many maps as
1077        /// `count_isomorphisms_vf2` reports, and the self-count is >= 1.
1078        #[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            // Each returned map is a valid permutation.
1086            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        /// Giving every vertex a distinct colour forces a unique (identity)
1096        /// mapping for self-comparison.
1097        #[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}