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}