canonical_form/
lib.rs

1//! Algorithm to reduce combinatorial structures modulo isomorphism.
2//!
3//! This can typically be used to to test if two graphs are isomorphic.
4//!
5//! The algorithm manipulates its input as a black box by
6//! the action of permutations
7//! and by testing equallity with element of its orbit,
8//! plus some user-defined functions
9//! that help to break symmetries.
10//!
11//!```
12//!use canonical_form::Canonize;
13//!
14//!// Simple Graph implementation as adjacency lists
15//!#[derive(Ord, PartialOrd, PartialEq, Eq, Clone, Debug)]
16//!struct Graph {
17//!       adj: Vec<Vec<usize>>,
18//!}
19//!
20//!
21//!impl Graph {
22//!    fn new(n: usize, edges: &[(usize, usize)]) -> Self {
23//!        let mut adj = vec![Vec::new(); n];
24//!        for &(u, v) in edges {
25//!            adj[u].push(v);
26//!            adj[v].push(u);
27//!        }
28//!        for list in &mut adj {
29//!            list.sort() // Necessary to make the derived `==` correct
30//!        }
31//!        Graph { adj }
32//!    }
33//!}
34//!
35//!// The Canonize trait allows to use the canonial form algorithms
36//!impl Canonize for Graph {
37//!    fn size(&self) -> usize {
38//!        self.adj.len()
39//!    }
40//!    fn apply_morphism(&self, perm: &[usize]) -> Self {
41//!        let mut adj = vec![Vec::new(); self.size()];
42//!        for (i, nbrs) in self.adj.iter().enumerate() {
43//!            adj[perm[i]] = nbrs.iter().map(|&u| perm[u]).collect();
44//!            adj[perm[i]].sort();
45//!        }
46//!        Graph { adj }
47//!    }
48//!    fn invariant_neighborhood(&self, u: usize) -> Vec<Vec<usize>> {
49//!        vec![self.adj[u].clone()]
50//!    }
51//!}
52//!
53//! // Usage of library functions
54//! // Two isomorphic graphs
55//! let c5 = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]);
56//! let other_c5 = Graph::new(5, &[(0, 2), (2, 1), (1, 4), (4, 3), (3, 0)]);
57//! assert_eq!(c5.canonical(), other_c5.canonical());
58//!
59//! // Non-isomorphic graphs
60//! let p5 = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
61//! assert!(c5.canonical() != p5.canonical());
62//!
63//! // Recovering the permutation that gives the canonical form
64//! let p = c5.morphism_to_canonical();
65//! assert_eq!(c5.apply_morphism(&p), c5.canonical());
66//!
67//! // Enumerating automorphisms
68//! assert_eq!(c5.canonical().automorphisms().count(), 10)
69//!```
70
71#![warn(
72    missing_docs,
73    missing_debug_implementations,
74    missing_copy_implementations,
75    trivial_casts,
76    trivial_numeric_casts,
77    unsafe_code,
78    unstable_features,
79    unused_import_braces,
80    unused_qualifications,
81    unused_labels,
82    unused_results
83)]
84
85mod refine;
86
87use crate::refine::Partition;
88use std::collections::btree_map::Entry::{Occupied, Vacant};
89use std::collections::BTreeMap;
90use std::rc::Rc;
91pub mod example;
92
93/// Objects that can be reduced modulo the actions of a permutation group.
94///
95/// An object that implement this trait has a set of elements assimilated to
96/// {0,...,n-1} on which the group of permutations can act.
97/// The purpose of the trait is to compute a normal form of
98/// the object modulo the permutation of its elements.
99pub trait Canonize
100where
101    Self: Sized + Ord + Clone,
102{
103    /// Return the number of vertices.
104    ///
105    /// The elements of `self` are assimilated to the number of `0..self.size()`.
106    fn size(&self) -> usize;
107
108    /// Return the result of the action of a permuation `p` on the object.
109    ///
110    /// The input `p` is guarenteed to be a permutation represented
111    /// as a slice of size `self.size()`
112    /// where `p[u]` is the image of `u` by the permutation.
113    ///
114    /// The action of the permutation set on `Self` is assumed to be a group action.
115    /// ```
116    /// use canonical_form::Canonize;
117    /// use canonical_form::example::Graph;
118    ///
119    /// let g = Graph::new(4, &[(1, 2), (2, 3)]);
120    ///
121    /// let p = &[1, 2, 0, 3];
122    /// assert_eq!(g.apply_morphism(p), Graph::new(4, &[(2, 0), (0, 3)]));
123    ///
124    /// let identity = &[0, 1, 2, 3];
125    /// assert_eq!(g.apply_morphism(identity), g);
126    ///
127    /// let q = &[1, 0, 3, 2];
128    /// let p_circle_q = &[2, 1, 3, 0];
129    /// assert_eq!(
130    ///     g.apply_morphism(q).apply_morphism(p),
131    ///     g.apply_morphism(p_circle_q)
132    /// )
133    /// ```
134    fn apply_morphism(&self, p: &[usize]) -> Self;
135
136    /// Optionally returns a value for each node that is invariant by isomorphism.
137    ///
138    /// If defined, the returned vector `c` must have size `self.len()`, where `c[u]`
139    /// is the value associated to the element `u`. It must satisfy the property that if
140    /// `c[u]` and `c[v]` are different then no automorphism of `self`
141    /// maps `u` to `v`.
142    fn invariant_coloring(&self) -> Option<Vec<u64>> {
143        None
144    }
145
146    /// Return lists of vertices that are invariant isomorphism.
147    ///
148    /// This function helps the algorithm to be efficient.
149    /// The output `invar` is such that each `invar[i]` is a vector
150    /// of vertices `[v1, ..., vk]`
151    /// (so the `vi`s are elements of `0..self.size()`) such that
152    /// for every permutation `p`,
153    /// `self.apply_morphism(p).invariant_neighborhood(p[u])[i]`
154    /// is equal to `[p[v1], ..., p[vk]]` up to reordering.
155    ///
156    /// The length of the output (the number of lists) has to be independent from `u`.
157    fn invariant_neighborhood(&self, _u: usize) -> Vec<Vec<usize>> {
158        Vec::new()
159    }
160
161    /// Computes a canonical form of a combinatorial object.
162    ///
163    /// This is the main function provided by this trait.
164    /// A canonical form is a function that assigns to an object `g` (e.g. a graph)
165    /// another object of sane type `g.canonical()` that is isomorphic to `g`
166    /// with the property that `g1` and `g2` are isomorphic if and only if
167    /// `g1.canocial() == g2.canonical()`.
168    /// ```
169    /// use canonical_form::Canonize;
170    /// use canonical_form::example::Graph;
171    ///
172    /// let p5 = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
173    /// let other_p5 = Graph::new(5, &[(3, 4), (0, 4), (0, 2), (1, 2)]);
174    /// assert_eq!(p5.canonical(), other_p5.canonical());
175    ///
176    /// let not_p5 = Graph::new(5, &[(0, 1), (1, 2), (2, 0), (3, 4)]);
177    /// assert!(p5.canonical() != not_p5.canonical());
178    /// ```
179    fn canonical(&self) -> Self {
180        self.canonical_typed(0)
181    }
182
183    /// The "typed" objects refers to the case where only
184    /// the action of permutations that are constant
185    /// on `0..sigma` are considered.
186    ///
187    /// So `g.canonical_typed(sigma)` returns a normal form of `g`
188    /// modulo the permutations that stabilize the `sigma` first vertices.
189    /// ```
190    /// use canonical_form::Canonize;
191    /// use canonical_form::example::Graph;
192    ///
193    /// // p5 with the edge (0, 1) at an end
194    /// let p5 = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
195    /// // p5 with the edge (0, 1) is in the middle
196    /// let p5_bis = Graph::new(5, &[(3, 4), (4, 1), (1, 0), (0, 2)]);
197    /// // If we fix the vertices 0 and 1, these two (rooted) graphs are different
198    /// assert!(p5.canonical_typed(2) != p5_bis.canonical_typed(2));
199    ///
200    /// let p5_ter = Graph::new(5, &[(0, 1), (1, 3), (3, 4), (4, 2)]);
201    /// assert_eq!(p5.canonical_typed(2), p5_ter.canonical_typed(2));
202    /// ```
203    fn canonical_typed(&self, sigma: usize) -> Self {
204        let partition = Partition::with_singletons(self.size(), sigma);
205        canonical_constraint(self, partition)
206    }
207
208    #[inline]
209    /// Return a permutation `p` such that `self.apply_morphism(&p) = self.canonical()`.
210    /// ```
211    /// use canonical_form::Canonize;
212    /// use canonical_form::example::Graph;
213    ///
214    /// let g = Graph::new(6, &[(0, 1), (1, 2), (2, 3), (3, 4), (3, 5)]);
215    /// let p = g.morphism_to_canonical();
216    /// assert_eq!(g.apply_morphism(&p), g.canonical());
217    /// ```
218    fn morphism_to_canonical(&self) -> Vec<usize> {
219        self.morphism_to_canonical_typed(0)
220    }
221
222    /// Return a permutation `phi` such that
223    /// `g.apply_morphism(&phi) = canonical_typed(&g, sigma)`.
224    /// ```
225    /// use canonical_form::Canonize;
226    /// use canonical_form::example::Graph;
227    ///
228    /// let g = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
229    /// let p = g.morphism_to_canonical_typed(2);
230    /// assert_eq!(g.apply_morphism(&p), g.canonical_typed(2));
231    /// ```
232    fn morphism_to_canonical_typed(&self, sigma: usize) -> Vec<usize> {
233        assert!(sigma <= self.size());
234        let partition = Partition::with_singletons(self.size(), sigma);
235        morphism_to_canonical_constraint(self, partition)
236    }
237
238    /// Iterator on the automorphism group of `g`.
239    ///
240    /// The input `g` must be in normal form.
241    /// ```
242    /// use canonical_form::Canonize;
243    /// use canonical_form::example::Graph;
244    ///
245    /// let c6 = Graph::new(6, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]).canonical();
246    ///
247    /// let mut count = 0;
248    /// for p in c6.automorphisms() {
249    ///     assert_eq!(c6.apply_morphism(&p), c6);
250    ///     count += 1;
251    /// }
252    /// assert_eq!(count, 12);
253    /// ```
254    #[inline]
255    fn automorphisms(&self) -> AutomorphismIterator<Self> {
256        self.stabilizer(0)
257    }
258
259    /// Iterator on the automorphisms of `g`
260    /// that fix the `sigma` first vertices.
261    ///
262    /// The input `g` must be in normal form computed with `canonical_typed`.
263    /// ```
264    /// use canonical_form::Canonize;
265    /// use canonical_form::example::Graph;
266    ///
267    /// // Cube graph with one fixed vertex
268    /// let cube = Graph::new(8, &[(0, 1), (1, 2), (2, 3), (3, 0),
269    ///                            (4, 5), (5, 6), (6, 7), (7, 4),
270    ///                            (0, 4), (1, 5), (2, 6), (3, 7)]).canonical_typed(1);
271    ///
272    /// let mut count = 0;
273    /// for p in cube.stabilizer(1) {
274    ///     assert_eq!(cube.apply_morphism(&p), cube);
275    ///     assert_eq!(p[0], 0);
276    ///     count += 1;
277    /// }
278    /// assert_eq!(count, 6);
279    /// ```
280    #[inline]
281    fn stabilizer(&self, sigma: usize) -> AutomorphismIterator<Self> {
282        let mut partition = Partition::simple(self.size());
283        for i in 0..sigma {
284            let _ = partition.individualize(i);
285        }
286        AutomorphismIterator::new(self, partition)
287    }
288}
289
290/// Return the next part to be refined.
291/// This part is chosen as a smallest part with at least 2 elements.
292/// Return None is the partition is discrete.
293fn target_selector(part: &Partition) -> Option<usize> {
294    let mut min = usize::max_value();
295    let mut arg_min = None;
296    for i in part.parts() {
297        let length = part.part(i).len();
298        if 2 <= length && (length < min) {
299            min = length;
300            arg_min = Some(i);
301        }
302    }
303    arg_min
304}
305
306fn precompute_invariant<F>(g: &F) -> Vec<Vec<Vec<usize>>>
307where
308    F: Canonize,
309{
310    let n = g.size();
311    let mut res = Vec::with_capacity(n);
312    for i in 0..n {
313        res.push(g.invariant_neighborhood(i))
314    }
315    res
316}
317
318/// Compute the coarsest refinement of `partition` with part undistinguishable
319/// by the invarriants.
320/// If `new_part` is `Some(p)`, assumes that the partition is up-to-date up to the creation
321/// of the part `p`.
322fn refine(partition: &mut Partition, invariants: &[Vec<Vec<usize>>], new_part: Option<usize>) {
323    if !partition.is_discrete() {
324        let n = partition.num_elems();
325        assert!(n >= 2);
326        let invariant_size = invariants[0].len();
327        debug_assert!(invariants.iter().all(|v| v.len() == invariant_size));
328        // Stack contains the new created partitions
329        let mut stack: Vec<_> = match new_part {
330            Some(p) => vec![p],
331            None => partition.parts().collect(),
332        };
333        // base
334        let max_step = ((n + 1 - partition.num_parts()) as u64).pow(invariant_size as u32);
335        let threshold = u64::max_value() / max_step; //
336        let mut part_buffer = Vec::new();
337        while !stack.is_empty() && !partition.is_discrete() {
338            let mut weight = 1; // multiplicator to make the values in the sieve unique
339            while let Some(part) = stack.pop() {
340                part_buffer.clear();
341                part_buffer.extend_from_slice(partition.part(part));
342                let factor = (part_buffer.len() + 1) as u64;
343                for i in 0..invariant_size {
344                    weight *= factor;
345                    // Compute sieve
346                    for &u in &part_buffer {
347                        for &v in &invariants[u][i] {
348                            partition.sieve(v, weight)
349                        }
350                    }
351                }
352                if weight > threshold {
353                    break;
354                };
355            }
356            partition.split(|new| {
357                stack.push(new);
358            })
359        }
360    }
361}
362
363/// Return the first index on which `u` and `v` differ.
364fn fca(u: &[usize], v: &[usize]) -> usize {
365    let mut i = 0;
366    while i < u.len() && i < v.len() && u[i] == v[i] {
367        i += 1;
368    }
369    i
370}
371
372/// Node of the tree of the normalization process
373#[derive(Clone, Debug)]
374struct IsoTreeNode {
375    nparts: usize,
376    children: Vec<usize>,
377    inv: Rc<Vec<Vec<Vec<usize>>>>,
378}
379
380impl IsoTreeNode {
381    fn root<F: Canonize>(partition: &mut Partition, g: &F) -> Self {
382        let inv = Rc::new(precompute_invariant(g));
383        if let Some(coloring) = g.invariant_coloring() {
384            partition.refine_by_value(&coloring, |_| {})
385        }
386        Self::new(partition, inv, None)
387    }
388    fn new(
389        partition: &mut Partition,
390        inv: Rc<Vec<Vec<Vec<usize>>>>,
391        new_part: Option<usize>,
392    ) -> Self {
393        refine(partition, &inv, new_part);
394        Self {
395            children: match target_selector(partition) {
396                Some(set) => partition.part(set).to_vec(),
397                None => Vec::new(),
398            },
399            nparts: partition.num_parts(),
400            inv,
401        }
402    }
403    fn explore(&self, v: usize, pi: &mut Partition) -> Self {
404        debug_assert!(self.is_restored(pi));
405        let new_part = pi.individualize(v);
406        Self::new(pi, self.inv.clone(), new_part)
407    }
408    // Should never be used
409    fn dummy() -> Self {
410        Self {
411            children: Vec::new(),
412            nparts: 1,
413            inv: Rc::new(Vec::new()),
414        }
415    }
416    fn restore(&self, partition: &mut Partition) {
417        partition.undo(self.nparts)
418    }
419    fn is_restored(&self, partition: &mut Partition) -> bool {
420        partition.num_parts() == self.nparts
421    }
422}
423
424/// Normal form of `g` under the action of isomorphisms that
425/// stabilize the parts of `partition`.
426fn canonical_constraint<F>(g: &F, mut partition: Partition) -> F
427where
428    F: Canonize,
429{
430    // contains the images of `g` already computed associated to the path to the corresponding leaf
431    let mut zeta: BTreeMap<F, Vec<usize>> = BTreeMap::new();
432    let mut tree = Vec::new(); // A stack of IsoTreeNode
433    let mut path = Vec::new(); // Current path as a vector of chosen vertices
434    let mut node = IsoTreeNode::root(&mut partition, g);
435    loop {
436        // If we have a leaf, treat it
437        if let Some(phi) = partition.as_bijection() {
438            match zeta.entry(g.apply_morphism(phi)) {
439                Occupied(entry) =>
440                // We are in a branch isomorphic to a branch we explored
441                {
442                    let k = fca(entry.get(), &path) + 1;
443                    tree.truncate(k);
444                    path.truncate(k);
445                }
446                Vacant(entry) => {
447                    let _ = entry.insert(path.clone());
448                }
449            }
450        };
451        // If there is a child, explore it
452        if let Some(u) = node.children.pop() {
453            let new_node = node.explore(u, &mut partition);
454            tree.push(node);
455            path.push(u);
456            node = new_node;
457        } else {
458            match tree.pop() {
459                Some(n) => {
460                    node = n;
461                    let _ = path.pop();
462                    node.restore(&mut partition); // backtrack the partition
463                }
464                None => break,
465            }
466        };
467    }
468    let (g_max, _) = zeta.into_iter().next_back().unwrap(); // return the largest image found
469    g_max
470}
471
472/// Iterator on the automorphisms of a combinatorial structure.
473#[derive(Clone, Debug)]
474pub struct AutomorphismIterator<F> {
475    tree: Vec<IsoTreeNode>,
476    node: IsoTreeNode,
477    partition: Partition,
478    g: F,
479}
480
481impl<F: Canonize> AutomorphismIterator<F> {
482    /// Iterator on the automorphisms of `g` that preserve `partition`.
483    fn new(g: &F, mut partition: Partition) -> Self {
484        debug_assert!(g == &canonical_constraint(g, partition.clone()));
485        Self {
486            tree: vec![IsoTreeNode::root(&mut partition, g)],
487            partition,
488            node: IsoTreeNode::dummy(), // Dummy node that will be unstacked at the first iteration
489            g: g.clone(),
490        }
491    }
492}
493
494impl<F: Canonize> Iterator for AutomorphismIterator<F> {
495    type Item = Vec<usize>;
496    #[inline]
497    fn next(&mut self) -> Option<Self::Item> {
498        loop {
499            if let Some(u) = self.node.children.pop() {
500                let new_node = self.node.explore(u, &mut self.partition);
501                let old_node = std::mem::replace(&mut self.node, new_node);
502                self.tree.push(old_node)
503            } else {
504                match self.tree.pop() {
505                    Some(n) => {
506                        n.restore(&mut self.partition);
507                        self.node = n
508                    }
509                    None => return None,
510                }
511            }
512            if let Some(phi) = self.partition.as_bijection() {
513                if self.g.apply_morphism(phi) == self.g {
514                    return Some(phi.to_vec());
515                }
516            };
517        }
518    }
519}
520
521/// Return a morphism `phi`
522/// such that `g.apply_morphism(phi) = canonical_constraint(g, partition)`.
523fn morphism_to_canonical_constraint<F>(g: &F, mut partition: Partition) -> Vec<usize>
524where
525    F: Canonize,
526{
527    // initialisation
528    let mut tree = Vec::new();
529    let mut node = IsoTreeNode::root(&mut partition, g);
530    let mut max = None;
531    let mut phimax = Vec::new();
532    loop {
533        if let Some(phi) = partition.as_bijection() {
534            // If node is a leaf
535            let phi_g = Some(g.apply_morphism(phi));
536            if phi_g > max {
537                max = phi_g;
538                phimax = phi.to_vec();
539            }
540        };
541        if let Some(u) = node.children.pop() {
542            let new_node = node.explore(u, &mut partition);
543            tree.push(node);
544            node = new_node;
545        } else {
546            match tree.pop() {
547                Some(n) => {
548                    n.restore(&mut partition);
549                    node = n
550                }
551                None => break,
552            }
553        }
554    }
555    phimax
556}
557
558/// Tests
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[derive(Ord, PartialOrd, PartialEq, Eq, Clone, Debug)]
564    struct Graph {
565        adj: Vec<Vec<usize>>,
566    }
567
568    impl Graph {
569        fn new(n: usize, edges: &[(usize, usize)]) -> Self {
570            let mut adj = vec![Vec::new(); n];
571            for &(u, v) in edges {
572                adj[u].push(v);
573                adj[v].push(u);
574            }
575            Graph { adj }
576        }
577    }
578
579    impl Canonize for Graph {
580        fn size(&self) -> usize {
581            self.adj.len()
582        }
583        fn apply_morphism(&self, perm: &[usize]) -> Self {
584            let mut adj = vec![Vec::new(); self.size()];
585            for (i, nbrs) in self.adj.iter().enumerate() {
586                adj[perm[i]] = nbrs.iter().map(|&u| perm[u]).collect();
587                adj[perm[i]].sort();
588            }
589            Graph { adj }
590        }
591        fn invariant_neighborhood(&self, u: usize) -> Vec<Vec<usize>> {
592            vec![self.adj[u].clone()]
593        }
594    }
595
596    #[test]
597    fn graph() {
598        let c5 = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]);
599        let other_c5 = Graph::new(5, &[(0, 2), (2, 1), (1, 4), (4, 3), (3, 0)]);
600        assert_eq!(c5.canonical(), other_c5.canonical());
601
602        let p5 = Graph::new(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
603        assert!(c5.canonical() != p5.canonical());
604
605        let p = c5.morphism_to_canonical();
606        assert_eq!(c5.apply_morphism(&p), c5.canonical());
607    }
608
609    #[test]
610    fn empty_graphs() {
611        let empty = Graph::new(0, &[]);
612        assert_eq!(empty, empty.canonical());
613        assert_eq!(empty, empty.canonical_typed(0));
614        assert_eq!(empty.automorphisms().count(), 1);
615    }
616    
617    #[test]
618    fn automorphisms_iterator() {
619        let c4 = Graph::new(4, &[(0, 1), (1, 2), (2, 3), (3, 0)]).canonical();
620        let mut count = 0;
621        for phi in c4.automorphisms() {
622            assert_eq!(c4.apply_morphism(&phi), c4);
623            count += 1;
624        }
625        assert_eq!(count, 8)
626    }
627}