Copy code from private repository

This commit is contained in:
Owen Troke-Billard 2024-09-16 10:39:31 -06:00
parent 1a20a6d43d
commit 8a4c88eff9
11 changed files with 1402 additions and 2 deletions

20
.gitignore vendored Normal file
View File

@ -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

11
Cargo.toml Normal file
View File

@ -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 }

View File

@ -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. 6981,
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. 13671372,
Oct. 2004, doi: https://doi.org/10.1109/tpami.2004.75.

209
src/builder.rs Normal file
View File

@ -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<NodeEq>,
/// Edge equality function.
edge_eq: Option<EdgeEq>,
}
/// 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(&<Query as Graph>::NodeLabel, &<Data as Graph>::NodeLabel) -> bool,
fn(&<Query as Graph>::EdgeLabel, &<Data as Graph>::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<Data::NodeLabel>,
Query::EdgeLabel: PartialEq<Data::EdgeLabel>,
{
Vf2ppBuilder {
problem: self.problem,
query: self.query,
data: self.data,
node_eq: Some(<Query::NodeLabel as PartialEq<Data::NodeLabel>>::eq),
edge_eq: Some(<Query::EdgeLabel as PartialEq<Data::EdgeLabel>>::eq),
}
}
/// Configures VF2++ to use `node_eq` as the node equality function.
pub fn node_eq<NewNodeEq>(
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<NewEdgeEq>(
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<Isomorphism> {
self.iter().into_next()
}
/// Returns a vector of isomorphisms
/// from the query graph to the data graph.
pub fn vec(self) -> Vec<Isomorphism> {
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,
}

46
src/graph.rs Normal file
View File

@ -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<Item = NodeIndex>;
/// 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,
}

7
src/isomorphism.rs Normal file
View File

@ -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<NodeIndex>;

68
src/iter.rs Normal file
View File

@ -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<NodeEq>,
edge_eq: Option<EdgeEq>,
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<Isomorphism> {
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::Item> {
self.next_ref().cloned()
}
}

12
src/lib.rs Normal file
View File

@ -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::*;

59
src/petgraph.rs Normal file
View File

@ -0,0 +1,59 @@
use crate::{Direction, Graph, NodeIndex};
use petgraph::adj::IndexType;
use petgraph::EdgeType;
use std::fmt::Debug;
impl<N, E, Ty, Ix> Graph for petgraph::Graph<N, E, Ty, Ix>
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::<Ix>::new(index))
}
#[inline]
fn neighbors(&self, node: NodeIndex, direction: Direction) -> impl Iterator<Item = NodeIndex> {
self.neighbors_directed(
petgraph::graph::NodeIndex::<Ix>::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::<Ix>::new(source),
petgraph::graph::NodeIndex::<Ix>::new(target),
)
}
#[inline]
fn edge_label(&self, source: NodeIndex, target: NodeIndex) -> Option<&Self::EdgeLabel> {
self.find_edge(
petgraph::graph::NodeIndex::<Ix>::new(source),
petgraph::graph::NodeIndex::<Ix>::new(target),
)
.and_then(|index| self.edge_weight(index))
}
}

527
src/state.rs Normal file
View File

@ -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<Source>,
/// The previous candidate pair at the current depth.
previous: Option<Pair>,
/// Node equality function.
node_eq: Option<NodeEq>,
/// Edge equality function.
edge_eq: Option<EdgeEq>,
}
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<NodeEq>,
edge_eq: Option<EdgeEq>,
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<Pair> {
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<Pair> {
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<Pair> {
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<NodeIndex> {
&self.query.map
}
/// Returns the query partial map.
pub(crate) fn into_query_map(self) -> Vec<NodeIndex> {
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<NodeIndex>,
/// 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<usize>,
/// 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<usize>,
/// 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<NodeIndex>,
}
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<NodeIndex> {
self.next_node(source, 0)
}
/// Returns the next node in `source` beginning at `skip`.
fn next_node(&self, source: Source, skip: usize) -> Option<NodeIndex> {
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<Item = NodeIndex> + '_ {
(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<Item = NodeIndex> + '_ {
(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,
}
}
}

401
tests/isomorphisms.rs Normal file
View File

@ -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::<Directed>();
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::<Undirected>();
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::<Directed>();
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::<Undirected>();
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::<Directed>();
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::<Directed>();
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::<Undirected>();
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::<Directed>();
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::<Directed>();
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::<Directed>();
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::<Directed>();
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::<Directed>();
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::<Directed>();
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::<Directed>();
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::<Directed>();
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<D: EdgeType>() -> (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<D: EdgeType>() -> (Graph<Color, Color, D>, Graph<Color, Color, D>) {
let query = Graph::<Color, Color, D>::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::<Color, Color, D>::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,
}