Skip to main content

rust_igraph/algorithms/isomorphism/canonical/
isomorphic_bliss.rs

1//! `isomorphic_bliss` (`ALGO-ISO-006`).
2
3use super::canonicalize;
4use crate::algorithms::isomorphism::vf2::Vf2Isomorphism;
5use crate::core::{Graph, IgraphError, IgraphResult};
6
7/// Test whether two graphs are isomorphic via canonical labeling (BLISS).
8///
9/// Both graphs are reduced to their canonical form by the shared
10/// individualization-refinement engine (ALGO-ISO-003); they are isomorphic
11/// iff the canonical certificates (and, when colours are supplied, the colour
12/// sequence in canonical order) coincide. When they match, a concrete vertex
13/// mapping is recovered by composing the two canonical labelings.
14///
15/// Optional `vertex_color1` / `vertex_color2` slices restrict the matching:
16/// two vertices may correspond only if their colours are equal. Pass `None`
17/// for uncoloured graphs; supplying a colour for only one side makes that
18/// colour be ignored (matching upstream). Unlike VF2, self-loops are
19/// supported (BLISS folds a loop into the vertex invariant); multi-edges are
20/// rejected.
21///
22/// On success [`Vf2Isomorphism::iso`] tells whether a mapping exists; when it
23/// does, `map12` / `map21` hold the permutation taking `graph1` to `graph2`
24/// and its inverse, otherwise they are empty.
25///
26/// # Errors
27///
28/// Returns [`IgraphError::InvalidArgument`] if the two graphs differ in
29/// directedness or if a supplied colour vector has the wrong length, and
30/// [`IgraphError::Unsupported`] if either graph has multiple edges.
31///
32/// # Examples
33///
34/// ```
35/// use rust_igraph::{Graph, isomorphic_bliss};
36///
37/// // Two triangles with relabelled vertices are isomorphic.
38/// let mut a = Graph::new(3, false).unwrap();
39/// a.add_edge(0, 1).unwrap();
40/// a.add_edge(1, 2).unwrap();
41/// a.add_edge(2, 0).unwrap();
42/// let mut b = Graph::new(3, false).unwrap();
43/// b.add_edge(0, 2).unwrap();
44/// b.add_edge(2, 1).unwrap();
45/// b.add_edge(1, 0).unwrap();
46/// let r = isomorphic_bliss(&a, &b, None, None).unwrap();
47/// assert!(r.iso);
48/// assert_eq!(r.map12.len(), 3);
49/// ```
50pub fn isomorphic_bliss(
51    graph1: &Graph,
52    graph2: &Graph,
53    vertex_color1: Option<&[u32]>,
54    vertex_color2: Option<&[u32]>,
55) -> IgraphResult<Vf2Isomorphism> {
56    if graph1.is_directed() != graph2.is_directed() {
57        return Err(IgraphError::InvalidArgument(
58            "cannot compare directed and undirected graphs".to_owned(),
59        ));
60    }
61
62    // Supplying a colour for only one side ignores both (matches upstream).
63    let (color1, color2) = if vertex_color1.is_some() == vertex_color2.is_some() {
64        (vertex_color1, vertex_color2)
65    } else {
66        (None, None)
67    };
68
69    let n1 = graph1.vcount() as usize;
70    let n2 = graph2.vcount() as usize;
71
72    // Different vertex counts: trivially non-isomorphic. (canonicalize still
73    // validates colour-vector lengths so callers get a consistent error.)
74    let canon1 = canonicalize(graph1, color1)?;
75    let canon2 = canonicalize(graph2, color2)?;
76
77    let not_iso = || Vf2Isomorphism {
78        iso: false,
79        map12: Vec::new(),
80        map21: Vec::new(),
81    };
82
83    if n1 != n2 || canon1.certificate != canon2.certificate {
84        return Ok(not_iso());
85    }
86
87    // Canonical orders (position -> vertex), inverting `labeling` (vertex ->
88    // position).
89    let mut order1 = vec![0u32; n1];
90    for (v, &pos) in canon1.labeling.iter().enumerate() {
91        order1[pos as usize] = v as u32;
92    }
93    let mut order2 = vec![0u32; n2];
94    for (v, &pos) in canon2.labeling.iter().enumerate() {
95        order2[pos as usize] = v as u32;
96    }
97
98    // Colour-aware check: vertices sharing a canonical position must share a
99    // raw colour value, otherwise the map is not colour-preserving.
100    if let (Some(c1), Some(c2)) = (color1, color2) {
101        for pos in 0..n1 {
102            if c1[order1[pos] as usize] != c2[order2[pos] as usize] {
103                return Ok(not_iso());
104            }
105        }
106    }
107
108    // map12[v1] = the graph2 vertex sharing v1's canonical position.
109    let mut map12 = vec![0u32; n1];
110    let mut map21 = vec![0u32; n2];
111    for (v1, &pos) in canon1.labeling.iter().enumerate() {
112        let v2 = order2[pos as usize];
113        map12[v1] = v2;
114        map21[v2 as usize] = v1 as u32;
115    }
116
117    Ok(Vf2Isomorphism {
118        iso: true,
119        map12,
120        map21,
121    })
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn triangle(perm: [u32; 3]) -> Graph {
129        let mut g = Graph::new(3, false).expect("graph");
130        g.add_edge(perm[0], perm[1]).expect("edge");
131        g.add_edge(perm[1], perm[2]).expect("edge");
132        g.add_edge(perm[2], perm[0]).expect("edge");
133        g
134    }
135
136    fn path(n: u32) -> Graph {
137        let mut g = Graph::new(n, false).expect("graph");
138        for i in 0..n.saturating_sub(1) {
139            g.add_edge(i, i + 1).expect("edge");
140        }
141        g
142    }
143
144    fn complete(n: u32) -> Graph {
145        let mut g = Graph::new(n, false).expect("graph");
146        for a in 0..n {
147            for b in (a + 1)..n {
148                g.add_edge(a, b).expect("edge");
149            }
150        }
151        g
152    }
153
154    fn directed_cycle(n: u32) -> Graph {
155        let mut g = Graph::new(n, true).expect("graph");
156        for i in 0..n {
157            g.add_edge(i, (i + 1) % n).expect("edge");
158        }
159        g
160    }
161
162    /// Assert `map12` is a genuine edge-preserving bijection from `a` to `b`.
163    #[allow(clippy::many_single_char_names)]
164    fn assert_valid_map(a: &Graph, b: &Graph, r: &Vf2Isomorphism) {
165        let n = a.vcount() as usize;
166        assert_eq!(r.map12.len(), n);
167        assert_eq!(r.map21.len(), b.vcount() as usize);
168        // Bijection: map12 is a permutation and map21 is its inverse.
169        let mut seen = vec![false; n];
170        for (v, &iv) in r.map12.iter().enumerate() {
171            assert!(!seen[iv as usize], "map12 not injective");
172            seen[iv as usize] = true;
173            assert_eq!(r.map21[iv as usize], v as u32, "map21 not inverse of map12");
174        }
175        // Edge preservation.
176        let b_edges: std::collections::HashSet<(u32, u32)> = (0..b.ecount())
177            .map(|e| b.edge(e as u32).expect("edge"))
178            .map(|(u, v)| {
179                if b.is_directed() || u <= v {
180                    (u, v)
181                } else {
182                    (v, u)
183                }
184            })
185            .collect();
186        for e in 0..a.ecount() {
187            let (u, v) = a.edge(e as u32).expect("edge");
188            let (iu, iv) = (r.map12[u as usize], r.map12[v as usize]);
189            let key = if a.is_directed() || iu <= iv {
190                (iu, iv)
191            } else {
192                (iv, iu)
193            };
194            assert!(b_edges.contains(&key), "edge not preserved by map");
195        }
196    }
197
198    #[test]
199    fn relabelled_triangles_are_isomorphic() {
200        let a = triangle([0, 1, 2]);
201        let b = triangle([2, 0, 1]);
202        let r = isomorphic_bliss(&a, &b, None, None).expect("bliss");
203        assert!(r.iso);
204        assert_valid_map(&a, &b, &r);
205    }
206
207    #[test]
208    fn triangle_is_not_a_path() {
209        let r = isomorphic_bliss(&triangle([0, 1, 2]), &path(3), None, None).expect("bliss");
210        assert!(!r.iso);
211        assert!(r.map12.is_empty());
212        assert!(r.map21.is_empty());
213    }
214
215    #[test]
216    fn different_vertex_counts_are_not_isomorphic() {
217        let r = isomorphic_bliss(&complete(4), &complete(5), None, None).expect("bliss");
218        assert!(!r.iso);
219    }
220
221    #[test]
222    fn complete_graphs_match_under_any_relabelling() {
223        let a = complete(5);
224        let b = complete(5);
225        let r = isomorphic_bliss(&a, &b, None, None).expect("bliss");
226        assert!(r.iso);
227        assert_valid_map(&a, &b, &r);
228    }
229
230    #[test]
231    fn directed_cycles_respect_orientation() {
232        let a = directed_cycle(4);
233        let b = directed_cycle(4);
234        let r = isomorphic_bliss(&a, &b, None, None).expect("bliss");
235        assert!(r.iso);
236        assert_valid_map(&a, &b, &r);
237    }
238
239    #[test]
240    fn directedness_mismatch_is_an_error() {
241        let und = path(3);
242        let dir = directed_cycle(3);
243        assert!(isomorphic_bliss(&und, &dir, None, None).is_err());
244    }
245
246    #[test]
247    fn colours_must_match_for_isomorphism() {
248        // Same structure (P_3), but the centre vertex carries a different
249        // colour, so no colour-preserving map exists.
250        let a = path(3);
251        let b = path(3);
252        // a: ends colour 0, centre colour 1.
253        let ca = [0u32, 1, 0];
254        // b: ends colour 1, centre colour 0 -> colour classes differ.
255        let cb = [1u32, 0, 1];
256        let r = isomorphic_bliss(&a, &b, Some(&ca), Some(&cb)).expect("bliss");
257        assert!(!r.iso);
258    }
259
260    #[test]
261    fn colour_matching_uses_raw_values_not_class_structure() {
262        // Same structure (P_3) and identical colour-class *sizes* {2, 1}, but
263        // disjoint raw colour values: no colour-preserving map exists because
264        // BLISS matches colours by value, not by class shape (verified against
265        // python-igraph 0.11.9).
266        let a = path(3);
267        let b = path(3);
268        let ca = [5u32, 6, 5];
269        let cb = [7u32, 8, 7];
270        let r = isomorphic_bliss(&a, &b, Some(&ca), Some(&cb)).expect("bliss");
271        assert!(!r.iso);
272    }
273
274    #[test]
275    fn matching_colours_yield_isomorphism() {
276        let a = path(3);
277        let b = path(3);
278        let ca = [0u32, 1, 0];
279        let cb = [0u32, 1, 0];
280        let r = isomorphic_bliss(&a, &b, Some(&ca), Some(&cb)).expect("bliss");
281        assert!(r.iso);
282        assert_valid_map(&a, &b, &r);
283    }
284
285    #[test]
286    fn one_sided_colour_is_ignored() {
287        // Supplying a colour for only one side ignores both (upstream parity).
288        let a = triangle([0, 1, 2]);
289        let b = triangle([1, 2, 0]);
290        let ca = [0u32, 1, 2];
291        let r = isomorphic_bliss(&a, &b, Some(&ca), None).expect("bliss");
292        assert!(r.iso);
293    }
294
295    #[test]
296    fn wrong_colour_length_is_an_error() {
297        let a = triangle([0, 1, 2]);
298        let b = triangle([0, 1, 2]);
299        let bad = [0u32, 0];
300        assert!(isomorphic_bliss(&a, &b, Some(&bad), Some(&[0, 0, 0])).is_err());
301    }
302
303    #[test]
304    fn rejects_multigraph() {
305        let mut a = Graph::new(2, false).expect("graph");
306        a.add_edge(0, 1).expect("edge");
307        a.add_edge(0, 1).expect("edge");
308        let b = path(2);
309        assert!(isomorphic_bliss(&a, &b, None, None).is_err());
310    }
311}