Copy code from private repository
This commit is contained in:
parent
1a20a6d43d
commit
8a4c88eff9
|
@ -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
|
||||
|
|
@ -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 }
|
44
README.md
44
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.
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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>;
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
Loading…
Reference in New Issue