From 8175b5c2fce56ff159dc14a421804b9a2221bd9e Mon Sep 17 00:00:00 2001 From: Ben Ruijl Date: Tue, 10 Sep 2024 15:03:56 +0200 Subject: [PATCH] Compute the automorphism group size during canonization - Compute the orbit during canonization - Take permutations of edges into account for automorphism group size computation - External edges can now give a color to the external node - Add mermaid output format for graphs - Fix fallback to common ancestor logic --- src/graph.rs | 339 ++++++++++++++++++++++++++++++++++++++++--------- src/tensors.rs | 2 +- 2 files changed, 279 insertions(+), 62 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 572778ee..d661c6a7 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -5,6 +5,8 @@ use std::{ hash::Hash, }; +use crate::domains::integer::Integer; + /// A node in a graph, with arbitrary data. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Node { @@ -133,6 +135,31 @@ impl Graph { out.push_str("}\n"); out } + + pub fn to_mermaid(&self) -> String { + let mut out = String::new(); + out.push_str("graph TD;\n"); + + for (i, x) in self.nodes.iter().enumerate() { + out.push_str(&format!(" {}[{}];\n", i, x.data)); + } + + for x in &self.edges { + if x.directed { + out.push_str(&format!( + " {} --> {}[{}];\n", + x.vertices.0, x.vertices.1, x.data + )); + } else { + out.push_str(&format!( + " {} --- {}[{}];\n", + x.vertices.0, x.vertices.1, x.data + )); + } + } + + out + } } #[derive(Clone, Debug)] @@ -357,6 +384,40 @@ impl Graph { } } +impl Graph { + /// Get the number of different ways to permute the multi-edges, leading + /// to the same graph, while keeping the vertices fixed. + pub fn get_edge_automorphism_group_size(&self) -> Integer { + let mut count = Integer::one(); + let mut h = HashMap::default(); + + for e in &self.edges { + h.entry(e.vertices) + .or_insert(vec![]) + .push((e.directed, &e.data)); + } + + for (_, mut v) in h.into_iter() { + v.sort(); + + let mut counter = 1; + let mut last = &v[0]; + for d in v.iter().skip(1) { + if d == last { + counter += 1; + } else { + count *= Integer::factorial(counter); + counter = 1; + last = d; + } + } + count *= Integer::factorial(counter); + } + + count + } +} + struct GenerationSettings<'a, E> { vertex_signatures: &'a [Vec], max_vertices: Option, @@ -367,17 +428,19 @@ struct GenerationSettings<'a, E> { max_degree: usize, } -impl Graph { - /// Generate all connected graphs with `external_edges` as external half-edges and the given allowed list +impl Graph { + /// Generate all connected graphs with `external_edges` half-edges and the given allowed list /// of vertex connections. + /// + /// Returns the canonical form of the graph and the size of its automorphism group (including edge permutations). pub fn generate( - external_edges: &[E], + external_edges: &[(N, E)], vertex_signatures: &[Vec], max_vertices: Option, max_loops: Option, max_bridges: Option, allow_self_loops: bool, - ) -> Vec { + ) -> HashMap, Integer> { let vertex_sorted: Vec<_> = vertex_signatures .iter() .map(|x| { @@ -388,17 +451,17 @@ impl Graph { .collect(); let mut g = Self::new(); - for _ in 0..external_edges.len() { - g.add_node(Empty); - g.add_node(Empty); + for (n, _) in external_edges { + g.add_node(n.clone()); + g.add_node(N::default()); } for (i, e) in external_edges.iter().enumerate() { - g.add_edge(i, external_edges.len() + i, false, e.clone()); + g.add_edge(i, external_edges.len() + i, false, e.1.clone()); } if external_edges.len() == 0 { - g.add_node(Empty); + g.add_node(N::default()); } let settings = GenerationSettings { @@ -411,7 +474,7 @@ impl Graph { max_degree: vertex_sorted.iter().map(|x| x.len()).max().unwrap_or(0), }; - let mut out = vec![]; + let mut out = HashMap::default(); g.generate_impl(external_edges.len(), &settings, &mut out); out } @@ -420,7 +483,7 @@ impl Graph { &mut self, cur_vertex: usize, settings: &GenerationSettings, - out: &mut Vec, + out: &mut HashMap, Integer>, ) { if let Some(max_vertices) = settings.max_vertices { if self.nodes.len() > max_vertices { @@ -493,7 +556,8 @@ impl Graph { } } - out.push(self.clone()); + let c = self.canonize(); + out.insert(c.graph, c.automorphism_group_size); return; } @@ -557,7 +621,7 @@ impl Graph { edge_count: &mut [(E, usize)], cur_edge_count_group_index: usize, settings: &GenerationSettings, - out: &mut Vec, + out: &mut HashMap, Integer>, ) { if edge_count.iter().all(|x| x.1 == 0) { return self.generate_impl(source + 1, settings, out); @@ -566,7 +630,7 @@ impl Graph { let mut grown = false; if cur_target == self.nodes.len() { grown = true; - self.add_node(Empty); + self.add_node(N::default()); } else { self.distribute_edges(source, cur_target + 1, edge_count, 0, settings, out); } @@ -613,33 +677,52 @@ impl Graph { } } +pub struct CanonicalForm { + /// Mapping of the vertices from the input graph to the canonical graph. + pub vertex_map: Vec, + pub orbit_generators: Vec>>, + pub orbit: Vec, + /// The size of the automorphism group of the graph, including + /// the permutations stemming from identical edges. + pub automorphism_group_size: Integer, + pub graph: Graph, +} + impl Graph { /// Canonize the graph using McKay's canonical graph labeling algorithm, /// returning the vertex mapping and the canonical form. - pub fn canonize(&self) -> (Vec, Self) { + pub fn canonize(&self) -> CanonicalForm { if self.nodes.is_empty() { - return (vec![], self.clone()); + return CanonicalForm { + vertex_map: vec![], + orbit_generators: vec![], + orbit: vec![], + automorphism_group_size: Integer::one(), + graph: self.clone(), + }; } if self.nodes.len() <= u16::MAX as usize { - let r = self.canonize_impl::(); - (r.0.into_iter().map(|x| x as usize).collect(), r.1) + self.canonize_impl::(false) } else if self.nodes.len() <= u32::MAX as usize { - let r = self.canonize_impl::(); - (r.0.into_iter().map(|x| x as usize).collect(), r.1) + self.canonize_impl::(false) } else { - self.canonize_impl::() + self.canonize_impl::(false) } } - fn canonize_impl(&self) -> (Vec, Self) { - let mut stack = vec![SearchTreeNode::new(self)]; + fn canonize_impl(&self, verbose: bool) -> CanonicalForm { + let mut stack = vec![SearchTreeNode::::new(self)]; let mut automorphisms = vec![]; + let mut minimal_representatives_per_generator = vec![]; let mut leaf_nodes: HashMap<_, (Vec<_>, Vec<_>)> = HashMap::default(); // TODO: limit growth let mut current_best: Option<(Graph<&N, &E>, Vec, Vec>)> = None; let mut node_buffer = vec![]; + let mut automorphism_group_len = Integer::one(); + let mut orbit = (0..self.nodes.len()).collect::>(); + while let Some(mut node) = stack.pop() { if node.selected_vertex.is_none() { node.refine(self); @@ -699,7 +782,9 @@ impl { n.partition.clear(); n.children_to_visit.clear(); + n.children_visited_equal_to_first = 0; + n.orig_selected_vertex = None; n.selected_part = None; n.selected_vertex = None; n @@ -834,6 +997,12 @@ impl SearchTreeNode::default(), }; + new_node.left_node = node.left_node && node.orig_selected_vertex.is_none(); + + if node.orig_selected_vertex.is_none() { + node.orig_selected_vertex = Some(x); + } + new_node .partition .extend(node.partition.iter().take(p).cloned()); @@ -860,7 +1029,40 @@ impl = (0..self.nodes.len()) + .map(|x| { + map.iter() + .position(|y| y.to_usize() == x) + .unwrap() + .to_usize() + }) + .collect(); + + // transform the automorphisms to the new vertex numbering + let automorphisms: Vec<_> = automorphisms + .into_iter() + .map(|x| { + x.into_iter() + .map(|y| { + y.into_iter() + .map(|z| inv_map[z.to_usize()]) + .collect::>() + }) + .collect::>() + }) + .collect(); + + let orbit = (0..orbit.len()) + .map(|x| inv_map[orbit[map[x].to_usize()]]) + .collect(); + + CanonicalForm { + vertex_map: inv_map, + orbit_generators: automorphisms, + automorphism_group_size: automorphism_group_len * g.get_edge_automorphism_group_size(), + orbit, + graph: g, + } } /// Returns `true` iff the graph is isomorphic to `other`. @@ -895,7 +1097,7 @@ impl { selected_part: Option, selected_vertex: Option, children_to_visit: Vec, + left_node: bool, + orig_selected_vertex: Option, + children_visited_equal_to_first: usize, invariant: Invariant, } @@ -977,6 +1182,9 @@ impl SearchTreeNode { selected_part: None, selected_vertex: None, children_to_visit: vec![], + orig_selected_vertex: None, + left_node: true, + children_visited_equal_to_first: 0, invariant: Invariant::default(), } } @@ -997,13 +1205,19 @@ impl SearchTreeNode { let largest_partition = self.partition.iter().map(|x| x.len()).max().unwrap(); let mut degrees = vec![(vec![], I::from_usize(0)); largest_partition]; + let mut last_stable_index = 0; // no splits happened before this index in the last round 'next: loop { for (ii, i) in self.partition.iter().enumerate() { if i.len() == 1 { continue; } - for j in &self.partition { + for (jj, j) in self.partition.iter().enumerate() { + if ii < last_stable_index && jj < last_stable_index { + // this part is already tested and stable + continue; + } + // sorted edge colors of edges in i that connect to vertices in j // the length of this vector is the degree of the vertex i in j for ((edge_data, vert), v) in degrees.iter_mut().zip(i) { @@ -1024,7 +1238,7 @@ impl SearchTreeNode { } } } - edge_data.sort(); + edge_data.sort_unstable(); *vert = *v; } @@ -1032,7 +1246,7 @@ impl SearchTreeNode { continue; } - degrees[..i.len()].sort(); + degrees[..i.len()].sort_unstable(); let mut degs = vec![]; let mut cur = vec![degrees[0].1]; @@ -1047,6 +1261,7 @@ impl SearchTreeNode { degs.push(cur); self.partition.splice(ii..=ii, degs); + last_stable_index = ii; continue 'next; } } @@ -1060,7 +1275,7 @@ impl SearchTreeNode { #[cfg(test)] mod test { - use crate::graph::{Empty, Graph, SearchTreeNode}; + use crate::graph::{Graph, SearchTreeNode}; #[test] fn directed() { @@ -1148,13 +1363,15 @@ mod test { let c = g.canonize(); - assert_eq!(c.1.edge(0).vertices, (0, 2)); + assert_eq!(c.orbit_generators.len(), 2); + assert_eq!(c.automorphism_group_size, 8); + assert_eq!(c.graph.edge(0).vertices, (0, 2)); } #[test] fn generate() { - let gs = Graph::::generate( - &["g", "g"], + let gs = Graph::<_, &str>::generate( + &[(1, "g"), (2, "g")], &[ vec!["g", "g", "g"], vec!["q", "qb", "g"], @@ -1166,6 +1383,6 @@ mod test { false, ); - assert_eq!(gs.len(), 129); + assert_eq!(gs.len(), 94); } } diff --git a/src/tensors.rs b/src/tensors.rs index bd3fab57..90aa5328 100644 --- a/src/tensors.rs +++ b/src/tensors.rs @@ -138,7 +138,7 @@ impl<'a> AtomView<'a> { } } - let (_, gc) = g.canonize(); + let gc = g.canonize().graph; let mut funcs = vec![]; for n in gc.nodes() {