diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..643ee7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +### IDEs +.idea +.vscode + +### Rust template +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ed9ffa6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vf2" +version = "0.1.0" +edition = "2021" + +[features] +default = ["petgraph"] +petgraph = ["dep:petgraph"] + +[dependencies] +petgraph = { version = "0.6", optional = true, default-features = false } diff --git a/README.md b/README.md index f965962..8af69a2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ -# vf2 -VF2 subgraph isomorphism algorithm in Rust. +# `gb_vf2pp` — VF2++ in Rust + +This crate implements the VF2++ subgraph isomorphism algorithm [1]. +It can find +[graph isomorphisms](https://en.wikipedia.org/wiki/Graph_isomorphism), +[subgraph isomorphisms](https://en.wikipedia.org/wiki/Subgraph_isomorphism_problem), +and [induced subgraph isomorphisms](https://en.wikipedia.org/wiki/Induced_subgraph_isomorphism_problem). + +# Features + +This is a work in progress. Some features are not yet implemented. + +- [x] Enumerate graph isomorphisms +- [x] Enumerate subgraph isomorphisms +- [x] Enumerate induced subgraph isomorphisms +- [ ] Find minimum cost isomorphism +- [x] Support directed graphs +- [x] Support undirected graphs +- [x] Support disconnected graphs +- [x] Support node labels +- [x] Support edge labels +- [x] Graph trait +- [ ] Performance benchmarks +- [ ] Test databases +- [ ] Examples + +# Remaining work + +- [ ] Implement VF2 cutting rules +- [ ] Implement all of VF2++ (only VF2 implemented so far) + +# References + +[1] A. Jüttner and P. Madarasi, +“VF2++—An improved subgraph isomorphism algorithm,” +Discrete Applied Mathematics, vol. 242, pp. 69–81, +Jun. 2018, doi: https://doi.org/10.1016/j.dam.2018.02.018. + +[2] L. P. Cordella, P. Foggia, C. Sansone, and M. Vento, +“A (sub)graph isomorphism algorithm for matching large graphs,” +IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 26, no. 10, pp. 1367–1372, +Oct. 2004, doi: https://doi.org/10.1109/tpami.2004.75. diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..9ab37de --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,209 @@ +use crate::{Graph, Isomorphism, IsomorphismIter}; +use std::fmt::Debug; + +/// Creates a new [`Vf2ppBuilder`] to find +/// isomorphisms from `query` to `data`. +/// +/// Node and edge equality are not checked by default. +/// Use [`node_eq`], [`edge_eq`], and [`default_eq`] +/// on the builder to set equality functions. +/// +/// [`node_eq`]: Vf2ppBuilder::node_eq +/// [`edge_eq`]: Vf2ppBuilder::edge_eq +/// [`default_eq`]: Vf2ppBuilder::default_eq +pub fn isomorphisms<'a, Query, Data>( + query: &'a Query, + data: &'a Data, +) -> DefaultVf2ppBuilder<'a, Query, Data> +where + Query: Graph, + Data: Graph, +{ + DefaultVf2ppBuilder::new(Problem::Isomorphism, query, data) +} + +/// Creates a new [`Vf2ppBuilder`] to find +/// subgraph isomorphisms from `query` to `data`. +/// +/// Node and edge equality are not checked by default. +/// Use [`node_eq`], [`edge_eq`], and [`default_eq`] +/// on the builder to set equality functions. +/// +/// [`node_eq`]: Vf2ppBuilder::node_eq +/// [`edge_eq`]: Vf2ppBuilder::edge_eq +/// [`default_eq`]: Vf2ppBuilder::default_eq +pub fn subgraph_isomorphisms<'a, Query, Data>( + query: &'a Query, + data: &'a Data, +) -> DefaultVf2ppBuilder<'a, Query, Data> +where + Query: Graph, + Data: Graph, +{ + DefaultVf2ppBuilder::new(Problem::SubgraphIsomorphism, query, data) +} + +/// Creates a new [`Vf2ppBuilder`] to find +/// induced subgraph isomorphisms from `query` to `data`. +/// +/// Node and edge equality are not checked by default. +/// Use [`node_eq`], [`edge_eq`], and [`default_eq`] +/// on the builder to set equality functions. +/// +/// [`node_eq`]: Vf2ppBuilder::node_eq +/// [`edge_eq`]: Vf2ppBuilder::edge_eq +/// [`default_eq`]: Vf2ppBuilder::default_eq +pub fn induced_subgraph_isomorphisms<'a, Query, Data>( + query: &'a Query, + data: &'a Data, +) -> DefaultVf2ppBuilder<'a, Query, Data> +where + Query: Graph, + Data: Graph, +{ + DefaultVf2ppBuilder::new(Problem::InducedSubgraphIsomorphism, query, data) +} + +/// A VF2++ builder. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Vf2ppBuilder<'a, Query, Data, NodeEq, EdgeEq> { + /// Problem type. + problem: Problem, + /// Query graph. + query: &'a Query, + /// Data graph. + data: &'a Data, + /// Node equality function. + node_eq: Option, + /// Edge equality function. + edge_eq: Option, +} + +/// Default VF2++ builder type. +/// +/// This is [`Vf2ppBuilder`] with function pointers as +/// the node and edge equality function types. +pub type DefaultVf2ppBuilder<'a, Query, Data> = Vf2ppBuilder< + 'a, + Query, + Data, + fn(&::NodeLabel, &::NodeLabel) -> bool, + fn(&::EdgeLabel, &::EdgeLabel) -> bool, +>; + +impl<'a, Query, Data> DefaultVf2ppBuilder<'a, Query, Data> +where + Query: Graph, + Data: Graph, +{ + /// Creates a new [`Vf2ppBuilder`] that does not check + /// node and edge equality. + fn new(problem: Problem, query: &'a Query, data: &'a Data) -> Self { + Self { + problem, + query, + data, + node_eq: None, + edge_eq: None, + } + } +} + +impl<'a, Query, Data, NodeEq, EdgeEq> Vf2ppBuilder<'a, Query, Data, NodeEq, EdgeEq> +where + Query: Graph, + Data: Graph, + NodeEq: Fn(&Query::NodeLabel, &Data::NodeLabel) -> bool, + EdgeEq: Fn(&Query::EdgeLabel, &Data::EdgeLabel) -> bool, +{ + /// Configures VF2++ to use the [`PartialEq`] implementations + /// for node and edge equalities. + pub fn default_eq(self) -> DefaultVf2ppBuilder<'a, Query, Data> + where + Query::NodeLabel: PartialEq, + Query::EdgeLabel: PartialEq, + { + Vf2ppBuilder { + problem: self.problem, + query: self.query, + data: self.data, + node_eq: Some(>::eq), + edge_eq: Some(>::eq), + } + } + + /// Configures VF2++ to use `node_eq` as the node equality function. + pub fn node_eq( + self, + node_eq: NewNodeEq, + ) -> Vf2ppBuilder<'a, Query, Data, NewNodeEq, EdgeEq> + where + NewNodeEq: Fn(&Query::NodeLabel, &Data::NodeLabel) -> bool, + { + Vf2ppBuilder { + problem: self.problem, + query: self.query, + data: self.data, + node_eq: Some(node_eq), + edge_eq: self.edge_eq, + } + } + + /// Configures VF2++ to use `edge_eq` as the edge equality function. + pub fn edge_eq( + self, + edge_eq: NewEdgeEq, + ) -> Vf2ppBuilder<'a, Query, Data, NodeEq, NewEdgeEq> + where + NewEdgeEq: Fn(&Query::EdgeLabel, &Data::EdgeLabel) -> bool, + { + Vf2ppBuilder { + problem: self.problem, + query: self.query, + data: self.data, + node_eq: self.node_eq, + edge_eq: Some(edge_eq), + } + } + + /// Returns the first isomorphism + /// from the query graph to the data graph. + pub fn first(self) -> Option { + self.iter().into_next() + } + + /// Returns a vector of isomorphisms + /// from the query graph to the data graph. + pub fn vec(self) -> Vec { + self.iter().collect() + } + + /// Returns an iterator of isomorphisms + /// from the query graph to the data graph. + pub fn iter(self) -> IsomorphismIter<'a, Query, Data, NodeEq, EdgeEq> { + if self.problem == Problem::Isomorphism { + assert_eq!( + self.query.node_count(), + self.data.node_count(), + "graphs must be the same size" + ); + } + let induced = match self.problem { + Problem::Isomorphism => true, + Problem::SubgraphIsomorphism => false, + Problem::InducedSubgraphIsomorphism => true, + }; + IsomorphismIter::new(self.query, self.data, self.node_eq, self.edge_eq, induced) + } +} + +/// Problem type. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Problem { + /// Graph isomorphism. + Isomorphism, + /// Subgraph isomorphism. + SubgraphIsomorphism, + /// Induced subgraph isomorphism. + InducedSubgraphIsomorphism, +} diff --git a/src/graph.rs b/src/graph.rs new file mode 100644 index 0000000..e50fd44 --- /dev/null +++ b/src/graph.rs @@ -0,0 +1,46 @@ +/// A graph. +pub trait Graph { + /// Node label type. + type NodeLabel; + + /// Edge label type. + type EdgeLabel; + + /// Returns `true` if the graph is directed + /// or `false` if the graph is undirected. + fn is_directed(&self) -> bool; + + /// Returns the number of nodes in the graph. + fn node_count(&self) -> usize; + + /// Returns a reference to the label of `node`; + fn node_label(&self, node: NodeIndex) -> Option<&Self::NodeLabel>; + + /// Returns an iterator of neighbors of `node`. + /// + /// If the graph is directed, returns neighbors in `direction` only. + /// If undirected, ignores `direction` and returns all neighbors. + fn neighbors(&self, node: NodeIndex, direction: Direction) -> impl Iterator; + + /// Returns `true` if there is an edge from `source` to `target`. + /// + /// If the graph is directed, the edge must must go from `source` to `target`. + /// If undirected, an edge must exist between `source` and `target`. + fn contains_edge(&self, source: NodeIndex, target: NodeIndex) -> bool; + + /// Returns a reference to the label of the edge from `source` to `target`. + /// + /// If the graph is directed, the edge must must go from `source` to `target`. + /// If undirected, the edge must be between `source` and `target`. + fn edge_label(&self, source: NodeIndex, target: NodeIndex) -> Option<&Self::EdgeLabel>; +} + +/// A node index. +pub type NodeIndex = usize; + +/// Edge direction. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Direction { + Outgoing, + Incoming, +} diff --git a/src/isomorphism.rs b/src/isomorphism.rs new file mode 100644 index 0000000..5c4a8f0 --- /dev/null +++ b/src/isomorphism.rs @@ -0,0 +1,7 @@ +use crate::NodeIndex; + +/// An isomorphism mapping query nodes to data nodes. +/// +/// The value at index `i` is the data node index +/// that query node index `i` maps to. +pub type Isomorphism = Vec; diff --git a/src/iter.rs b/src/iter.rs new file mode 100644 index 0000000..c51b55b --- /dev/null +++ b/src/iter.rs @@ -0,0 +1,68 @@ +use crate::state::State; +use crate::{Graph, Isomorphism}; +use std::fmt::Debug; + +/// An isomorphism iterator. +#[derive(Clone, Debug)] +pub struct IsomorphismIter<'a, Query, Data, NodeEq, EdgeEq> { + state: State<'a, Query, Data, NodeEq, EdgeEq>, +} + +impl<'a, Query, Data, NodeEq, EdgeEq> IsomorphismIter<'a, Query, Data, NodeEq, EdgeEq> +where + Query: Graph, + Data: Graph, + NodeEq: Fn(&Query::NodeLabel, &Data::NodeLabel) -> bool, + EdgeEq: Fn(&Query::EdgeLabel, &Data::EdgeLabel) -> bool, +{ + pub(crate) fn new( + query: &'a Query, + data: &'a Data, + node_eq: Option, + edge_eq: Option, + induced: bool, + ) -> Self { + Self { + state: State::new(query, data, node_eq, edge_eq, induced), + } + } + + /// Advances the search and returns the next isomorphism. + /// + /// Unlike [`next`], this does not allocate. + /// Returns [`None`] if the search is complete. + /// + /// [`next`]: Self::next + pub fn into_next(mut self) -> Option { + match self.next_ref() { + None => None, + Some(_) => Some(self.state.into_query_map()), + } + } + + /// Advances the search and returns a reference + /// to the next isomorphism. + /// + /// Unlike [`next`], this returns a reference so as not to allocate. + /// Returns [`None`] when the search is complete. + /// + /// [`next`]: Self::next + pub fn next_ref(&mut self) -> Option<&Isomorphism> { + while !self.state.step() {} + self.state.all_covered().then_some(self.state.query_map()) + } +} + +impl<'a, Query, Data, NodeEq, EdgeEq> Iterator for IsomorphismIter<'a, Query, Data, NodeEq, EdgeEq> +where + Query: Graph, + Data: Graph, + NodeEq: Fn(&Query::NodeLabel, &Data::NodeLabel) -> bool, + EdgeEq: Fn(&Query::EdgeLabel, &Data::EdgeLabel) -> bool, +{ + type Item = Isomorphism; + + fn next(&mut self) -> Option { + self.next_ref().cloned() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6d5c65c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +mod builder; +mod graph; +mod isomorphism; +mod iter; +#[cfg(feature = "petgraph")] +mod petgraph; +mod state; + +pub use builder::*; +pub use graph::*; +pub use isomorphism::*; +pub use iter::*; diff --git a/src/petgraph.rs b/src/petgraph.rs new file mode 100644 index 0000000..99a08d3 --- /dev/null +++ b/src/petgraph.rs @@ -0,0 +1,59 @@ +use crate::{Direction, Graph, NodeIndex}; +use petgraph::adj::IndexType; +use petgraph::EdgeType; +use std::fmt::Debug; + +impl Graph for petgraph::Graph +where + N: Debug, + E: Debug, + Ty: EdgeType, + Ix: IndexType, +{ + type NodeLabel = N; + type EdgeLabel = E; + + #[inline] + fn is_directed(&self) -> bool { + self.is_directed() + } + + #[inline] + fn node_count(&self) -> usize { + self.node_count() + } + + #[inline] + fn node_label(&self, index: NodeIndex) -> Option<&Self::NodeLabel> { + self.node_weight(petgraph::graph::NodeIndex::::new(index)) + } + + #[inline] + fn neighbors(&self, node: NodeIndex, direction: Direction) -> impl Iterator { + self.neighbors_directed( + petgraph::graph::NodeIndex::::new(node), + match direction { + Direction::Outgoing => petgraph::Direction::Outgoing, + Direction::Incoming => petgraph::Direction::Incoming, + }, + ) + .map(|neighbor| neighbor.index()) + } + + #[inline] + fn contains_edge(&self, source: NodeIndex, target: NodeIndex) -> bool { + self.contains_edge( + petgraph::graph::NodeIndex::::new(source), + petgraph::graph::NodeIndex::::new(target), + ) + } + + #[inline] + fn edge_label(&self, source: NodeIndex, target: NodeIndex) -> Option<&Self::EdgeLabel> { + self.find_edge( + petgraph::graph::NodeIndex::::new(source), + petgraph::graph::NodeIndex::::new(target), + ) + .and_then(|index| self.edge_weight(index)) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..c334561 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,527 @@ +use crate::{Direction, Graph, NodeIndex}; +use std::fmt::Debug; + +/// A reserved value indicating the node is uncovered. +/// Assumes the graph size is below [`NodeIndex::MAX`]. +const NOT_IN_MAP: NodeIndex = NodeIndex::MAX; + +/// A reserved value indicating the node is not in the set. +const NOT_IN_SET: NodeIndex = 0; + +#[derive(Clone, Debug)] +pub(crate) struct State<'a, Query, Data, NodeEq, EdgeEq> { + /// Whether the subgraph is induced. + induced: bool, + /// Depth in the SSR tree. + depth: usize, + /// Query graph state. + query: GraphState<'a, Query>, + /// Data graph state. + data: GraphState<'a, Data>, + /// A stack of candidate pair sources. + /// + /// The value at index `i` is the source of nodes at depth `i + 1`. + source_stack: Vec, + /// The previous candidate pair at the current depth. + previous: Option, + /// Node equality function. + node_eq: Option, + /// Edge equality function. + edge_eq: Option, +} + +impl<'a, Query, Data, NodeEq, EdgeEq> State<'a, Query, Data, NodeEq, EdgeEq> +where + Query: Graph, + Data: Graph, + NodeEq: Fn(&Query::NodeLabel, &Data::NodeLabel) -> bool, + EdgeEq: Fn(&Query::EdgeLabel, &Data::EdgeLabel) -> bool, +{ + /// Creates a new [`State`]. + pub(crate) fn new( + query: &'a Query, + data: &'a Data, + node_eq: Option, + edge_eq: Option, + induced: bool, + ) -> Self { + assert!(query.node_count() > 0, "query graph cannot be empty"); + assert!( + query.node_count() <= data.node_count(), + "query graph cannot have more nodes than data graph" + ); + assert!( + data.node_count() < NOT_IN_MAP, + "data graph is so large it uses reserved values" + ); + Self { + induced, + depth: 0, + query: GraphState::new(query), + data: GraphState::new(data), + source_stack: vec![Source::Outgoing; query.node_count()], + previous: None, + node_eq, + edge_eq, + } + } + + /// Advances the search one step. Returns `true` + /// if the map is ready or the search is complete. + pub(crate) fn step(&mut self) -> bool { + if let Some(pair) = self.next_pair() { + self.previous = Some(pair); + if self.feasible(pair) { + self.push(pair); + } + self.all_covered() + } else if self.depth > 0 { + self.pop(); + false + } else { + true + } + } + + /// Pushes `pair` to the partial map. Increments depth. + fn push(&mut self, pair: Pair) { + self.depth += 1; + self.previous = None; + self.query.push(pair.query_node, pair.data_node, self.depth); + self.data.push(pair.data_node, pair.query_node, self.depth); + } + + /// Pops the last pair from the partial map. Decrements depth. + fn pop(&mut self) { + self.previous = Some(Pair { + query_node: self.query.pop(self.depth), + data_node: self.data.pop(self.depth), + }); + self.depth -= 1; + } + + /// Returns the next candidate pair. + fn next_pair(&mut self) -> Option { + if self.all_covered() { + None + } else if let Some(previous) = self.previous { + let source = self.source_stack[self.depth]; + self.following_pair(source, previous) + } else { + self.first_pair().map(|(pair, source)| { + self.source_stack[self.depth] = source; + pair + }) + } + } + + /// Returns the first candidate pair and its source. + fn first_pair(&self) -> Option<(Pair, Source)> { + let source = if self.query.outgoing_size > 0 && self.data.outgoing_size > 0 { + Source::Outgoing + } else if self.query.incoming_size > 0 && self.data.incoming_size > 0 { + Source::Incoming + } else { + Source::Uncovered + }; + self.first_pair_in(source).map(|pair| (pair, source)) + } + + /// Returns the first candidate pair from `source`. + fn first_pair_in(&self, source: Source) -> Option { + if let Some(query_node) = self.query.first_node(source) { + if let Some(data_node) = self.data.first_node(source) { + return Some(Pair::new(query_node, data_node)); + } + } + None + } + + /// Returns the candidate pair from `source` following `previous`. + fn following_pair(&self, source: Source, previous: Pair) -> Option { + self.data + .next_node(source, previous.data_node + 1) + .map(|data_node| Pair::new(previous.query_node, data_node)) + } + + /// Returns `true` if a successor state would remain + /// consistent with `pair` in the partial map. + /// + /// This is *F(s, n, m)* in the original VF2 paper. + fn feasible(&self, pair: Pair) -> bool { + self.feasible_syntactic(pair) && self.feasible_semantic(pair) + } + + /// Returns `true` if a successor state would remain + /// syntactically consistent with `pair` in the partial map. + /// That is, if the graph structures would match. + /// + /// This is *F_syn* in the original VF2 paper. + fn feasible_syntactic(&self, pair: Pair) -> bool { + let consistent = if self.is_directed() { + self.rule_neighbors(pair, Direction::Incoming) + && self.rule_neighbors(pair, Direction::Outgoing) + } else { + // This will check all neighbors since the graphs are undirected. + self.rule_neighbors(pair, Direction::Incoming) + }; + consistent && self.rule_in(pair) && self.rule_out(pair) && self.rule_new(pair) + } + + /// Returns `true` if the predecessors or successors rule + /// is satisfied, depending on `direction`. + /// + /// [`Direction::Incoming`] is the predecessors rule. + /// + /// This is *R_pred* or *R_succ* in the original VF2 paper. + fn rule_neighbors(&self, pair: Pair, direction: Direction) -> bool { + let source_target = |node, neighbor| match direction { + Direction::Outgoing => (node, neighbor), + Direction::Incoming => (neighbor, node), + }; + for neighbor in self + .query + .graph + // If the graph is undirected, this returns all neighbors. + .neighbors(pair.query_node, direction) + .filter(|&n| self.query.is_covered(n)) + { + let mapped = self.query.map[neighbor]; + let (source, target) = source_target(pair.data_node, mapped); + if !self.data.graph.contains_edge(source, target) { + return false; + } + } + if !self.induced { + return true; + } + for neighbor in self + .data + .graph + // If the graph is undirected, this returns all neighbors. + .neighbors(pair.data_node, direction) + .filter(|&n| self.data.is_covered(n)) + { + let mapped = self.data.map[neighbor]; + let (source, target) = source_target(pair.query_node, mapped); + if !self.query.graph.contains_edge(source, target) { + return false; + } + } + true + } + + /// Returns `true` if the in rule is satisfied. + /// + /// This is *R_in* in the original VF2 paper. + fn rule_in(&self, _pair: Pair) -> bool { + // Not implemented. The algorithm works without + // this, but may be much slower. + true + } + + /// Returns `true` if the out rule is satisfied. + /// + /// This is *R_out* in the original VF2 paper. + fn rule_out(&self, _pair: Pair) -> bool { + // Not implemented. The algorithm works without + // this, but may be much slower. + true + } + + /// Returns `true` if the new rule is satisfied. + /// + /// This is *R_new* in the original VF2 paper. + fn rule_new(&self, _pair: Pair) -> bool { + // Not implemented. The algorithm works without + // this, but may be much slower. + true + } + + /// Returns `true` if a successor state would remain + /// semantically consistent with `pair` in the partial map. + /// That is, if the node and edge labels would match. + /// + /// This is *F_sem* in the original VF2 paper. + fn feasible_semantic(&self, pair: Pair) -> bool { + self.nodes_are_eq(pair) + && if self.is_directed() { + self.edges_are_eq(pair, Direction::Incoming) + && self.edges_are_eq(pair, Direction::Outgoing) + } else { + // This will check all neighbors since the graphs are undirected. + self.edges_are_eq(pair, Direction::Incoming) + } + } + + /// Returns `true` if the nodes in the pair + /// are semantically equivalent. + fn nodes_are_eq(&self, pair: Pair) -> bool { + let node_eq = match &self.node_eq { + None => return true, + Some(node_eq) => node_eq, + }; + node_eq( + self.query.node_label(pair.query_node), + self.data.node_label(pair.data_node), + ) + } + + /// Returns `true` if the pair edges in `direction` + /// are semantically equivalent. + fn edges_are_eq(&self, pair: Pair, direction: Direction) -> bool { + let edge_eq = match &self.edge_eq { + None => return true, + Some(edge_eq) => edge_eq, + }; + let source_target = |node, neighbor| match direction { + Direction::Outgoing => (node, neighbor), + Direction::Incoming => (neighbor, node), + }; + // If the graph is undirected, this returns all neighbors. + for neighbor in self + .query + .graph + .neighbors(pair.query_node, direction) + .filter(|&neighbor| self.query.is_covered(neighbor)) + { + let (query_source, query_target) = source_target(pair.query_node, neighbor); + let mapped = self.query.map[neighbor]; + let (data_source, data_target) = source_target(pair.data_node, mapped); + if !edge_eq( + self.query.edge_label(query_source, query_target), + self.data.edge_label(data_source, data_target), + ) { + return false; + } + } + true + } + + /// Returns a reference to the query partial map. + pub(crate) fn query_map(&self) -> &Vec { + &self.query.map + } + + /// Returns the query partial map. + pub(crate) fn into_query_map(self) -> Vec { + self.query.map + } + + /// Returns `true` if all query nodes are covered. + pub(crate) fn all_covered(&self) -> bool { + self.depth == self.query.map.len() + } + + /// Returns `true` if the graphs are directed. + fn is_directed(&self) -> bool { + self.query.graph.is_directed() + } +} + +#[derive(Clone, Debug)] +struct GraphState<'a, G> { + /// Graph. + /// + /// This is *G_1* or *G_2* in the original VF2 paper. + graph: &'a G, + /// A partial map of this graph's node indices to the other's. + /// + /// This is *M_1* or *M_2* in the original VF2 paper. + map: Vec, + /// Outgoing terminal set. + /// + /// This is *T^1_out* or *T^2_out* in the original VF2 paper. + /// + /// For undirected graphs, this set contains all the terminal + /// nodes and [`Self::incoming`] is unused. + /// + /// A nonzero value at index *n* indicates node *n* is either + /// in the set or covered by the partial map. + /// The value is the depth in the SSR tree at which the node was added. + outgoing: Vec, + /// Number of nodes in the outgoing terminal set. + outgoing_size: usize, + /// Incoming terminal set. + /// + /// This is *T^1_in* or *T^2_in* in the original VF2 paper. + incoming: Vec, + /// Number of nodes in the incoming terminal set. + incoming_size: usize, + /// Tracks the order nodes were added to the partial map. + /// + /// The value at index `i` is the node that + /// was added to the partial map at depth `i + 1`. + node_stack: Vec, +} + +impl<'a, G> GraphState<'a, G> +where + G: Graph, +{ + /// Creates a new [`GraphState`]. + fn new(graph: &'a G) -> Self { + Self { + graph, + map: vec![NOT_IN_MAP; graph.node_count()], + outgoing: vec![NOT_IN_SET; graph.node_count()], + outgoing_size: 0, + incoming: vec![NOT_IN_SET; graph.node_count()], + incoming_size: 0, + node_stack: vec![0; graph.node_count()], + } + } + + /// Returns the first node in `source`. + fn first_node(&self, source: Source) -> Option { + self.next_node(source, 0) + } + + /// Returns the next node in `source` beginning at `skip`. + fn next_node(&self, source: Source, skip: usize) -> Option { + match source { + Source::Outgoing => self.terminal_nodes(&self.outgoing, skip).next(), + Source::Incoming => self.terminal_nodes(&self.incoming, skip).next(), + Source::Uncovered => self.uncovered_nodes(skip).next(), + } + } + + /// Returns an iterator of nodes in the terminal set beginning at `skip`. + fn terminal_nodes( + &self, + set: &'a [usize], + skip: usize, + ) -> impl Iterator + '_ { + (skip..self.map.len()).filter(|&node| self.in_terminal_set(node, set)) + } + + /// Returns `true` if `node` is in the terminal set. + fn in_terminal_set(&self, node: NodeIndex, set: &[usize]) -> bool { + set[node] != NOT_IN_SET && !self.is_covered(node) + } + + /// Returns an iterator of uncovered nodes beginning at `skip`. + fn uncovered_nodes(&self, skip: usize) -> impl Iterator + '_ { + (skip..self.map.len()).filter(|&node| !self.is_covered(node)) + } + + /// Pushes a map from `node` to `to_node` to the partial map. + fn push(&mut self, node: NodeIndex, to_node: NodeIndex, depth: usize) { + self.node_stack[depth - 1] = node; + self.map[node] = to_node; + if self.outgoing[node] != NOT_IN_SET { + self.outgoing_size -= 1; + } + self.push_neighbors(node, Direction::Outgoing, depth); + if self.graph.is_directed() { + if self.incoming[node] != NOT_IN_SET { + self.incoming_size -= 1; + } + self.push_neighbors(node, Direction::Incoming, depth); + } + } + + /// Pushes neighbors of `node` in `direction` to the corresponding terminal set. + fn push_neighbors(&mut self, node: NodeIndex, direction: Direction, depth: usize) { + let (set, len) = match direction { + Direction::Outgoing => (&mut self.outgoing, &mut self.outgoing_size), + Direction::Incoming => (&mut self.incoming, &mut self.incoming_size), + }; + // If the graph is undirected, this returns all neighbors. + for neighbor in self.graph.neighbors(node, direction) { + if set[neighbor] == NOT_IN_SET { + set[neighbor] = depth; + if self.map[neighbor] == NOT_IN_MAP { + *len += 1; + } + } + } + } + + /// Pops the node at `depth` from the partial map and returns it. + fn pop(&mut self, depth: usize) -> NodeIndex { + let node = self.node_stack[depth - 1]; + self.map[node] = NOT_IN_MAP; + if self.outgoing[node] != NOT_IN_SET { + self.outgoing_size += 1; + } + self.pop_neighbors(node, Direction::Outgoing, depth); + if self.graph.is_directed() { + if self.incoming[node] != NOT_IN_SET { + self.incoming_size += 1; + } + self.pop_neighbors(node, Direction::Incoming, depth); + } + node + } + + /// Pops neighbors of `node` in `direction` from the corresponding + /// terminal set if they were added at `depth`. + fn pop_neighbors(&mut self, node: NodeIndex, direction: Direction, depth: usize) { + let (set, len) = match direction { + Direction::Outgoing => (&mut self.outgoing, &mut self.outgoing_size), + Direction::Incoming => (&mut self.incoming, &mut self.incoming_size), + }; + // If the graph is undirected, this returns all neighbors. + for neighbor in self.graph.neighbors(node, direction) { + if set[neighbor] == depth { + set[neighbor] = NOT_IN_SET; + if self.map[neighbor] == NOT_IN_MAP { + *len -= 1; + } + } + } + } + + /// Returns `true` if `node` is covered by the partial map. + fn is_covered(&self, node: NodeIndex) -> bool { + self.map[node] != NOT_IN_MAP + } + + /// Returns the label of `node`. + fn node_label(&self, node: NodeIndex) -> &G::NodeLabel { + self.graph.node_label(node).expect("node should exist") + } + + /// Returns the label of `node`. + /// + /// Has the same behaviour as [`Graph::edge_label`]. + fn edge_label(&self, source: NodeIndex, target: NodeIndex) -> &G::EdgeLabel { + self.graph + .edge_label(source, target) + .expect("edge should exist") + } +} + +/// Candidate pair source. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Source { + /// Uncovered neighbors of covered nodes that are edge destinations. + /// + /// This is *T_out* in the original VF2 paper. + Outgoing, + /// Uncovered neighbors of covered nodes that are edge sources. + /// + /// This is *T_in* in the original VF2 paper. + Incoming, + /// Uncovered nodes. + /// + /// This is *P^d* in the original VF2 paper. + Uncovered, +} + +/// A pair of query and data node indices. +#[derive(Copy, Clone, Debug)] +struct Pair { + query_node: NodeIndex, + data_node: NodeIndex, +} + +impl Pair { + fn new(query_node: NodeIndex, data_node: NodeIndex) -> Self { + Self { + query_node, + data_node, + } + } +} diff --git a/tests/isomorphisms.rs b/tests/isomorphisms.rs new file mode 100644 index 0000000..0fc2e64 --- /dev/null +++ b/tests/isomorphisms.rs @@ -0,0 +1,401 @@ +use petgraph::data::{Element, FromElements}; +use petgraph::graph::{DiGraph, UnGraph}; +use petgraph::{Directed, EdgeType, Graph, Undirected}; + +/// Tests graph isomorphism enumeration on directed graphs. +#[test] +fn isomorphisms_directed() { + let query = DiGraph::<(), ()>::from_edges([(0, 2), (1, 2), (2, 3)]); + let data = DiGraph::<(), ()>::from_edges([(0, 2), (1, 2), (2, 3)]); + + let isomorphisms = vf2::isomorphisms(&query, &data).vec(); + + assert_eq!(isomorphisms, vec![vec![0, 1, 2, 3], vec![1, 0, 2, 3]]); +} + +/// Tests graph isomorphism enumeration on undirected graphs. +#[test] +fn isomorphisms_undirected() { + let query = UnGraph::<(), ()>::from_edges([(0, 2), (1, 2), (2, 3)]); + let data = UnGraph::<(), ()>::from_edges([(0, 2), (1, 2), (2, 3)]); + + let isomorphisms = vf2::isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 2, 3], + vec![0, 3, 2, 1], + vec![1, 0, 2, 3], + vec![1, 3, 2, 0], + vec![3, 0, 2, 1], + vec![3, 1, 2, 0], + ] + ); +} + +/// Tests subgraph isomorphism enumeration on directed graphs. +#[test] +fn subgraph_isomorphisms_directed() { + let (query, data) = small_graphs::(); + + let isomorphisms = vf2::subgraph_isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 3, 4, 5], + vec![0, 2, 3, 4, 5], + vec![1, 0, 3, 4, 5], + vec![1, 2, 3, 4, 5], + vec![2, 0, 3, 4, 5], + vec![2, 1, 3, 4, 5], + ] + ); +} + +/// Tests subgraph isomorphism enumeration on undirected graphs. +#[test] +fn subgraph_isomorphisms_undirected() { + let (query, data) = small_graphs::(); + + let isomorphisms = vf2::subgraph_isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 3, 4, 5], + vec![0, 1, 3, 6, 7], + vec![0, 2, 3, 4, 5], + vec![0, 2, 3, 6, 7], + vec![0, 4, 3, 1, 2], + vec![0, 4, 3, 2, 1], + vec![0, 4, 3, 6, 7], + vec![0, 6, 3, 1, 2], + vec![0, 6, 3, 2, 1], + vec![0, 6, 3, 4, 5], + vec![1, 0, 3, 4, 5], + vec![1, 0, 3, 6, 7], + vec![1, 2, 3, 4, 5], + vec![1, 2, 3, 6, 7], + vec![1, 4, 3, 6, 7], + vec![1, 6, 3, 4, 5], + vec![2, 0, 3, 4, 5], + vec![2, 0, 3, 6, 7], + vec![2, 1, 3, 4, 5], + vec![2, 1, 3, 6, 7], + vec![2, 4, 3, 6, 7], + vec![2, 6, 3, 4, 5], + vec![4, 0, 3, 1, 2], + vec![4, 0, 3, 2, 1], + vec![4, 0, 3, 6, 7], + vec![4, 1, 3, 6, 7], + vec![4, 2, 3, 6, 7], + vec![4, 6, 3, 1, 2], + vec![4, 6, 3, 2, 1], + vec![6, 0, 3, 1, 2], + vec![6, 0, 3, 2, 1], + vec![6, 0, 3, 4, 5], + vec![6, 1, 3, 4, 5], + vec![6, 2, 3, 4, 5], + vec![6, 4, 3, 1, 2], + vec![6, 4, 3, 2, 1], + ] + ); +} + +/// Tests induced subgraph isomorphism enumeration on directed graphs. +#[test] +fn induced_subgraph_isomorphisms_directed() { + let (query, data) = small_graphs::(); + + let isomorphisms = vf2::induced_subgraph_isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 3, 4, 5], + vec![0, 2, 3, 4, 5], + vec![1, 0, 3, 4, 5], + vec![2, 0, 3, 4, 5], + ] + ); +} + +/// Tests induced subgraph isomorphism enumeration on undirected graphs. +#[test] +fn induced_subgraph_isomorphisms_undirected() { + let (query, data) = small_graphs::(); + + let isomorphisms = vf2::induced_subgraph_isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 3, 4, 5], + vec![0, 1, 3, 6, 7], + vec![0, 2, 3, 4, 5], + vec![0, 2, 3, 6, 7], + vec![0, 4, 3, 6, 7], + vec![0, 6, 3, 4, 5], + vec![1, 0, 3, 4, 5], + vec![1, 0, 3, 6, 7], + vec![1, 4, 3, 6, 7], + vec![1, 6, 3, 4, 5], + vec![2, 0, 3, 4, 5], + vec![2, 0, 3, 6, 7], + vec![2, 4, 3, 6, 7], + vec![2, 6, 3, 4, 5], + vec![4, 0, 3, 6, 7], + vec![4, 1, 3, 6, 7], + vec![4, 2, 3, 6, 7], + vec![6, 0, 3, 4, 5], + vec![6, 1, 3, 4, 5], + vec![6, 2, 3, 4, 5], + ] + ); +} + +/// Tests that node and edge labels are not compared by default. +#[test] +fn no_eq_by_default() { + let (query, data) = small_labeled_graphs::(); + + let isomorphisms = vf2::induced_subgraph_isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 3, 4, 5], + vec![0, 2, 3, 4, 5], + vec![1, 0, 3, 4, 5], + vec![2, 0, 3, 4, 5], + ] + ); +} + +/// Tests default equality functions on directed graphs. +#[test] +fn default_eq_directed() { + let (query, data) = small_labeled_graphs::(); + + let isomorphisms = vf2::induced_subgraph_isomorphisms(&query, &data) + .default_eq() + .vec(); + + assert_eq!(isomorphisms, vec![vec![0, 2, 3, 4, 5]]); +} + +/// Tests default equality functions on undirected graphs. +#[test] +fn default_eq_undirected() { + let (query, data) = small_labeled_graphs::(); + + let isomorphisms = vf2::induced_subgraph_isomorphisms(&query, &data) + .default_eq() + .vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 2, 3, 4, 5], + vec![0, 2, 3, 6, 7], + vec![4, 2, 3, 6, 7], + vec![6, 2, 3, 4, 5], + ] + ); +} + +/// Tests custom equality functions. +#[test] +fn custom_eq() { + let (query, data) = small_labeled_graphs::(); + + let isomorphisms = vf2::induced_subgraph_isomorphisms(&query, &data) + .node_eq(|left, right| left == right) + .edge_eq(|left, right| left == right) + .vec(); + + assert_eq!(isomorphisms, vec![vec![0, 2, 3, 4, 5]]); +} + +/// Tests enumeration on disconnected graphs. +#[test] +fn disconnected() { + let query = DiGraph::<(), ()>::from_edges([(0, 1), (2, 3)]); + let data = DiGraph::<(), ()>::from_edges([(0, 1), (1, 2), (3, 4)]); + + let isomorphisms = vf2::subgraph_isomorphisms(&query, &data).vec(); + + assert_eq!( + isomorphisms, + vec![ + vec![0, 1, 3, 4], + vec![1, 2, 3, 4], + vec![3, 4, 0, 1], + vec![3, 4, 1, 2], + ] + ); +} + +/// Tests that an empty query results in a panic. +#[test] +#[should_panic] +fn empty_query() { + let query = DiGraph::<(), ()>::new(); + let data = DiGraph::<(), ()>::from_edges([(0, 1), (1, 2)]); + + // Should panic since query is empty. + vf2::induced_subgraph_isomorphisms(&query, &data).vec(); +} + +/// Tests that query and data graphs must be the same size +/// when finding graph isomorphisms. +#[test] +#[should_panic] +fn isomorphisms_same_size() { + let query = DiGraph::<(), ()>::from_edges([(0, 1)]); + let data = DiGraph::<(), ()>::from_edges([(0, 1), (1, 2)]); + + // Should panic since query and data are not the same size. + vf2::isomorphisms(&query, &data).vec(); +} + +/// Tests that [`Debug`] is implemented for [`Vf2ppBuilder`]. +/// +/// [`Vf2ppBuilder`]: vf2::Vf2ppBuilder +#[test] +fn builder_debug() { + let (query, data) = small_graphs::(); + let builder = vf2::subgraph_isomorphisms(&query, &data); + + // This should not panic due to missing debug implementation. + let debug = format!("{builder:#?}"); + + assert!(!debug.is_empty()); +} + +/// Tests that [`Debug`] is implemented for [`IsomorphismIter`]. +/// +/// [`IsomorphismIter`]: vf2::IsomorphismIter +#[test] +fn iter_debug() { + let (query, data) = small_graphs::(); + let iter = vf2::subgraph_isomorphisms(&query, &data).iter(); + + // This should not panic due to missing debug implementation. + let debug = format!("{iter:#?}"); + + assert!(!debug.is_empty()); +} + +/// Tests finding only the first isomorphism. +#[test] +fn first() { + let (query, data) = small_graphs::(); + + let first = vf2::subgraph_isomorphisms(&query, &data).first(); + + assert_eq!(first, Some(vec![0, 1, 3, 4, 5])); +} + +/// Tests collecting isomorphisms into a vector. +#[test] +fn vec() { + let (query, data) = small_graphs::(); + + let vec = vf2::subgraph_isomorphisms(&query, &data).vec(); + + assert!(!vec.is_empty()); +} + +/// Tests getting an iterator of isomorphisms. +#[test] +fn iter() { + let (query, data) = small_graphs::(); + + let mut iter = vf2::subgraph_isomorphisms(&query, &data).iter(); + + assert!(iter.next().is_some()); +} + +/// Tests getting a reference to the next isomorphism. +#[test] +fn iter_next_ref() { + let (query, data) = small_graphs::(); + let mut iter = vf2::subgraph_isomorphisms(&query, &data).iter(); + + let next_ref = iter.next_ref(); + + assert_eq!(next_ref, Some(&vec![0, 1, 3, 4, 5])); +} + +/// Tests converting the iterator into the next isomorphism. +#[test] +fn iter_into_next() { + let (query, data) = small_graphs::(); + let iter = vf2::subgraph_isomorphisms(&query, &data).iter(); + + let next = iter.into_next(); + + assert_eq!(next, Some(vec![0, 1, 3, 4, 5])); +} + +/// Returns small query and data graphs used across tests. +fn small_graphs() -> (Graph<(), (), D>, Graph<(), (), D>) { + let query = Graph::<(), (), D>::from_edges([(0, 2), (1, 2), (2, 3), (3, 4)]); + let data = Graph::<(), (), D>::from_edges([ + (0, 3), + (1, 3), + (2, 3), + (1, 2), + (3, 4), + (4, 5), + (3, 6), + (7, 6), + ]); + (query, data) +} + +/// Returns small query and data graphs, +/// with node and edge labels, used across tests. +#[rustfmt::skip] +fn small_labeled_graphs() -> (Graph, Graph) { + let query = Graph::::from_elements([ + Element::Node { weight: Color::Black, }, + Element::Node { weight: Color::White, }, + Element::Node { weight: Color::White, }, + Element::Node { weight: Color::Black, }, + Element::Node { weight: Color::White, }, + Element::Edge { source: 0, target: 2, weight: Color::White }, + Element::Edge { source: 1, target: 2, weight: Color::Black }, + Element::Edge { source: 2, target: 3, weight: Color::White }, + Element::Edge { source: 3, target: 4, weight: Color::Black }, + ]); + let data = Graph::::from_elements([ + Element::Node { weight: Color::Black }, + Element::Node { weight: Color::White }, + Element::Node { weight: Color::White }, + Element::Node { weight: Color::White }, + Element::Node { weight: Color::Black }, + Element::Node { weight: Color::White }, + Element::Node { weight: Color::Black }, + Element::Node { weight: Color::White }, + Element::Edge { source: 0, target: 3, weight: Color::White }, + Element::Edge { source: 1, target: 3, weight: Color::White }, + Element::Edge { source: 2, target: 3, weight: Color::Black }, + Element::Edge { source: 1, target: 2, weight: Color::White }, + Element::Edge { source: 3, target: 4, weight: Color::White }, + Element::Edge { source: 4, target: 5, weight: Color::Black }, + Element::Edge { source: 3, target: 6, weight: Color::White }, + Element::Edge { source: 7, target: 6, weight: Color::Black }, + ]); + (query, data) +} + +/// A color enum used as node and edge labels. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum Color { + White, + Black, +}