#![allow(clippy::cast_possible_truncation)]
use std::collections::HashSet;
use crate::core::{Graph, IgraphError, IgraphResult};
pub(crate) mod automorphism_group;
pub(crate) mod canonical_permutation;
pub(crate) mod count_automorphisms;
pub(crate) mod isomorphic_bliss;
pub(crate) struct Canonicalization {
pub labeling: Vec<u32>,
pub generators: Vec<Vec<u32>>,
pub group_order: f64,
pub certificate: Vec<u64>,
}
#[allow(clippy::struct_field_names)]
struct Adj {
n: usize,
directed: bool,
out_adj: Vec<Vec<usize>>,
in_adj: Vec<Vec<usize>>,
mat: Vec<Vec<bool>>,
}
impl Adj {
fn build(graph: &Graph) -> IgraphResult<Self> {
let n = graph.vcount() as usize;
let directed = graph.is_directed();
let m = graph.ecount();
let mut out_adj = vec![Vec::new(); n];
let mut in_adj = vec![Vec::new(); n];
let mut mat = vec![vec![false; n]; n];
let mut seen: HashSet<(usize, usize)> = HashSet::with_capacity(m);
for eid in 0..m {
let (u, v) = graph.edge(u32::try_from(eid).map_err(|_| {
IgraphError::InvalidArgument("edge id exceeds u32 range".to_owned())
})?)?;
let (ui, vi) = (u as usize, v as usize);
let key = if directed || ui <= vi {
(ui, vi)
} else {
(vi, ui)
};
if !seen.insert(key) {
return Err(IgraphError::Unsupported(
"canonical labeling does not support multigraphs (multiple edges)",
));
}
out_adj[ui].push(vi);
mat[ui][vi] = true;
if directed {
in_adj[vi].push(ui);
} else {
out_adj[vi].push(ui);
mat[vi][ui] = true;
}
}
for nbrs in &mut out_adj {
nbrs.sort_unstable();
nbrs.dedup();
}
if directed {
for nbrs in &mut in_adj {
nbrs.sort_unstable();
nbrs.dedup();
}
} else {
in_adj.clone_from(&out_adj);
}
Ok(Self {
n,
directed,
out_adj,
in_adj,
mat,
})
}
}
fn renumber<K: Ord>(keys: &[K]) -> (Vec<usize>, usize) {
let n = keys.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| keys[a].cmp(&keys[b]));
let mut color = vec![0usize; n];
let mut cur = 0usize;
for idx in 0..n {
if idx > 0 && keys[order[idx]] != keys[order[idx - 1]] {
cur += 1;
}
color[order[idx]] = cur;
}
(color, if n == 0 { 0 } else { cur + 1 })
}
fn refine(color: &mut Vec<usize>, adj: &Adj) {
let n = adj.n;
if n == 0 {
return;
}
let mut sigs: Vec<u32> = Vec::new();
loop {
let k = color.iter().copied().max().map_or(0, |m| m + 1);
let width = if adj.directed { 2 * k } else { k };
sigs.clear();
sigs.resize(n * width, 0);
for v in 0..n {
let base = v * width;
for &w in &adj.out_adj[v] {
sigs[base + color[w]] += 1;
}
if adj.directed {
for &w in &adj.in_adj[v] {
sigs[base + k + color[w]] += 1;
}
}
}
let keys: Vec<(usize, &[u32])> = (0..n)
.map(|v| (color[v], &sigs[v * width..(v + 1) * width]))
.collect();
let (new_color, new_k) = renumber(&keys);
let stable = new_k == k;
*color = new_color;
if stable {
break;
}
}
}
fn individualize(color: &[usize], target: usize, v: usize) -> Vec<usize> {
let keys: Vec<(usize, u8)> = (0..color.len())
.map(|u| {
let rank = u8::from(u != v && color[u] == target);
(color[u], rank)
})
.collect();
renumber(&keys).0
}
fn certificate(lab: &[usize], adj: &Adj) -> Vec<u64> {
let n = adj.n;
let bits = n * n;
let words = bits.div_ceil(64);
let mut cert = vec![0u64; words];
let mut idx = 0usize;
for i in 0..n {
let row = &adj.mat[lab[i]];
for j in 0..n {
if row[lab[j]] {
cert[idx / 64] |= 1u64 << (idx % 64);
}
idx += 1;
}
}
cert
}
struct Search<'a> {
adj: &'a Adj,
best_cert: Option<Vec<u64>>,
canon_lab: Option<Vec<usize>>,
leaf_count: usize,
generators: Vec<Vec<u32>>,
uf_parent: Vec<usize>,
}
impl Search<'_> {
fn uf_find(&mut self, mut v: usize) -> usize {
let mut root = v;
while self.uf_parent[root] != root {
root = self.uf_parent[root];
}
while v != root {
let next = self.uf_parent[v];
self.uf_parent[v] = root;
v = next;
}
root
}
fn uf_union(&mut self, a: usize, b: usize) {
let ra = self.uf_find(a);
let rb = self.uf_find(b);
if ra != rb {
self.uf_parent[ra] = rb;
}
}
fn recurse(&mut self, mut color: Vec<usize>) {
refine(&mut color, self.adj);
let n = self.adj.n;
let k = color.iter().copied().max().map_or(0, |m| m + 1);
if k == n {
let mut lab = vec![0usize; n];
for (v, &pos) in color.iter().enumerate() {
lab[pos] = v;
}
let cert = certificate(&lab, self.adj);
match &self.best_cert {
None => {
self.best_cert = Some(cert);
self.canon_lab = Some(lab);
self.leaf_count = 1;
self.generators.clear();
self.uf_parent = (0..n).collect();
}
Some(bc) => match cert.cmp(bc) {
std::cmp::Ordering::Greater => {
self.best_cert = Some(cert);
self.canon_lab = Some(lab);
self.leaf_count = 1;
self.generators.clear();
self.uf_parent = (0..n).collect();
}
std::cmp::Ordering::Equal => {
self.leaf_count += 1;
self.record_automorphism(&lab);
}
std::cmp::Ordering::Less => {}
},
}
return;
}
let mut sizes = vec![0usize; k];
for &c in &color {
sizes[c] += 1;
}
let target = (0..k).find(|&c| sizes[c] > 1).unwrap_or(0);
let members: Vec<usize> = (0..n).filter(|&v| color[v] == target).collect();
for &v in &members {
let child = individualize(&color, target, v);
self.recurse(child);
}
}
fn record_automorphism(&mut self, lab: &[usize]) {
let n = self.adj.n;
let Some(canon) = &self.canon_lab else {
return;
};
let mut linv = vec![0usize; n];
for (pos, &v) in lab.iter().enumerate() {
linv[v] = pos;
}
let phi: Vec<u32> = (0..n).map(|v| canon[linv[v]] as u32).collect();
let merges_orbits = (0..n).any(|v| self.uf_find(v) != self.uf_find(phi[v] as usize));
if merges_orbits || self.generators.len() < n {
for (v, &pv) in phi.iter().enumerate() {
self.uf_union(v, pv as usize);
}
self.generators.push(phi);
}
}
}
#[cfg(test)]
fn compose(g: &[u32], p: &[u32]) -> Vec<u32> {
p.iter().map(|&pv| g[pv as usize]).collect()
}
#[cfg(test)]
fn group_closure(gens: &[Vec<u32>], n: usize) -> HashSet<Vec<u32>> {
let mut set: HashSet<Vec<u32>> = HashSet::new();
let id: Vec<u32> = (0..n as u32).collect();
set.insert(id.clone());
let mut frontier = vec![id];
while let Some(p) = frontier.pop() {
for g in gens {
let q = compose(g, &p);
if set.insert(q.clone()) {
frontier.push(q);
}
}
}
set
}
pub(crate) fn canonicalize(
graph: &Graph,
colors: Option<&[u32]>,
) -> IgraphResult<Canonicalization> {
let adj = Adj::build(graph)?;
let n = adj.n;
if let Some(c) = colors {
if c.len() != n {
return Err(IgraphError::InvalidArgument(format!(
"colour vector length {} does not match vertex count {n}",
c.len()
)));
}
}
let init_keys: Vec<(u32, u8)> = (0..n)
.map(|v| {
let col = colors.map_or(0, |c| c[v]);
(col, u8::from(adj.mat[v][v]))
})
.collect();
let (init_color, _) = renumber(&init_keys);
let mut search = Search {
adj: &adj,
best_cert: None,
canon_lab: None,
leaf_count: 0,
generators: Vec::new(),
uf_parent: (0..n).collect(),
};
search.recurse(init_color);
let Some(canon) = &search.canon_lab else {
return Ok(Canonicalization {
labeling: Vec::new(),
generators: Vec::new(),
group_order: 1.0,
certificate: Vec::new(),
});
};
let canon_certificate = search.best_cert.clone().unwrap_or_default();
let mut labeling = vec![0u32; n];
for (pos, &v) in canon.iter().enumerate() {
labeling[v] = pos as u32;
}
#[allow(clippy::cast_precision_loss)]
let group_order = search.leaf_count as f64;
Ok(Canonicalization {
labeling,
generators: search.generators,
group_order,
certificate: canon_certificate,
})
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)] mod tests {
use super::*;
fn brute_force_aut_count(graph: &Graph, colors: Option<&[u32]>) -> u64 {
let adj = Adj::build(graph).expect("adj");
let n = adj.n;
let mut perm: Vec<usize> = (0..n).collect();
let mut count = 0u64;
let mut c = vec![0usize; n];
let is_aut = |p: &[usize]| -> bool {
for u in 0..n {
if let Some(cl) = colors {
if cl[u] != cl[p[u]] {
return false;
}
}
for v in 0..n {
if adj.mat[u][v] != adj.mat[p[u]][p[v]] {
return false;
}
}
}
true
};
if n == 0 {
return 1;
}
if is_aut(&perm) {
count += 1;
}
let mut i = 0;
while i < n {
if c[i] < i {
if i % 2 == 0 {
perm.swap(0, i);
} else {
perm.swap(c[i], i);
}
if is_aut(&perm) {
count += 1;
}
c[i] += 1;
i = 0;
} else {
c[i] = 0;
i += 1;
}
}
count
}
fn cycle(n: u32, directed: bool) -> Graph {
let mut g = Graph::new(n, directed).expect("graph");
for i in 0..n {
g.add_edge(i, (i + 1) % n).expect("edge");
}
g
}
fn complete(n: u32) -> Graph {
let mut g = Graph::new(n, false).expect("graph");
for a in 0..n {
for b in (a + 1)..n {
g.add_edge(a, b).expect("edge");
}
}
g
}
fn path(n: u32) -> Graph {
let mut g = Graph::new(n, false).expect("graph");
for i in 0..n.saturating_sub(1) {
g.add_edge(i, i + 1).expect("edge");
}
g
}
fn petersen() -> Graph {
let mut g = Graph::new(10, false).expect("graph");
for i in 0..5 {
g.add_edge(i, (i + 1) % 5).expect("edge"); g.add_edge(i + 5, (i + 2) % 5 + 5).expect("edge"); g.add_edge(i, i + 5).expect("edge"); }
g
}
fn assert_generators_complete(graph: &Graph, colors: Option<&[u32]>, order: u64) {
let c = canonicalize(graph, colors).expect("canonicalize");
let n = graph.vcount() as usize;
let closure = group_closure(&c.generators, n);
assert_eq!(
closure.len() as u64,
order,
"generators do not generate the full automorphism group"
);
let adj = Adj::build(graph).expect("adj");
for g in &c.generators {
for u in 0..n {
for v in 0..n {
assert_eq!(
adj.mat[u][v], adj.mat[g[u] as usize][g[v] as usize],
"generator is not an automorphism"
);
}
}
}
}
fn check(graph: &Graph, colors: Option<&[u32]>) {
let expected = brute_force_aut_count(graph, colors);
let c = canonicalize(graph, colors).expect("canonicalize");
assert!(
(c.group_order - expected as f64).abs() < 0.5,
"engine |Aut| {} != brute force {}",
c.group_order,
expected
);
assert_generators_complete(graph, colors, expected);
}
#[test]
fn complete_graphs_have_factorial_automorphisms() {
let fact = [1u64, 1, 2, 6, 24, 120, 720];
for n in 1u32..=6 {
let c = canonicalize(&complete(n), None).expect("canon");
assert!((c.group_order - fact[n as usize] as f64).abs() < 0.5);
}
}
#[test]
fn cycle_graphs_have_dihedral_automorphisms() {
for n in 3u32..=7 {
let c = canonicalize(&cycle(n, false), None).expect("canon");
assert!((c.group_order - f64::from(2 * n)).abs() < 0.5, "C_{n}");
}
}
#[test]
fn path_graphs_have_two_automorphisms() {
for n in 2u32..=7 {
let c = canonicalize(&path(n), None).expect("canon");
assert!((c.group_order - 2.0).abs() < 0.5, "path_{n}");
}
let c = canonicalize(&path(1), None).expect("canon");
assert!((c.group_order - 1.0).abs() < 0.5);
}
#[test]
fn petersen_has_120_automorphisms() {
let c = canonicalize(&petersen(), None).expect("canon");
assert!((c.group_order - 120.0).abs() < 0.5);
assert_generators_complete(&petersen(), None, 120);
}
#[test]
fn directed_cycle_has_n_rotations() {
for n in 3u32..=7 {
let c = canonicalize(&cycle(n, true), None).expect("canon");
assert!((c.group_order - f64::from(n)).abs() < 0.5, "directed C_{n}");
}
}
#[test]
fn matches_brute_force_battery() {
check(&complete(5), None);
check(&cycle(6, false), None);
check(&path(6), None);
check(&petersen(), None);
check(&cycle(5, true), None);
let mut d = Graph::new(4, true).expect("graph");
for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)] {
d.add_edge(u, v).expect("edge");
}
check(&d, None);
let k4 = complete(4);
let colors = [0u32, 0, 1, 1];
check(&k4, Some(&colors));
let mut tri_loop = Graph::new(3, false).expect("graph");
for (u, v) in [(0, 1), (1, 2), (0, 2), (0, 0)] {
tri_loop.add_edge(u, v).expect("edge");
}
check(&tri_loop, None);
let mut two_edges = Graph::new(4, false).expect("graph");
two_edges.add_edge(0, 1).expect("edge");
two_edges.add_edge(2, 3).expect("edge");
check(&two_edges, None);
check(&Graph::new(4, false).expect("graph"), None);
}
}