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}