Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function that returns an iterator with subgraph isomorphisms #500

Merged
merged 4 commits into from Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
223 changes: 211 additions & 12 deletions src/algo/isomorphism.rs
@@ -1,3 +1,5 @@
use std::convert::TryFrom;

use crate::data::DataMap;
use crate::visit::EdgeCount;
use crate::visit::EdgeRef;
Expand Down Expand Up @@ -552,11 +554,40 @@ mod matching {

/// Return Some(bool) if isomorphism is decided, else None.
pub fn try_match<G0, G1, NM, EM>(
mut st: &mut (Vf2State<'_, G0>, Vf2State<'_, G1>),
st: &mut (Vf2State<'_, G0>, Vf2State<'_, G1>),
node_match: &mut NM,
edge_match: &mut EM,
match_subgraph: bool,
) -> Option<bool>
where
G0: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
G1: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
NM: NodeMatcher<G0, G1>,
EM: EdgeMatcher<G0, G1>,
{
let mut stack = vec![Frame::Outer];
if isomorphisms(st, node_match, edge_match, match_subgraph, &mut stack).is_some() {
Some(true)
} else {
None
}
}

fn isomorphisms<G0, G1, NM, EM>(
st: &mut (Vf2State<'_, G0>, Vf2State<'_, G1>),
node_match: &mut NM,
edge_match: &mut EM,
match_subgraph: bool,
stack: &mut Vec<Frame<G0, G1>>,
) -> Option<Vec<usize>>
where
G0: NodeCompactIndexable
+ EdgeCount
Expand All @@ -572,19 +603,19 @@ mod matching {
EM: EdgeMatcher<G0, G1>,
{
if st.0.is_complete() {
return Some(true);
return Some(st.0.mapping.clone());
}

// A "depth first" search of a valid mapping from graph 1 to graph 2
// F(s, n, m) -- evaluate state s and add mapping n <-> m
// Find least T1out node (in st.out[1] but not in M[1])
let mut stack: Vec<Frame<G0, G1>> = vec![Frame::Outer];
let mut result = None;
while let Some(frame) = stack.pop() {
match frame {
Frame::Unwind { nodes, open_list } => {
pop_state(&mut st, nodes);
pop_state(st, nodes);

match next_from_ix(&mut st, nodes.0, open_list) {
match next_from_ix(st, nodes.0, open_list) {
None => continue,
Some(nx) => {
let f = Frame::Inner {
Expand All @@ -595,7 +626,7 @@ mod matching {
}
}
}
Frame::Outer => match next_candidate(&mut st) {
Frame::Outer => match next_candidate(st) {
None => continue,
Some((nx, mx, open_list)) => {
let f = Frame::Inner {
Expand All @@ -606,10 +637,10 @@ mod matching {
}
},
Frame::Inner { nodes, open_list } => {
if is_feasible(&mut st, nodes, node_match, edge_match) {
push_state(&mut st, nodes);
if is_feasible(st, nodes, node_match, edge_match) {
push_state(st, nodes);
if st.0.is_complete() {
return Some(true);
result = Some(st.0.mapping.clone());
}
// Check cardinalities of Tin, Tout sets
if (!match_subgraph
Expand All @@ -624,9 +655,9 @@ mod matching {
stack.push(Frame::Outer);
continue;
}
pop_state(&mut st, nodes);
pop_state(st, nodes);
}
match next_from_ix(&mut st, nodes.0, open_list) {
match next_from_ix(st, nodes.0, open_list) {
None => continue,
Some(nx) => {
let f = Frame::Inner {
Expand All @@ -638,8 +669,137 @@ mod matching {
}
}
}
if result.is_some() {
return result;
}
}
result
}

pub struct GraphMatcher<'a, 'b, 'c, G0, G1, NM, EM>
where
G0: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
G1: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
NM: NodeMatcher<G0, G1>,
EM: EdgeMatcher<G0, G1>,
{
st: (Vf2State<'a, G0>, Vf2State<'b, G1>),
node_match: &'c mut NM,
edge_match: &'c mut EM,
match_subgraph: bool,
stack: Vec<Frame<G0, G1>>,
}

impl<'a, 'b, 'c, G0, G1, NM, EM> GraphMatcher<'a, 'b, 'c, G0, G1, NM, EM>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you implement Debug and Clone for the iterator ?

where
G0: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
G1: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
NM: NodeMatcher<G0, G1>,
EM: EdgeMatcher<G0, G1>,
{
pub fn new(
g0: &'a G0,
g1: &'b G1,
node_match: &'c mut NM,
edge_match: &'c mut EM,
match_subgraph: bool,
) -> Self {
let stack = vec![Frame::Outer];
Self {
st: (Vf2State::new(g0), Vf2State::new(g1)),
node_match,
edge_match,
match_subgraph,
stack,
}
}
}
ABorgna marked this conversation as resolved.
Show resolved Hide resolved

impl<'a, 'b, 'c, G0, G1, NM, EM> Iterator for GraphMatcher<'a, 'b, 'c, G0, G1, NM, EM>
where
G0: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
G1: NodeCompactIndexable
+ EdgeCount
+ GetAdjacencyMatrix
+ GraphProp
+ IntoNeighborsDirected,
NM: NodeMatcher<G0, G1>,
EM: EdgeMatcher<G0, G1>,
{
type Item = Vec<usize>;

fn next(&mut self) -> Option<Self::Item> {
isomorphisms(
&mut self.st,
self.node_match,
self.edge_match,
self.match_subgraph,
&mut self.stack,
)
}

fn size_hint(&self) -> (usize, Option<usize>) {
// To calculate the upper bound of results we use n! where n is the
// number of nodes in graph 1. n! values fit into a 64-bit usize up
// to n = 20, so we don't estimate an upper limit for n > 20.
let n = self.st.0.graph.node_count();
const MAX_N: usize = 20;

if n > MAX_N {
return (0, None);
}
ABorgna marked this conversation as resolved.
Show resolved Hide resolved

// We hardcode n! values into an array that accounts for architectures
// with smaller usizes to get our upper bound.
let upper_bounds: Vec<Option<usize>> = vec![
1u64,
1,
2,
6,
24,
120,
720,
5040,
40320,
362880,
3628800,
39916800,
479001600,
6227020800,
87178291200,
1307674368000,
20922789888000,
355687428096000,
6402373705728000,
121645100408832000,
2432902008176640000,
]
.iter()
.map(|n| usize::try_from(*n).ok())
.collect();

ABorgna marked this conversation as resolved.
Show resolved Hide resolved
(0, upper_bounds[n])
}
None
}
}

Expand Down Expand Up @@ -794,3 +954,42 @@ where
let mut st = (Vf2State::new(&g0), Vf2State::new(&g1));
self::matching::try_match(&mut st, &mut node_match, &mut edge_match, true).unwrap_or(false)
}

/// Using the VF2 algorithm, examine both syntactic and semantic graph
/// isomorphism (graph structure and matching node and edge weights) and,
/// if `g0` is isomorphic to a subgraph of `g1`, return the mappings between
/// them.
///
/// The graphs should not be multigraphs.
pub fn subgraph_isomorphisms_iter<'a, G0, G1, NM, EM>(
g0: &'a G0,
g1: &'a G1,
node_match: &'a mut NM,
edge_match: &'a mut EM,
) -> Option<impl Iterator<Item = Vec<usize>> + 'a>
where
G0: 'a
+ NodeCompactIndexable
+ EdgeCount
+ DataMap
+ GetAdjacencyMatrix
+ GraphProp
+ IntoEdgesDirected,
G1: 'a
+ NodeCompactIndexable
+ EdgeCount
+ DataMap
+ GetAdjacencyMatrix
+ GraphProp<EdgeType = G0::EdgeType>
+ IntoEdgesDirected,
NM: 'a + FnMut(&G0::NodeWeight, &G1::NodeWeight) -> bool,
EM: 'a + FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> bool,
{
if g0.node_count() > g1.node_count() || g0.edge_count() > g1.edge_count() {
return None;
}

Some(self::matching::GraphMatcher::new(
g0, g1, node_match, edge_match, true,
))
}
1 change: 1 addition & 0 deletions src/algo/mod.rs
Expand Up @@ -40,6 +40,7 @@ pub use feedback_arc_set::greedy_feedback_arc_set;
pub use floyd_warshall::floyd_warshall;
pub use isomorphism::{
is_isomorphic, is_isomorphic_matching, is_isomorphic_subgraph, is_isomorphic_subgraph_matching,
subgraph_isomorphisms_iter,
};
pub use k_shortest_path::k_shortest_path;
pub use matching::{greedy_matching, maximum_matching, Matching};
Expand Down
50 changes: 49 additions & 1 deletion tests/iso.rs
@@ -1,13 +1,16 @@
extern crate petgraph;

use std::collections::HashSet;
use std::fs::File;
use std::io::prelude::*;

use petgraph::graph::{edge_index, node_index};
use petgraph::prelude::*;
use petgraph::EdgeType;

use petgraph::algo::{is_isomorphic, is_isomorphic_matching, is_isomorphic_subgraph};
use petgraph::algo::{
is_isomorphic, is_isomorphic_matching, is_isomorphic_subgraph, subgraph_isomorphisms_iter,
};

/// Petersen A and B are isomorphic
///
Expand Down Expand Up @@ -493,6 +496,51 @@ fn iso_subgraph() {
assert!(is_isomorphic_subgraph(&g0, &g1));
}

#[test]
fn iter_subgraph() {
let a = Graph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 0)]);
let b = Graph::<(), ()>::from_edges(&[(0, 1), (1, 2), (2, 0), (2, 3), (0, 4)]);
let a_ref = &a;
let b_ref = &b;
let mut node_match = { |x: &(), y: &()| x == y };
let mut edge_match = { |x: &(), y: &()| x == y };

let mappings =
subgraph_isomorphisms_iter(&a_ref, &b_ref, &mut node_match, &mut edge_match).unwrap();

// Verify the iterator returns the expected mappings
let expected_mappings: Vec<Vec<usize>> = vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]];
for mapping in mappings {
assert!(expected_mappings.contains(&mapping))
}

// Verify all the mappings from the iterator are different
let a = str_to_digraph(COXETER_A);
let b = str_to_digraph(COXETER_B);
let a_ref = &a;
let b_ref = &b;

let mut unique = HashSet::new();
assert!(
subgraph_isomorphisms_iter(&a_ref, &b_ref, &mut node_match, &mut edge_match)
.unwrap()
.all(|x| unique.insert(x))
);

// The iterator should return None for graphs that are not isomorphic
let a = str_to_digraph(G8_1);
let b = str_to_digraph(G8_2);
let a_ref = &a;
let b_ref = &b;

assert!(
subgraph_isomorphisms_iter(&a_ref, &b_ref, &mut node_match, &mut edge_match)
.unwrap()
.next()
.is_none()
);
}

/// Isomorphic pair
const COXETER_A: &str = "
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1
Expand Down