Skip to main content

rust_igraph/algorithms/isomorphism/
queries.rs

1//! Generic isomorphism dispatchers (`ALGO-ISO-007`).
2//!
3//! [`isomorphic`] and [`subisomorphic`] are the "first set" of igraph's
4//! isomorphism API: they pick a backend automatically rather than exposing
5//! the engine knobs. Only the yes/no verdict is observable, so the choice of
6//! backend is an implementation detail.
7
8use super::canonical::isomorphic_bliss::isomorphic_bliss;
9use super::simplify_and_colorize::simplify_and_colorize;
10use super::subiso::subisomorphic_vf2;
11use super::vf2::isomorphic_vf2;
12use crate::algorithms::properties::multiplicity::has_multiple;
13use crate::core::{Graph, IgraphError, IgraphResult};
14
15/// Decide whether two graphs are isomorphic, choosing a backend automatically.
16///
17/// Mirrors `igraph_isomorphic`: the dispatch picks whichever engine fits the
18/// input. Only the Boolean verdict is returned — call [`isomorphic_vf2`] or
19/// [`isomorphic_bliss`] directly when you also need the vertex mapping.
20///
21/// The dispatch is:
22/// 1. directed vs. undirected → [`IgraphError::InvalidArgument`];
23/// 2. if either graph has multi-edges, both are reduced with
24///    [`simplify_and_colorize`] and compared by colour-aware VF2 (self-loops
25///    become vertex colours, parallel-edge multiplicities become edge
26///    colours);
27/// 3. otherwise, differing vertex *or* edge counts → `false`;
28/// 4. otherwise the canonical-form (BLISS) test decides.
29///
30/// Upstream takes an O(1) precomputed-`isoclass` shortcut for tiny graphs
31/// (directed 3–4, undirected 3–6 vertices) before falling back to BLISS; the
32/// canonical-form test returns the identical verdict, so this port uses it
33/// uniformly. Self-loops are supported (folded into the BLISS vertex
34/// invariant).
35///
36/// # Errors
37///
38/// Returns [`IgraphError::InvalidArgument`] if the two graphs differ in
39/// directedness. Propagates backend errors otherwise.
40///
41/// # Examples
42///
43/// ```
44/// use rust_igraph::{Graph, isomorphic};
45///
46/// // A 4-cycle and its relabelling are isomorphic.
47/// let mut a = Graph::new(4, false).unwrap();
48/// for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 0)] {
49///     a.add_edge(u, v).unwrap();
50/// }
51/// let mut b = Graph::new(4, false).unwrap();
52/// for (u, v) in [(0, 2), (2, 1), (1, 3), (3, 0)] {
53///     b.add_edge(u, v).unwrap();
54/// }
55/// assert!(isomorphic(&a, &b).unwrap());
56///
57/// // A 4-cycle is not isomorphic to a path on 4 vertices.
58/// let mut p = Graph::new(4, false).unwrap();
59/// for (u, v) in [(0, 1), (1, 2), (2, 3)] {
60///     p.add_edge(u, v).unwrap();
61/// }
62/// assert!(!isomorphic(&a, &p).unwrap());
63/// ```
64pub fn isomorphic(graph1: &Graph, graph2: &Graph) -> IgraphResult<bool> {
65    if graph1.is_directed() != graph2.is_directed() {
66        return Err(IgraphError::InvalidArgument(
67            "cannot compare directed and undirected graphs for isomorphism".to_owned(),
68        ));
69    }
70
71    // Multi-edge path: reduce both graphs to a vertex/edge coloured simple
72    // graph and let colour-aware VF2 decide (BLISS rejects multi-edges).
73    if has_multiple(graph1)? || has_multiple(graph2)? {
74        let r1 = simplify_and_colorize(graph1)?;
75        let r2 = simplify_and_colorize(graph2)?;
76        let res = isomorphic_vf2(
77            &r1.graph,
78            &r2.graph,
79            Some(&r1.vertex_color),
80            Some(&r2.vertex_color),
81            Some(&r1.edge_color),
82            Some(&r2.edge_color),
83        )?;
84        return Ok(res.iso);
85    }
86
87    if graph1.vcount() != graph2.vcount() || graph1.ecount() != graph2.ecount() {
88        return Ok(false);
89    }
90
91    Ok(isomorphic_bliss(graph1, graph2, None, None)?.iso)
92}
93
94/// Decide whether `graph2` is isomorphic to a subgraph of `graph1`.
95///
96/// Mirrors `igraph_subisomorphic`, which simply delegates to the VF2 subgraph
97/// engine for all inputs. `graph1` is the (larger) target and `graph2` the
98/// (smaller) pattern; both must share directedness. Like upstream, non-simple
99/// graphs are not supported by the VF2 backend.
100///
101/// Only the Boolean verdict is returned — call [`subisomorphic_vf2`] directly
102/// when you also need a concrete embedding.
103///
104/// # Errors
105///
106/// Propagates the VF2 backend's errors (e.g. directedness mismatch, or a
107/// graph the engine rejects).
108///
109/// # Examples
110///
111/// ```
112/// use rust_igraph::{Graph, subisomorphic};
113///
114/// // A single edge embeds into a triangle.
115/// let mut triangle = Graph::new(3, false).unwrap();
116/// for (u, v) in [(0, 1), (1, 2), (2, 0)] {
117///     triangle.add_edge(u, v).unwrap();
118/// }
119/// let mut edge = Graph::new(2, false).unwrap();
120/// edge.add_edge(0, 1).unwrap();
121/// assert!(subisomorphic(&triangle, &edge).unwrap());
122/// ```
123pub fn subisomorphic(graph1: &Graph, graph2: &Graph) -> IgraphResult<bool> {
124    Ok(subisomorphic_vf2(graph1, graph2, None, None, None, None)?.iso)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn ring(n: u32, directed: bool) -> Graph {
132        let mut g = Graph::new(n, directed).expect("graph");
133        for i in 0..n {
134            g.add_edge(i, (i + 1) % n).expect("edge");
135        }
136        g
137    }
138
139    fn path(n: u32) -> Graph {
140        let mut g = Graph::new(n, false).expect("graph");
141        for i in 0..n.saturating_sub(1) {
142            g.add_edge(i, i + 1).expect("edge");
143        }
144        g
145    }
146
147    fn complete(n: u32) -> Graph {
148        let mut g = Graph::new(n, false).expect("graph");
149        for a in 0..n {
150            for b in (a + 1)..n {
151                g.add_edge(a, b).expect("edge");
152            }
153        }
154        g
155    }
156
157    #[test]
158    fn relabelled_cycle_is_isomorphic() {
159        let a = ring(6, false);
160        let mut b = Graph::new(6, false).expect("graph");
161        for (u, v) in [(0, 2), (2, 4), (4, 1), (1, 3), (3, 5), (5, 0)] {
162            b.add_edge(u, v).expect("edge");
163        }
164        assert!(isomorphic(&a, &b).expect("iso"));
165    }
166
167    #[test]
168    fn cycle_is_not_a_path() {
169        assert!(!isomorphic(&ring(6, false), &path(6)).expect("iso"));
170    }
171
172    #[test]
173    fn different_edge_count_short_circuits_to_false() {
174        // Same vertex count, different edge count: dispatch returns false
175        // without invoking a backend.
176        let c4 = ring(4, false);
177        let mut three_edges = Graph::new(4, false).expect("graph");
178        for (u, v) in [(0, 1), (1, 2), (2, 3)] {
179            three_edges.add_edge(u, v).expect("edge");
180        }
181        assert!(!isomorphic(&c4, &three_edges).expect("iso"));
182    }
183
184    #[test]
185    fn directedness_mismatch_is_an_error() {
186        assert!(isomorphic(&ring(3, false), &ring(3, true)).is_err());
187    }
188
189    #[test]
190    fn directed_cycles_respect_orientation() {
191        assert!(isomorphic(&ring(5, true), &ring(5, true)).expect("iso"));
192    }
193
194    #[test]
195    fn self_loops_are_supported() {
196        // Two single-vertex graphs each with one self-loop are isomorphic;
197        // BLISS folds the loop into the vertex invariant.
198        let mut a = Graph::new(2, false).expect("graph");
199        a.add_edge(0, 0).expect("loop");
200        a.add_edge(0, 1).expect("edge");
201        let mut b = Graph::new(2, false).expect("graph");
202        b.add_edge(1, 1).expect("loop");
203        b.add_edge(0, 1).expect("edge");
204        assert!(isomorphic(&a, &b).expect("iso"));
205    }
206
207    #[test]
208    fn multigraph_path_uses_colorized_vf2() {
209        // Two parallel edges between 0-1 plus a pendant; relabelled copy must
210        // still be detected as isomorphic via the simplify_and_colorize path.
211        let mut a = Graph::new(3, false).expect("graph");
212        a.add_edge(0, 1).expect("edge");
213        a.add_edge(0, 1).expect("edge");
214        a.add_edge(1, 2).expect("edge");
215        let mut b = Graph::new(3, false).expect("graph");
216        b.add_edge(2, 1).expect("edge");
217        b.add_edge(2, 1).expect("edge");
218        b.add_edge(1, 0).expect("edge");
219        assert!(isomorphic(&a, &b).expect("iso"));
220    }
221
222    #[test]
223    fn multigraph_distinguishes_multiplicity() {
224        // Both have 3 edges on 3 vertices, but a doubles edge 0-1 while b is a
225        // simple path: edge-colour (multiplicity) breaks the match.
226        let mut a = Graph::new(3, false).expect("graph");
227        a.add_edge(0, 1).expect("edge");
228        a.add_edge(0, 1).expect("edge");
229        a.add_edge(1, 2).expect("edge");
230        let mut b = Graph::new(3, false).expect("graph");
231        b.add_edge(0, 1).expect("edge");
232        b.add_edge(1, 2).expect("edge");
233        b.add_edge(2, 0).expect("edge");
234        assert!(!isomorphic(&a, &b).expect("iso"));
235    }
236
237    #[test]
238    fn edge_embeds_into_triangle() {
239        let mut edge = Graph::new(2, false).expect("graph");
240        edge.add_edge(0, 1).expect("edge");
241        assert!(subisomorphic(&complete(3), &edge).expect("subiso"));
242    }
243
244    #[test]
245    fn triangle_does_not_embed_into_path() {
246        assert!(!subisomorphic(&path(3), &ring(3, false)).expect("subiso"));
247    }
248
249    #[test]
250    fn whole_graph_embeds_into_itself() {
251        let g = complete(4);
252        assert!(subisomorphic(&g, &g).expect("subiso"));
253    }
254}