From c6c2394bbd3dadb2ce9a89cd169375468841af16 Mon Sep 17 00:00:00 2001 From: Sergio Valverde Date: Tue, 26 Jul 2022 13:15:11 -0600 Subject: [PATCH] Add function that returns an iterator with subgraph isomorphisms (#500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add function that returns an iterator with subgraph isomorphisms * Add size hint to the subgraph isomorphism iterator * Remove unused mut from the isomorphism module * Apply suggestions from code review Co-authored-by: Agustín Borgna --- src/algo/isomorphism.rs | 222 +++++++++++++++++++++++++++++++++++++--- src/algo/mod.rs | 1 + tests/iso.rs | 50 ++++++++- 3 files changed, 260 insertions(+), 13 deletions(-) diff --git a/src/algo/isomorphism.rs b/src/algo/isomorphism.rs index febabcca1..5cc640cea 100644 --- a/src/algo/isomorphism.rs +++ b/src/algo/isomorphism.rs @@ -1,3 +1,5 @@ +use std::convert::TryFrom; + use crate::data::DataMap; use crate::visit::EdgeCount; use crate::visit::EdgeRef; @@ -552,11 +554,40 @@ mod matching { /// Return Some(bool) if isomorphism is decided, else None. pub fn try_match( - 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 + where + G0: NodeCompactIndexable + + EdgeCount + + GetAdjacencyMatrix + + GraphProp + + IntoNeighborsDirected, + G1: NodeCompactIndexable + + EdgeCount + + GetAdjacencyMatrix + + GraphProp + + IntoNeighborsDirected, + NM: NodeMatcher, + EM: EdgeMatcher, + { + 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( + st: &mut (Vf2State<'_, G0>, Vf2State<'_, G1>), + node_match: &mut NM, + edge_match: &mut EM, + match_subgraph: bool, + stack: &mut Vec>, + ) -> Option> where G0: NodeCompactIndexable + EdgeCount @@ -572,19 +603,19 @@ mod matching { EM: EdgeMatcher, { 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> = 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 { @@ -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 { @@ -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 @@ -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 { @@ -638,8 +669,136 @@ 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, + EM: EdgeMatcher, + { + st: (Vf2State<'a, G0>, Vf2State<'b, G1>), + node_match: &'c mut NM, + edge_match: &'c mut EM, + match_subgraph: bool, + stack: Vec>, + } + + impl<'a, 'b, 'c, G0, G1, NM, EM> GraphMatcher<'a, 'b, 'c, G0, G1, NM, EM> + where + G0: NodeCompactIndexable + + EdgeCount + + GetAdjacencyMatrix + + GraphProp + + IntoNeighborsDirected, + G1: NodeCompactIndexable + + EdgeCount + + GetAdjacencyMatrix + + GraphProp + + IntoNeighborsDirected, + NM: NodeMatcher, + EM: EdgeMatcher, + { + 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, + } + } + } + + 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, + EM: EdgeMatcher, + { + type Item = Vec; + + fn next(&mut self) -> Option { + isomorphisms( + &mut self.st, + self.node_match, + self.edge_match, + self.match_subgraph, + &mut self.stack, + ) + } + + fn size_hint(&self) -> (usize, Option) { + // 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(); + + // We hardcode n! values into an array that accounts for architectures + // with smaller usizes to get our upper bound. + let upper_bounds: Vec> = 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(); + + if n > upper_bounds.len() { + return (0, None); + } + + (0, upper_bounds[n]) } - None } } @@ -794,3 +953,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> + 'a> +where + G0: 'a + + NodeCompactIndexable + + EdgeCount + + DataMap + + GetAdjacencyMatrix + + GraphProp + + IntoEdgesDirected, + G1: 'a + + NodeCompactIndexable + + EdgeCount + + DataMap + + GetAdjacencyMatrix + + GraphProp + + 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, + )) +} diff --git a/src/algo/mod.rs b/src/algo/mod.rs index 6e40d7bf9..9ad4ec117 100644 --- a/src/algo/mod.rs +++ b/src/algo/mod.rs @@ -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}; diff --git a/tests/iso.rs b/tests/iso.rs index 5880e5f20..f808830e4 100644 --- a/tests/iso.rs +++ b/tests/iso.rs @@ -1,5 +1,6 @@ extern crate petgraph; +use std::collections::HashSet; use std::fs::File; use std::io::prelude::*; @@ -7,7 +8,9 @@ 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 /// @@ -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![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