diff --git a/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml b/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml new file mode 100644 index 000000000..01c9f4627 --- /dev/null +++ b/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Graph isomorphism using VF2(++) is now supported in ``rustworkx-core`` + for ``petgraph`` graph types ``StableGraph`` and ``Graph`` (it may also + work for ``GraphMap``, but this is not verified). + To use it, import ``rustworkx_core::isomorphism::vf2`` which provides + function :func:`.is_isomorphic` for isomorphism testing in addition to the + ``Vf2Algorithm`` struct, which can be used to iterate over valid + isomorphic matches of two graphs. It also provides traits ``EdgeMatcher`` + and ``NodeMatcher``, which allow a user-provided implementation of + node and edge equality. diff --git a/rustworkx-core/src/isomorphism/mod.rs b/rustworkx-core/src/isomorphism/mod.rs new file mode 100644 index 000000000..25d5073ce --- /dev/null +++ b/rustworkx-core/src/isomorphism/mod.rs @@ -0,0 +1,15 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Module for graph isomorphism. + +pub mod vf2; diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs new file mode 100644 index 000000000..ea4cb05e8 --- /dev/null +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -0,0 +1,1337 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +#![allow(clippy::too_many_arguments)] +// This module was originally forked from petgraph's isomorphism module @ v0.5.0 +// to handle PyDiGraph inputs instead of petgraph's generic Graph. However it has +// since diverged significantly from the original petgraph implementation. + +use std::cmp::{Ordering, Reverse}; +use std::convert::Infallible; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::iter::Iterator; +use std::marker; +use std::ops::Deref; + +use crate::dictmap::*; +use hashbrown::HashMap; + +use petgraph::data::{Build, Create, DataMap}; +use petgraph::stable_graph::NodeIndex; +use petgraph::visit::{ + Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, + IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, + NodeIndexable, +}; +use petgraph::{Directed, Incoming, Outgoing}; + +use rayon::slice::ParallelSliceMut; + +/// Returns `true` if we can map every element of `xs` to a unique +/// element of `ys` while using `matcher` func to compare two elements. +fn is_subset(xs: &[T1], ys: &[T2], matcher: &mut F) -> Result +where + F: FnMut(T1, T2) -> Result, +{ + let mut valid = vec![true; ys.len()]; + for &a in xs { + let mut found = false; + for (&b, free) in ys.iter().zip(valid.iter_mut()) { + if *free && matcher(a, b)? { + found = true; + *free = false; + break; + } + } + + if !found { + return Ok(false); + } + } + + Ok(true) +} + +#[inline] +fn sorted(x: &mut (N, N)) { + let (a, b) = x; + if b < a { + std::mem::swap(a, b) + } +} + +/// Returns the adjacency matrix of a graph as a dictionary +/// with `(i, j)` entry equal to number of edges from node `i` to node `j`. +fn adjacency_matrix(graph: G) -> HashMap<(NodeIndex, NodeIndex), usize> +where + G: GraphProp + GraphBase + EdgeCount + IntoEdgeReferences, +{ + let mut matrix = HashMap::with_capacity(graph.edge_count()); + for edge in graph.edge_references() { + let mut item = (edge.source(), edge.target()); + if !graph.is_directed() { + sorted(&mut item); + } + let entry = matrix.entry(item).or_insert(0); + *entry += 1; + } + matrix +} + +/// Returns the number of edges from node `a` to node `b`. +fn edge_multiplicity( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, +) -> usize +where + G: GraphProp + GraphBase, +{ + let mut item = (a, b); + if !graph.is_directed() { + sorted(&mut item); + } + *matrix.get(&item).unwrap_or(&0) +} + +/// Nodes `a`, `b` are adjacent if the number of edges +/// from node `a` to node `b` is greater than `val`. +fn is_adjacent( + graph: &G, + matrix: &HashMap<(NodeIndex, NodeIndex), usize>, + a: NodeIndex, + b: NodeIndex, + val: usize, +) -> bool +where + G: GraphProp + GraphBase, +{ + edge_multiplicity(graph, matrix, a, b) >= val +} + +trait NodeSorter +where + G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, +{ + type OutputGraph: GraphBase + + Create + + Data; + + fn sort(&self, _: G) -> Vec; + + fn reorder(&self, graph: G) -> (Self::OutputGraph, HashMap) { + let order = self.sort(graph); + + let mut new_graph = + Self::OutputGraph::with_capacity(graph.node_count(), graph.edge_count()); + let mut id_map: HashMap = HashMap::with_capacity(graph.node_count()); + for node_index in order { + let node_data = graph.node_weight(node_index).unwrap(); + let new_index = new_graph.add_node(node_data.clone()); + id_map.insert(node_index, new_index); + } + for edge in graph.edge_references() { + let edge_w = edge.weight(); + let p_index = id_map[&edge.source()]; + let c_index = id_map[&edge.target()]; + new_graph.add_edge(p_index, c_index, edge_w.clone()); + } + ( + new_graph, + id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(), + ) + } +} + +/// Sort nodes based on node ids. +struct DefaultIdSorter {} + +impl DefaultIdSorter { + pub fn new() -> Self { + Self {} + } +} + +impl NodeSorter for DefaultIdSorter +where + G: Deref + + GraphBase + + DataMap + + NodeCount + + EdgeCount + + IntoEdgeReferences + + IntoNodeIdentifiers, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, +{ + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + graph.node_identifiers().collect() + } +} + +/// Sort nodes based on VF2++ heuristic. +struct Vf2ppSorter {} + +impl Vf2ppSorter { + pub fn new() -> Self { + Self {} + } +} + +impl NodeSorter for Vf2ppSorter +where + G: Deref + + GraphProp + + GraphBase + + DataMap + + NodeCount + + NodeIndexable + + EdgeCount + + IntoNodeIdentifiers + + IntoEdgesDirected, + G::Target: GraphBase + + Data + + Create, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, +{ + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { + let n = graph.node_bound(); + + let dout: Vec = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Outgoing) + .count() + }) + .collect(); + + let mut din: Vec = vec![0; n]; + if graph.is_directed() { + din = (0..n) + .map(|idx| { + graph + .neighbors_directed(graph.from_index(idx), Incoming) + .count() + }) + .collect(); + } + + let mut conn_in: Vec = vec![0; n]; + let mut conn_out: Vec = vec![0; n]; + + let mut order: Vec = Vec::with_capacity(n); + + // Process BFS level + let mut process = |mut vd: Vec| -> Vec { + // repeatedly bring largest element in front. + for i in 0..vd.len() { + let (index, &item) = vd[i..] + .iter() + .enumerate() + .max_by_key(|&(_, &node)| { + ( + conn_in[node], + dout[node], + conn_out[node], + din[node], + Reverse(node), + ) + }) + .unwrap(); + + vd.swap(i, i + index); + order.push(NodeIndex::new(item)); + + for neigh in graph.neighbors_directed(graph.from_index(item), Outgoing) { + conn_in[graph.to_index(neigh)] += 1; + } + + if graph.is_directed() { + for neigh in graph.neighbors_directed(graph.from_index(item), Incoming) { + conn_out[graph.to_index(neigh)] += 1; + } + } + } + vd + }; + + let mut seen: Vec = vec![false; n]; + + // Create BFS Tree from root and process each level. + let mut bfs_tree = |root: usize| { + if seen[root] { + return; + } + + let mut next_level: Vec = Vec::new(); + + seen[root] = true; + next_level.push(root); + while !next_level.is_empty() { + let this_level = next_level; + let this_level = process(this_level); + + next_level = Vec::new(); + for bfs_node in this_level { + for neighbor in graph.neighbors_directed(graph.from_index(bfs_node), Outgoing) { + let neigh = graph.to_index(neighbor); + if !seen[neigh] { + seen[neigh] = true; + next_level.push(neigh); + } + } + } + } + }; + + let mut sorted_nodes: Vec = + graph.node_identifiers().map(|node| node.index()).collect(); + sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node], Reverse(node))); + sorted_nodes.reverse(); + + for node in sorted_nodes { + bfs_tree(node); + } + + order + } +} + +#[derive(Debug)] +pub struct Vf2State { + pub graph: G, + /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, + /// NodeIndex::end() for no mapping. + mapping: Vec, + /// out[i] is non-zero if i is in either M_0(s) or Tout_0(s) + /// These are all the next vertices that are not mapped yet, but + /// have an outgoing edge from the mapping. + out: Vec, + /// ins[i] is non-zero if i is in either M_0(s) or Tin_0(s) + /// These are all the incoming vertices, those not mapped yet, but + /// have an edge from them into the mapping. + /// Unused if graph is undirected -- it's identical with out in that case. + ins: Vec, + out_size: usize, + ins_size: usize, + adjacency_matrix: HashMap<(NodeIndex, NodeIndex), usize>, + generation: usize, + _etype: marker::PhantomData, +} + +impl Vf2State +where + G: GraphBase + GraphProp + NodeCount + EdgeCount, + for<'a> &'a G: + GraphBase + GraphProp + NodeCount + EdgeCount + IntoEdgesDirected, +{ + pub fn new(graph: G) -> Self { + let c0 = graph.node_count(); + let is_directed = graph.is_directed(); + let adjacency_matrix = adjacency_matrix(&graph); + Vf2State { + graph, + mapping: vec![NodeIndex::end(); c0], + out: vec![0; c0], + ins: vec![0; c0 * (is_directed as usize)], + out_size: 0, + ins_size: 0, + adjacency_matrix, + generation: 0, + _etype: marker::PhantomData, + } + } + + /// Return **true** if we have a complete mapping + pub fn is_complete(&self) -> bool { + self.generation == self.mapping.len() + } + + /// Add mapping **from** <-> **to** to the state. + pub fn push_mapping(&mut self, from: NodeIndex, to: NodeIndex) { + self.generation += 1; + let s = self.generation; + self.mapping[from.index()] = to; + // update T0 & T1 ins/outs + // T0out: Node in G0 not in M0 but successor of a node in M0. + // st.out[0]: Node either in M0 or successor of M0 + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == 0 { + self.out[ix.index()] = s; + self.out_size += 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == 0 { + self.ins[ix.index()] = s; + self.ins_size += 1; + } + } + } + } + + /// Restore the state to before the last added mapping + pub fn pop_mapping(&mut self, from: NodeIndex) { + let s = self.generation; + self.generation -= 1; + + // undo (n, m) mapping + self.mapping[from.index()] = NodeIndex::end(); + + // unmark in ins and outs + for ix in self.graph.neighbors(from) { + if self.out[ix.index()] == s { + self.out[ix.index()] = 0; + self.out_size -= 1; + } + } + if self.graph.is_directed() { + for ix in self.graph.neighbors_directed(from, Incoming) { + if self.ins[ix.index()] == s { + self.ins[ix.index()] = 0; + self.ins_size -= 1; + } + } + } + } + + /// Find the next (least) node in the Tout set. + pub fn next_out_index(&self, from_index: usize) -> Option { + self.out[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the Tin set. + pub fn next_in_index(&self, from_index: usize) -> Option { + self.ins[from_index..] + .iter() + .enumerate() + .find(move |&(index, elt)| { + *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() + }) + .map(|(index, _)| index) + } + + /// Find the next (least) node in the N - M set. + pub fn next_rest_index(&self, from_index: usize) -> Option { + self.mapping[from_index..] + .iter() + .enumerate() + .find(|&(_, elt)| *elt == NodeIndex::end()) + .map(|(index, _)| index) + } +} + +#[derive(Debug)] +pub enum IsIsomorphicError { + NodeMatcherErr(NME), + EdgeMatcherErr(EME), +} + +impl Display for IsIsomorphicError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + IsIsomorphicError::NodeMatcherErr(e) => { + write!(f, "Node match callback failed with: {}", e) + } + IsIsomorphicError::EdgeMatcherErr(e) => { + write!(f, "Edge match callback failed with: {}", e) + } + } + } +} + +impl Error for IsIsomorphicError {} + +pub struct NoSemanticMatch; + +pub trait NodeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result; +} + +impl NodeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result { + Ok(true) + } +} + +impl NodeMatcher for F +where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> Result, +{ + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + n0: G0::NodeId, + n1: G1::NodeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.node_weight(n0), g1.node_weight(n1)) { + self(x, y) + } else { + Ok(false) + } + } +} + +pub trait EdgeMatcher { + type Error; + fn enabled(&self) -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result; +} + +impl EdgeMatcher for NoSemanticMatch { + type Error = Infallible; + #[inline] + fn enabled(&self) -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _e0: G0::EdgeId, + _e1: G1::EdgeId, + ) -> Result { + Ok(true) + } +} + +impl EdgeMatcher for F +where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, +{ + type Error = E; + #[inline] + fn enabled(&self) -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + e0: G0::EdgeId, + e1: G1::EdgeId, + ) -> Result { + if let (Some(x), Some(y)) = (g0.edge_weight(e0), g1.edge_weight(e1)) { + self(x, y) + } else { + Ok(false) + } + } +} + +/// [Graph] Return `true` if the graphs `g0` and `g1` are (sub) graph isomorphic. +/// +/// Using the VF2 algorithm, examining both syntactic and semantic +/// graph isomorphism (graph structure and matching node and edge weights). +/// +/// The graphs should not be multigraphs. +pub fn is_isomorphic( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, +) -> Result> +where + G0: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, +{ + if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) + || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) + { + return Ok(false); + } + + let mut vf2 = Vf2Algorithm::new( + g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, + ); + + match vf2.next() { + Some(Ok(_)) => Ok(true), + Some(Err(e)) => Err(e), + None => Ok(false), + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +enum OpenList { + Out, + In, + Other, +} + +#[derive(Clone, PartialEq, Debug)] +enum Frame { + Outer, + Inner { nodes: [N; 2], open_list: OpenList }, + Unwind { nodes: [N; 2], open_list: OpenList }, +} + +/// An iterator which uses the VF2(++) algorithm to produce isomorphic matches +/// between two graphs, examining both syntactic and semantic graph isomorphism +/// (graph structure and matching node and edge weights). +/// +/// The graphs should not be multigraphs. +pub struct Vf2Algorithm +where + G0: GraphBase + Data, + G1: GraphBase + Data, + NM: NodeMatcher, + EM: EdgeMatcher, +{ + pub st: (Vf2State, Vf2State), + pub node_match: NM, + pub edge_match: EM, + ordering: Ordering, + induced: bool, + node_map_g0: HashMap, + node_map_g1: HashMap, + stack: Vec>, + call_limit: Option, + _counter: usize, +} + +impl Vf2Algorithm +where + G0: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, +{ + pub fn new( + g0: &G0, + g1: &G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Self { + let (g0, node_map_g0) = if id_order { + DefaultIdSorter::new().reorder(g0) + } else { + Vf2ppSorter::new().reorder(g0) + }; + + let (g1, node_map_g1) = if id_order { + DefaultIdSorter::new().reorder(g1) + } else { + Vf2ppSorter::new().reorder(g1) + }; + + let st = (Vf2State::new(g0), Vf2State::new(g1)); + Vf2Algorithm { + st, + node_match, + edge_match, + ordering, + induced, + node_map_g0, + node_map_g1, + stack: vec![Frame::Outer], + call_limit, + _counter: 0, + } + } + + fn mapping(&self) -> DictMap { + let mut mapping: DictMap = DictMap::new(); + self.st + .1 + .mapping + .iter() + .enumerate() + .for_each(|(index, val)| { + mapping.insert(self.node_map_g0[&val.index()], self.node_map_g1[&index]); + }); + + mapping + } + + fn next_candidate( + st: &mut (Vf2State, Vf2State), + ) -> Option<(NodeIndex, NodeIndex, OpenList)> { + // Try the out list + let mut to_index = st.1.next_out_index(0); + let mut from_index = None; + let mut open_list = OpenList::Out; + + if to_index.is_some() { + from_index = st.0.next_out_index(0); + open_list = OpenList::Out; + } + // Try the in list + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_in_index(0); + + if to_index.is_some() { + from_index = st.0.next_in_index(0); + open_list = OpenList::In; + } + } + // Try the other list -- disconnected graph + if to_index.is_none() || from_index.is_none() { + to_index = st.1.next_rest_index(0); + if to_index.is_some() { + from_index = st.0.next_rest_index(0); + open_list = OpenList::Other; + } + } + match (from_index, to_index) { + (Some(n), Some(m)) => Some((NodeIndex::new(n), NodeIndex::new(m), open_list)), + // No more candidates + _ => None, + } + } + + fn next_from_ix( + st: &mut (Vf2State, Vf2State), + nx: NodeIndex, + open_list: OpenList, + ) -> Option { + // Find the next node index to try on the `from` side of the mapping + let start = nx.index() + 1; + let cand0 = match open_list { + OpenList::Out => st.0.next_out_index(start), + OpenList::In => st.0.next_in_index(start), + OpenList::Other => st.0.next_rest_index(start), + } + .map(|c| c + start); // compensate for start offset. + match cand0 { + None => None, // no more candidates + Some(ix) => { + debug_assert!(ix >= start); + Some(NodeIndex::new(ix)) + } + } + } + + fn pop_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Restore state. + st.0.pop_mapping(nodes[0]); + st.1.pop_mapping(nodes[1]); + } + + fn push_state(st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2]) { + // Add mapping nx <-> mx to the state + st.0.push_mapping(nodes[0], nodes[1]); + st.1.push_mapping(nodes[1], nodes[0]); + } + + fn is_feasible( + st: &mut (Vf2State, Vf2State), + nodes: [NodeIndex; 2], + node_match: &mut NM, + edge_match: &mut EM, + ordering: Ordering, + induced: bool, + ) -> Result> { + // Check syntactic feasibility of mapping by ensuring adjacencies + // of nx map to adjacencies of mx. + // + // nx == map to => mx + // + // R_succ + // + // Check that every neighbor of nx is mapped to a neighbor of mx, + // then check the reverse, from mx to nx. Check that they have the same + // count of edges. + // + // Note: We want to check the lookahead measures here if we can, + // R_out: Equal for G0, G1: Card(Succ(G, n) ^ Tout); for both Succ and Pred + // R_in: Same with Tin + // R_new: Equal for G0, G1: Ñ n Pred(G, n); both Succ and Pred, + // Ñ is G0 - M - Tin - Tout + let end = NodeIndex::end(); + let mut succ_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + succ_count[0] += 1; + if !induced { + continue; + } + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + continue; + } + let val = edge_multiplicity(&st.0.graph, &st.0.adjacency_matrix, nodes[0], n_neigh); + + let has_edge = is_adjacent(&st.1.graph, &st.1.adjacency_matrix, nodes[1], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors(nodes[1]) { + succ_count[1] += 1; + // handle the self loop case; it's not in the mapping (yet) + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + continue; + } + let val = edge_multiplicity(&st.1.graph, &st.1.adjacency_matrix, nodes[1], n_neigh); + + let has_edge = is_adjacent(&st.0.graph, &st.0.adjacency_matrix, nodes[0], m_neigh, val); + if !has_edge { + return Ok(false); + } + } + if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { + return Ok(false); + } + // R_pred + if st.0.graph.is_directed() { + let mut pred_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + pred_count[0] += 1; + if !induced { + continue; + } + // the self loop case is handled in outgoing + let m_neigh = st.0.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity(&st.0.graph, &st.0.adjacency_matrix, n_neigh, nodes[0]); + + let has_edge = + is_adjacent(&st.1.graph, &st.1.adjacency_matrix, m_neigh, nodes[1], val); + if !has_edge { + return Ok(false); + } + } + + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + pred_count[1] += 1; + // the self loop case is handled in outgoing + let m_neigh = st.1.mapping[n_neigh.index()]; + if m_neigh == end { + continue; + } + let val = edge_multiplicity(&st.1.graph, &st.1.adjacency_matrix, n_neigh, nodes[1]); + + let has_edge = + is_adjacent(&st.0.graph, &st.0.adjacency_matrix, m_neigh, nodes[0], val); + if !has_edge { + return Ok(false); + } + } + if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + macro_rules! field { + ($x:ident, 0) => { + $x.0 + }; + ($x:ident, 1) => { + $x.1 + }; + ($x:ident, 1 - 0) => { + $x.1 + }; + ($x:ident, 1 - 1) => { + $x.0 + }; + } + macro_rules! rule { + ($arr:ident, $j:tt, $dir:expr) => {{ + let mut count = 0; + for n_neigh in field!(st, $j).graph.neighbors_directed(nodes[$j], $dir) { + let index = n_neigh.index(); + if field!(st, $j).$arr[index] > 0 && st.$j.mapping[index] == end { + count += 1; + } + } + count + }}; + } + // R_out + if rule!(out, 0, Outgoing) + .cmp(&rule!(out, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + if st.0.graph.is_directed() + && rule!(out, 0, Incoming) + .cmp(&rule!(out, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + // R_in + if st.0.graph.is_directed() { + if rule!(ins, 0, Outgoing) + .cmp(&rule!(ins, 1, Outgoing)) + .then(ordering) + != ordering + { + return Ok(false); + } + + if rule!(ins, 0, Incoming) + .cmp(&rule!(ins, 1, Incoming)) + .then(ordering) + != ordering + { + return Ok(false); + } + } + // R_new + if induced { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors(nodes[0]) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && (st.0.ins.is_empty() || st.0.ins[index] == 0) { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors(nodes[1]) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && (st.1.ins.is_empty() || st.1.ins[index] == 0) { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + if st.0.graph.is_directed() { + let mut new_count = [0, 0]; + for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { + let index = n_neigh.index(); + if st.0.out[index] == 0 && st.0.ins[index] == 0 { + new_count[0] += 1; + } + } + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + let index = n_neigh.index(); + if st.1.out[index] == 0 && st.1.ins[index] == 0 { + new_count[1] += 1; + } + } + if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { + return Ok(false); + } + } + } + // semantic feasibility: compare associated data for nodes + if node_match.enabled() + && !node_match + .eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) + .map_err(IsIsomorphicError::NodeMatcherErr)? + { + return Ok(false); + } + // semantic feasibility: compare associated data for edges + if edge_match.enabled() { + let mut matcher = + |g0_edge: (NodeIndex, G0::EdgeId), + g1_edge: (NodeIndex, G1::EdgeId)| + -> Result> { + let (nx, e0) = g0_edge; + let (mx, e1) = g1_edge; + if nx == mx + && edge_match + .eq(&st.0.graph, &st.1.graph, e0, e1) + .map_err(IsIsomorphicError::EdgeMatcherErr)? + { + return Ok(true); + } + Ok(false) + }; + + // Used to reverse the order of edge args to the matcher + // when checking G1 subset of G0. + #[inline] + fn reverse_args(mut f: F) -> impl FnMut(T2, T1) -> R + where + F: FnMut(T1, T2) -> R, + { + move |y, x| f(x, y) + } + + // outgoing edges + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges(nodes[1]) + .filter_map(|edge| { + let n_neigh = edge.target(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges(nodes[0]) + .map(|edge| (edge.target(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + + // incoming edges + if st.0.graph.is_directed() { + if induced { + let e_first: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[0] != n_neigh { + st.0.mapping[n_neigh.index()] + } else { + nodes[1] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); + }; + } + + let e_first: Vec<(NodeIndex, G1::EdgeId)> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .filter_map(|edge| { + let n_neigh = edge.source(); + let m_neigh = if nodes[1] != n_neigh { + st.1.mapping[n_neigh.index()] + } else { + nodes[0] + }; + if m_neigh == end { + return None; + } + Some((m_neigh, edge.id())) + }) + .collect(); + + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), edge.id())) + .collect(); + + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; + } + } + Ok(true) + } +} + +impl Iterator for Vf2Algorithm +where + G0: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G0: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, + NM: NodeMatcher, + EM: EdgeMatcher, +{ + type Item = Result, IsIsomorphicError>; + + /// Return Some(mapping) if isomorphism is decided, else None. + fn next(&mut self) -> Option { + if (self + .st + .0 + .graph + .node_count() + .cmp(&self.st.1.graph.node_count()) + .then(self.ordering) + != self.ordering) + || (self + .st + .0 + .graph + .edge_count() + .cmp(&self.st.1.graph.edge_count()) + .then(self.ordering) + != self.ordering) + { + return None; + } + + // 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]) + while let Some(frame) = self.stack.pop() { + match frame { + Frame::Unwind { + nodes, + open_list: ol, + } => { + Vf2Algorithm::::pop_state(&mut self.st, nodes); + + match Vf2Algorithm::::next_from_ix(&mut self.st, nodes[0], ol) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Outer => { + match Vf2Algorithm::::next_candidate(&mut self.st) { + None => { + if self.st.1.is_complete() { + return Some(Ok(self.mapping())); + } + continue; + } + Some((nx, mx, ol)) => { + let f = Frame::Inner { + nodes: [nx, mx], + open_list: ol, + }; + self.stack.push(f); + } + } + } + Frame::Inner { + nodes, + open_list: ol, + } => { + let feasible = match Vf2Algorithm::::is_feasible( + &mut self.st, + nodes, + &mut self.node_match, + &mut self.edge_match, + self.ordering, + self.induced, + ) { + Ok(f) => f, + Err(e) => { + return Some(Err(e)); + } + }; + + if feasible { + Vf2Algorithm::::push_state(&mut self.st, nodes); + // Check cardinalities of Tin, Tout sets + if self + .st + .0 + .out_size + .cmp(&self.st.1.out_size) + .then(self.ordering) + == self.ordering + && self + .st + .0 + .ins_size + .cmp(&self.st.1.ins_size) + .then(self.ordering) + == self.ordering + { + self._counter += 1; + if let Some(limit) = self.call_limit { + if self._counter > limit { + return None; + } + } + let f0 = Frame::Unwind { + nodes, + open_list: ol, + }; + + self.stack.push(f0); + self.stack.push(Frame::Outer); + continue; + } + Vf2Algorithm::::pop_state(&mut self.st, nodes); + } + match Vf2Algorithm::::next_from_ix(&mut self.st, nodes[0], ol) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } + } + } + } + } + None + } +} diff --git a/rustworkx-core/src/lib.rs b/rustworkx-core/src/lib.rs index fc5d6f5df..47f67c313 100644 --- a/rustworkx-core/src/lib.rs +++ b/rustworkx-core/src/lib.rs @@ -102,6 +102,7 @@ pub mod connectivity; pub mod dag_algo; pub mod generators; pub mod graph_ext; +pub mod isomorphism; pub mod line_graph; /// Module for maximum weight matching algorithms. pub mod max_weight_matching; diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 6e48f1f10..4576154e7 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -12,15 +12,117 @@ #![allow(clippy::too_many_arguments)] -mod vf2; +mod vf2_mapping; use crate::{digraph, graph}; +use petgraph::data::{Create, DataMap}; +use petgraph::prelude::*; +use petgraph::visit::{ + Data, EdgeCount, GraphBase, GraphProp, IntoEdgesDirected, IntoNodeIdentifiers, NodeCount, + NodeIndexable, +}; +use rustworkx_core::isomorphism::vf2; + use std::cmp::Ordering; use pyo3::prelude::*; use pyo3::Python; +fn is_isomorphic( + py: Python, + first: &G, + second: &G, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, +) -> PyResult +where + G: GraphProp + + GraphBase + + Data + + DataMap + + Create + + NodeCount + + EdgeCount, + for<'a> &'a G: GraphBase + + Data + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, +{ + fn semantic_matcher( + py: Python, + matcher: PyObject, + ) -> impl FnMut(&PyObject, &PyObject) -> PyResult + '_ { + move |w1: &PyObject, w2: &PyObject| -> PyResult { + let res = matcher.call1(py, (w1, w2))?; + res.is_truthy(py) + } + } + let node_matcher = node_matcher.map(|nm| semantic_matcher(py, nm)); + let edge_matcher = edge_matcher.map(|em| semantic_matcher(py, em)); + let result = match (node_matcher, edge_matcher) { + (Some(node_matcher), Some(edge_matcher)) => vf2::is_isomorphic( + first, + second, + node_matcher, + edge_matcher, + id_order, + ordering, + induced, + call_limit, + ) + .map_err(|e| match e { + vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + vf2::IsIsomorphicError::EdgeMatcherErr(e) => e, + })?, + (Some(node_matcher), None) => vf2::is_isomorphic( + first, + second, + node_matcher, + vf2::NoSemanticMatch, + id_order, + ordering, + induced, + call_limit, + ) + .map_err(|e| match e { + vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + _ => unreachable!(), + })?, + (None, Some(edge_matcher)) => vf2::is_isomorphic( + first, + second, + vf2::NoSemanticMatch, + edge_matcher, + id_order, + ordering, + induced, + call_limit, + ) + .map_err(|e| match e { + vf2::IsIsomorphicError::EdgeMatcherErr(e) => e, + _ => unreachable!(), + })?, + (None, None) => vf2::is_isomorphic( + first, + second, + vf2::NoSemanticMatch, + vf2::NoSemanticMatch, + id_order, + ordering, + induced, + call_limit, + ) + .unwrap(), + }; + Ok(result) +} + /// Determine if 2 directed graphs are isomorphic /// /// This checks if 2 graphs are isomorphic both structurally and also @@ -72,7 +174,7 @@ pub fn digraph_is_isomorphic( id_order: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( + is_isomorphic( py, &first.graph, &second.graph, @@ -136,7 +238,7 @@ pub fn graph_is_isomorphic( id_order: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( + is_isomorphic( py, &first.graph, &second.graph, @@ -208,7 +310,7 @@ pub fn digraph_is_subgraph_isomorphic( induced: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( + is_isomorphic( py, &first.graph, &second.graph, @@ -280,7 +382,7 @@ pub fn graph_is_subgraph_isomorphic( induced: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( + is_isomorphic( py, &first.graph, &second.graph, @@ -352,14 +454,14 @@ pub fn digraph_vf2_mapping( subgraph: bool, induced: bool, call_limit: Option, -) -> vf2::DiGraphVf2Mapping { +) -> vf2_mapping::DiGraphVf2Mapping { let ordering = if subgraph { Ordering::Greater } else { Ordering::Equal }; - vf2::DiGraphVf2Mapping::new( + vf2_mapping::DiGraphVf2Mapping::new( py, &first.graph, &second.graph, @@ -430,14 +532,14 @@ pub fn graph_vf2_mapping( subgraph: bool, induced: bool, call_limit: Option, -) -> vf2::GraphVf2Mapping { +) -> vf2_mapping::GraphVf2Mapping { let ordering = if subgraph { Ordering::Greater } else { Ordering::Equal }; - vf2::GraphVf2Mapping::new( + vf2_mapping::GraphVf2Mapping::new( py, &first.graph, &second.graph, diff --git a/src/isomorphism/vf2.rs b/src/isomorphism/vf2.rs deleted file mode 100644 index f3e77c7c1..000000000 --- a/src/isomorphism/vf2.rs +++ /dev/null @@ -1,1045 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -#![allow(clippy::too_many_arguments)] -// This module was originally forked from petgraph's isomorphism module @ v0.5.0 -// to handle PyDiGraph inputs instead of petgraph's generic Graph. However it has -// since diverged significantly from the original petgraph implementation. - -use std::cmp::{Ordering, Reverse}; -use std::iter::Iterator; -use std::marker; - -use hashbrown::HashMap; -use rustworkx_core::dictmap::*; - -use pyo3::gc::PyVisit; -use pyo3::prelude::*; -use pyo3::PyTraverseError; - -use petgraph::stable_graph::NodeIndex; -use petgraph::visit::{EdgeRef, IntoEdgeReferences, NodeIndexable}; -use petgraph::EdgeType; -use petgraph::{Directed, Incoming, Outgoing, Undirected}; - -use rayon::slice::ParallelSliceMut; - -use crate::iterators::NodeMap; -use crate::StablePyGraph; - -/// Returns `true` if we can map every element of `xs` to a unique -/// element of `ys` while using `matcher` func to compare two elements. -fn is_subset(xs: &[T], ys: &[T], matcher: F) -> PyResult -where - F: Fn(T, T) -> PyResult, -{ - let mut valid = vec![true; ys.len()]; - for &a in xs { - let mut found = false; - for (&b, free) in ys.iter().zip(valid.iter_mut()) { - if *free && matcher(a, b)? { - found = true; - *free = false; - break; - } - } - - if !found { - return Ok(false); - } - } - - Ok(true) -} - -#[inline] -fn sorted(x: &mut (N, N)) { - let (a, b) = x; - if b < a { - std::mem::swap(a, b) - } -} - -/// Returns the adjacency matrix of a graph as a dictionary -/// with `(i, j)` entry equal to number of edges from node `i` to node `j`. -fn adjacency_matrix( - graph: &StablePyGraph, -) -> HashMap<(NodeIndex, NodeIndex), usize> { - let mut matrix = HashMap::with_capacity(graph.edge_count()); - for edge in graph.edge_references() { - let mut item = (edge.source(), edge.target()); - if !graph.is_directed() { - sorted(&mut item); - } - let entry = matrix.entry(item).or_insert(0); - *entry += 1; - } - matrix -} - -/// Returns the number of edges from node `a` to node `b`. -fn edge_multiplicity( - graph: &StablePyGraph, - matrix: &HashMap<(NodeIndex, NodeIndex), usize>, - a: NodeIndex, - b: NodeIndex, -) -> usize { - let mut item = (a, b); - if !graph.is_directed() { - sorted(&mut item); - } - *matrix.get(&item).unwrap_or(&0) -} - -/// Nodes `a`, `b` are adjacent if the number of edges -/// from node `a` to node `b` is greater than `val`. -fn is_adjacent( - graph: &StablePyGraph, - matrix: &HashMap<(NodeIndex, NodeIndex), usize>, - a: NodeIndex, - b: NodeIndex, - val: usize, -) -> bool { - edge_multiplicity(graph, matrix, a, b) >= val -} - -trait NodeSorter -where - Ty: EdgeType, -{ - fn sort(&self, _: &StablePyGraph) -> Vec; - - fn reorder( - &self, - py: Python, - graph: &StablePyGraph, - ) -> (StablePyGraph, HashMap) { - let order = self.sort(graph); - - let mut new_graph = - StablePyGraph::::with_capacity(graph.node_count(), graph.edge_count()); - let mut id_map: HashMap = HashMap::with_capacity(graph.node_count()); - for node_index in order { - let node_data = graph.node_weight(node_index).unwrap(); - let new_index = new_graph.add_node(node_data.clone_ref(py)); - id_map.insert(node_index, new_index); - } - for edge in graph.edge_references() { - let edge_w = edge.weight(); - let p_index = id_map[&edge.source()]; - let c_index = id_map[&edge.target()]; - new_graph.add_edge(p_index, c_index, edge_w.clone_ref(py)); - } - ( - new_graph, - id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(), - ) - } -} - -/// Sort nodes based on node ids. -struct DefaultIdSorter; - -impl NodeSorter for DefaultIdSorter -where - Ty: EdgeType, -{ - fn sort(&self, graph: &StablePyGraph) -> Vec { - graph.node_indices().collect() - } -} - -/// Sort nodes based on VF2++ heuristic. -struct Vf2ppSorter; - -impl NodeSorter for Vf2ppSorter -where - Ty: EdgeType, -{ - fn sort(&self, graph: &StablePyGraph) -> Vec { - let n = graph.node_bound(); - - let dout: Vec = (0..n) - .map(|idx| { - graph - .neighbors_directed(graph.from_index(idx), Outgoing) - .count() - }) - .collect(); - - let mut din: Vec = vec![0; n]; - if graph.is_directed() { - din = (0..n) - .map(|idx| { - graph - .neighbors_directed(graph.from_index(idx), Incoming) - .count() - }) - .collect(); - } - - let mut conn_in: Vec = vec![0; n]; - let mut conn_out: Vec = vec![0; n]; - - let mut order: Vec = Vec::with_capacity(n); - - // Process BFS level - let mut process = |mut vd: Vec| -> Vec { - // repeatedly bring largest element in front. - for i in 0..vd.len() { - let (index, &item) = vd[i..] - .iter() - .enumerate() - .max_by_key(|&(_, &node)| { - ( - conn_in[node], - dout[node], - conn_out[node], - din[node], - Reverse(node), - ) - }) - .unwrap(); - - vd.swap(i, i + index); - order.push(NodeIndex::new(item)); - - for neigh in graph.neighbors_directed(graph.from_index(item), Outgoing) { - conn_in[graph.to_index(neigh)] += 1; - } - - if graph.is_directed() { - for neigh in graph.neighbors_directed(graph.from_index(item), Incoming) { - conn_out[graph.to_index(neigh)] += 1; - } - } - } - vd - }; - - let mut seen: Vec = vec![false; n]; - - // Create BFS Tree from root and process each level. - let mut bfs_tree = |root: usize| { - if seen[root] { - return; - } - - let mut next_level: Vec = Vec::new(); - - seen[root] = true; - next_level.push(root); - while !next_level.is_empty() { - let this_level = next_level; - let this_level = process(this_level); - - next_level = Vec::new(); - for bfs_node in this_level { - for neighbor in graph.neighbors_directed(graph.from_index(bfs_node), Outgoing) { - let neigh = graph.to_index(neighbor); - if !seen[neigh] { - seen[neigh] = true; - next_level.push(neigh); - } - } - } - } - }; - - let mut sorted_nodes: Vec = graph.node_indices().map(|node| node.index()).collect(); - sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node], Reverse(node))); - sorted_nodes.reverse(); - - for node in sorted_nodes { - bfs_tree(node); - } - - order - } -} - -#[derive(Debug)] -struct Vf2State -where - Ty: EdgeType, -{ - graph: StablePyGraph, - /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, - /// NodeIndex::end() for no mapping. - mapping: Vec, - /// out[i] is non-zero if i is in either M_0(s) or Tout_0(s) - /// These are all the next vertices that are not mapped yet, but - /// have an outgoing edge from the mapping. - out: Vec, - /// ins[i] is non-zero if i is in either M_0(s) or Tin_0(s) - /// These are all the incoming vertices, those not mapped yet, but - /// have an edge from them into the mapping. - /// Unused if graph is undirected -- it's identical with out in that case. - ins: Vec, - out_size: usize, - ins_size: usize, - adjacency_matrix: HashMap<(NodeIndex, NodeIndex), usize>, - generation: usize, - _etype: marker::PhantomData, -} - -impl Vf2State -where - Ty: EdgeType, -{ - pub fn new(graph: StablePyGraph) -> Self { - let c0 = graph.node_count(); - let is_directed = graph.is_directed(); - let adjacency_matrix = adjacency_matrix(&graph); - Vf2State { - graph, - mapping: vec![NodeIndex::end(); c0], - out: vec![0; c0], - ins: vec![0; c0 * (is_directed as usize)], - out_size: 0, - ins_size: 0, - adjacency_matrix, - generation: 0, - _etype: marker::PhantomData, - } - } - - /// Return **true** if we have a complete mapping - pub fn is_complete(&self) -> bool { - self.generation == self.mapping.len() - } - - /// Add mapping **from** <-> **to** to the state. - pub fn push_mapping(&mut self, from: NodeIndex, to: NodeIndex) { - self.generation += 1; - let s = self.generation; - self.mapping[from.index()] = to; - // update T0 & T1 ins/outs - // T0out: Node in G0 not in M0 but successor of a node in M0. - // st.out[0]: Node either in M0 or successor of M0 - for ix in self.graph.neighbors(from) { - if self.out[ix.index()] == 0 { - self.out[ix.index()] = s; - self.out_size += 1; - } - } - if self.graph.is_directed() { - for ix in self.graph.neighbors_directed(from, Incoming) { - if self.ins[ix.index()] == 0 { - self.ins[ix.index()] = s; - self.ins_size += 1; - } - } - } - } - - /// Restore the state to before the last added mapping - pub fn pop_mapping(&mut self, from: NodeIndex) { - let s = self.generation; - self.generation -= 1; - - // undo (n, m) mapping - self.mapping[from.index()] = NodeIndex::end(); - - // unmark in ins and outs - for ix in self.graph.neighbors(from) { - if self.out[ix.index()] == s { - self.out[ix.index()] = 0; - self.out_size -= 1; - } - } - if self.graph.is_directed() { - for ix in self.graph.neighbors_directed(from, Incoming) { - if self.ins[ix.index()] == s { - self.ins[ix.index()] = 0; - self.ins_size -= 1; - } - } - } - } - - /// Find the next (least) node in the Tout set. - pub fn next_out_index(&self, from_index: usize) -> Option { - self.out[from_index..] - .iter() - .enumerate() - .find(move |&(index, elt)| { - *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() - }) - .map(|(index, _)| index) - } - - /// Find the next (least) node in the Tin set. - pub fn next_in_index(&self, from_index: usize) -> Option { - self.ins[from_index..] - .iter() - .enumerate() - .find(move |&(index, elt)| { - *elt > 0 && self.mapping[from_index + index] == NodeIndex::end() - }) - .map(|(index, _)| index) - } - - /// Find the next (least) node in the N - M set. - pub fn next_rest_index(&self, from_index: usize) -> Option { - self.mapping[from_index..] - .iter() - .enumerate() - .find(|&(_, elt)| *elt == NodeIndex::end()) - .map(|(index, _)| index) - } -} - -trait SemanticMatcher { - fn enabled(&self) -> bool; - fn eq(&self, _: Python, _: &T, _: &T) -> PyResult; -} - -impl SemanticMatcher for Option { - #[inline] - fn enabled(&self) -> bool { - self.is_some() - } - #[inline] - fn eq(&self, py: Python, a: &PyObject, b: &PyObject) -> PyResult { - let res = self.as_ref().unwrap().call1(py, (a, b))?; - res.is_truthy(py) - } -} - -/// [Graph] Return `true` if the graphs `g0` and `g1` are (sub) graph isomorphic. -/// -/// Using the VF2 algorithm, examining both syntactic and semantic -/// graph isomorphism (graph structure and matching node and edge weights). -/// -/// The graphs should not be multigraphs. -pub fn is_isomorphic( - py: Python, - g0: &StablePyGraph, - g1: &StablePyGraph, - node_match: Option, - edge_match: Option, - id_order: bool, - ordering: Ordering, - induced: bool, - call_limit: Option, -) -> PyResult { - if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) - || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) - { - return Ok(false); - } - - let mut vf2 = Vf2Algorithm::new( - py, g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, - ); - if vf2.next(py)?.is_some() { - return Ok(true); - } - Ok(false) -} - -#[derive(Copy, Clone, PartialEq, Debug)] -enum OpenList { - Out, - In, - Other, -} - -#[derive(Clone, PartialEq, Debug)] -enum Frame { - Outer, - Inner { nodes: [N; 2], open_list: OpenList }, - Unwind { nodes: [N; 2], open_list: OpenList }, -} - -struct Vf2Algorithm -where - Ty: EdgeType, - F: SemanticMatcher, - G: SemanticMatcher, -{ - st: [Vf2State; 2], - node_match: F, - edge_match: G, - ordering: Ordering, - induced: bool, - node_map_g0: HashMap, - node_map_g1: HashMap, - stack: Vec>, - call_limit: Option, - _counter: usize, -} - -impl Vf2Algorithm -where - Ty: EdgeType, - F: SemanticMatcher, - G: SemanticMatcher, -{ - pub fn new( - py: Python, - g0: &StablePyGraph, - g1: &StablePyGraph, - node_match: F, - edge_match: G, - id_order: bool, - ordering: Ordering, - induced: bool, - call_limit: Option, - ) -> Self { - let (g0, node_map_g0) = if id_order { - DefaultIdSorter.reorder(py, g0) - } else { - Vf2ppSorter.reorder(py, g0) - }; - - let (g1, node_map_g1) = if id_order { - DefaultIdSorter.reorder(py, g1) - } else { - Vf2ppSorter.reorder(py, g1) - }; - - let st = [Vf2State::new(g0), Vf2State::new(g1)]; - Vf2Algorithm { - st, - node_match, - edge_match, - ordering, - induced, - node_map_g0, - node_map_g1, - stack: vec![Frame::Outer], - call_limit, - _counter: 0, - } - } - - fn mapping(&self) -> NodeMap { - let mut mapping: DictMap = DictMap::new(); - self.st[1] - .mapping - .iter() - .enumerate() - .for_each(|(index, val)| { - mapping.insert(self.node_map_g0[&val.index()], self.node_map_g1[&index]); - }); - - NodeMap { node_map: mapping } - } - - fn next_candidate(st: &mut [Vf2State; 2]) -> Option<(NodeIndex, NodeIndex, OpenList)> { - // Try the out list - let mut to_index = st[1].next_out_index(0); - let mut from_index = None; - let mut open_list = OpenList::Out; - - if to_index.is_some() { - from_index = st[0].next_out_index(0); - open_list = OpenList::Out; - } - // Try the in list - if to_index.is_none() || from_index.is_none() { - to_index = st[1].next_in_index(0); - - if to_index.is_some() { - from_index = st[0].next_in_index(0); - open_list = OpenList::In; - } - } - // Try the other list -- disconnected graph - if to_index.is_none() || from_index.is_none() { - to_index = st[1].next_rest_index(0); - if to_index.is_some() { - from_index = st[0].next_rest_index(0); - open_list = OpenList::Other; - } - } - match (from_index, to_index) { - (Some(n), Some(m)) => Some((NodeIndex::new(n), NodeIndex::new(m), open_list)), - // No more candidates - _ => None, - } - } - - fn next_from_ix( - st: &mut [Vf2State; 2], - nx: NodeIndex, - open_list: OpenList, - ) -> Option { - // Find the next node index to try on the `from` side of the mapping - let start = nx.index() + 1; - let cand0 = match open_list { - OpenList::Out => st[0].next_out_index(start), - OpenList::In => st[0].next_in_index(start), - OpenList::Other => st[0].next_rest_index(start), - } - .map(|c| c + start); // compensate for start offset. - match cand0 { - None => None, // no more candidates - Some(ix) => { - debug_assert!(ix >= start); - Some(NodeIndex::new(ix)) - } - } - } - - fn pop_state(st: &mut [Vf2State; 2], nodes: [NodeIndex; 2]) { - // Restore state. - st[0].pop_mapping(nodes[0]); - st[1].pop_mapping(nodes[1]); - } - - fn push_state(st: &mut [Vf2State; 2], nodes: [NodeIndex; 2]) { - // Add mapping nx <-> mx to the state - st[0].push_mapping(nodes[0], nodes[1]); - st[1].push_mapping(nodes[1], nodes[0]); - } - - fn is_feasible( - py: Python, - st: &mut [Vf2State; 2], - nodes: [NodeIndex; 2], - node_match: &mut F, - edge_match: &mut G, - ordering: Ordering, - induced: bool, - ) -> PyResult { - // Check syntactic feasibility of mapping by ensuring adjacencies - // of nx map to adjacencies of mx. - // - // nx == map to => mx - // - // R_succ - // - // Check that every neighbor of nx is mapped to a neighbor of mx, - // then check the reverse, from mx to nx. Check that they have the same - // count of edges. - // - // Note: We want to check the lookahead measures here if we can, - // R_out: Equal for G0, G1: Card(Succ(G, n) ^ Tout); for both Succ and Pred - // R_in: Same with Tin - // R_new: Equal for G0, G1: Ñ n Pred(G, n); both Succ and Pred, - // Ñ is G0 - M - Tin - Tout - let end = NodeIndex::end(); - let mut succ_count = [0, 0]; - for j in 0..2 { - for n_neigh in st[j].graph.neighbors(nodes[j]) { - succ_count[j] += 1; - if !induced && j == 0 { - continue; - } - // handle the self loop case; it's not in the mapping (yet) - let m_neigh = if nodes[j] != n_neigh { - st[j].mapping[n_neigh.index()] - } else { - nodes[1 - j] - }; - if m_neigh == end { - continue; - } - let val = - edge_multiplicity(&st[j].graph, &st[j].adjacency_matrix, nodes[j], n_neigh); - - let has_edge = is_adjacent( - &st[1 - j].graph, - &st[1 - j].adjacency_matrix, - nodes[1 - j], - m_neigh, - val, - ); - if !has_edge { - return Ok(false); - } - } - } - if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { - return Ok(false); - } - // R_pred - if st[0].graph.is_directed() { - let mut pred_count = [0, 0]; - for j in 0..2 { - for n_neigh in st[j].graph.neighbors_directed(nodes[j], Incoming) { - pred_count[j] += 1; - if !induced && j == 0 { - continue; - } - // the self loop case is handled in outgoing - let m_neigh = st[j].mapping[n_neigh.index()]; - if m_neigh == end { - continue; - } - let val = - edge_multiplicity(&st[j].graph, &st[j].adjacency_matrix, n_neigh, nodes[j]); - - let has_edge = is_adjacent( - &st[1 - j].graph, - &st[1 - j].adjacency_matrix, - m_neigh, - nodes[1 - j], - val, - ); - if !has_edge { - return Ok(false); - } - } - } - if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { - return Ok(false); - } - } - macro_rules! rule { - ($arr:ident, $j:expr, $dir:expr) => {{ - let mut count = 0; - for n_neigh in st[$j].graph.neighbors_directed(nodes[$j], $dir) { - let index = n_neigh.index(); - if st[$j].$arr[index] > 0 && st[$j].mapping[index] == end { - count += 1; - } - } - count - }}; - } - // R_out - if rule!(out, 0, Outgoing) - .cmp(&rule!(out, 1, Outgoing)) - .then(ordering) - != ordering - { - return Ok(false); - } - if st[0].graph.is_directed() - && rule!(out, 0, Incoming) - .cmp(&rule!(out, 1, Incoming)) - .then(ordering) - != ordering - { - return Ok(false); - } - // R_in - if st[0].graph.is_directed() { - if rule!(ins, 0, Outgoing) - .cmp(&rule!(ins, 1, Outgoing)) - .then(ordering) - != ordering - { - return Ok(false); - } - - if rule!(ins, 0, Incoming) - .cmp(&rule!(ins, 1, Incoming)) - .then(ordering) - != ordering - { - return Ok(false); - } - } - // R_new - if induced { - let mut new_count = [0, 0]; - for j in 0..2 { - for n_neigh in st[j].graph.neighbors(nodes[j]) { - let index = n_neigh.index(); - if st[j].out[index] == 0 && (st[j].ins.is_empty() || st[j].ins[index] == 0) { - new_count[j] += 1; - } - } - } - if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { - return Ok(false); - } - if st[0].graph.is_directed() { - let mut new_count = [0, 0]; - for j in 0..2 { - for n_neigh in st[j].graph.neighbors_directed(nodes[j], Incoming) { - let index = n_neigh.index(); - if st[j].out[index] == 0 && st[j].ins[index] == 0 { - new_count[j] += 1; - } - } - } - if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { - return Ok(false); - } - } - } - // semantic feasibility: compare associated data for nodes - if node_match.enabled() - && !node_match.eq(py, &st[0].graph[nodes[0]], &st[1].graph[nodes[1]])? - { - return Ok(false); - } - // semantic feasibility: compare associated data for edges - if edge_match.enabled() { - let matcher = - |a: (NodeIndex, &PyObject), b: (NodeIndex, &PyObject)| -> PyResult { - let (nx, n_edge) = a; - let (mx, m_edge) = b; - if nx == mx && edge_match.eq(py, n_edge, m_edge)? { - return Ok(true); - } - Ok(false) - }; - - // outgoing edges - let range = if induced { 0..2 } else { 1..2 }; - for j in range { - let e_first: Vec<(NodeIndex, &PyObject)> = st[j] - .graph - .edges(nodes[j]) - .filter_map(|edge| { - let n_neigh = edge.target(); - let m_neigh = if nodes[j] != n_neigh { - st[j].mapping[n_neigh.index()] - } else { - nodes[1 - j] - }; - if m_neigh == end { - return None; - } - Some((m_neigh, edge.weight())) - }) - .collect(); - - let e_second: Vec<(NodeIndex, &PyObject)> = st[1 - j] - .graph - .edges(nodes[1 - j]) - .map(|edge| (edge.target(), edge.weight())) - .collect(); - - if !is_subset(&e_first, &e_second, matcher)? { - return Ok(false); - }; - } - // incoming edges - if st[0].graph.is_directed() { - let range = if induced { 0..2 } else { 1..2 }; - for j in range { - let e_first: Vec<(NodeIndex, &PyObject)> = st[j] - .graph - .edges_directed(nodes[j], Incoming) - .filter_map(|edge| { - let n_neigh = edge.source(); - let m_neigh = if nodes[j] != n_neigh { - st[j].mapping[n_neigh.index()] - } else { - nodes[1 - j] - }; - if m_neigh == end { - return None; - } - Some((m_neigh, edge.weight())) - }) - .collect(); - - let e_second: Vec<(NodeIndex, &PyObject)> = st[1 - j] - .graph - .edges_directed(nodes[1 - j], Incoming) - .map(|edge| (edge.source(), edge.weight())) - .collect(); - - if !is_subset(&e_first, &e_second, matcher)? { - return Ok(false); - }; - } - } - } - Ok(true) - } - - /// Return Some(mapping) if isomorphism is decided, else None. - fn next(&mut self, py: Python) -> PyResult> { - if (self.st[0] - .graph - .node_count() - .cmp(&self.st[1].graph.node_count()) - .then(self.ordering) - != self.ordering) - || (self.st[0] - .graph - .edge_count() - .cmp(&self.st[1].graph.edge_count()) - .then(self.ordering) - != self.ordering) - { - return Ok(None); - } - - // 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]) - while let Some(frame) = self.stack.pop() { - match frame { - Frame::Unwind { - nodes, - open_list: ol, - } => { - Vf2Algorithm::::pop_state(&mut self.st, nodes); - - match Vf2Algorithm::::next_from_ix(&mut self.st, nodes[0], ol) { - None => continue, - Some(nx) => { - let f = Frame::Inner { - nodes: [nx, nodes[1]], - open_list: ol, - }; - self.stack.push(f); - } - } - } - Frame::Outer => match Vf2Algorithm::::next_candidate(&mut self.st) { - None => { - if self.st[1].is_complete() { - return Ok(Some(self.mapping())); - } - continue; - } - Some((nx, mx, ol)) => { - let f = Frame::Inner { - nodes: [nx, mx], - open_list: ol, - }; - self.stack.push(f); - } - }, - Frame::Inner { - nodes, - open_list: ol, - } => { - if Vf2Algorithm::::is_feasible( - py, - &mut self.st, - nodes, - &mut self.node_match, - &mut self.edge_match, - self.ordering, - self.induced, - )? { - Vf2Algorithm::::push_state(&mut self.st, nodes); - // Check cardinalities of Tin, Tout sets - if self.st[0] - .out_size - .cmp(&self.st[1].out_size) - .then(self.ordering) - == self.ordering - && self.st[0] - .ins_size - .cmp(&self.st[1].ins_size) - .then(self.ordering) - == self.ordering - { - self._counter += 1; - if let Some(limit) = self.call_limit { - if self._counter > limit { - return Ok(None); - } - } - let f0 = Frame::Unwind { - nodes, - open_list: ol, - }; - - self.stack.push(f0); - self.stack.push(Frame::Outer); - continue; - } - Vf2Algorithm::::pop_state(&mut self.st, nodes); - } - match Vf2Algorithm::::next_from_ix(&mut self.st, nodes[0], ol) { - None => continue, - Some(nx) => { - let f = Frame::Inner { - nodes: [nx, nodes[1]], - open_list: ol, - }; - self.stack.push(f); - } - } - } - } - } - Ok(None) - } -} - -macro_rules! vf2_mapping_impl { - ($name:ident, $Ty:ty) => { - #[pyclass(module = "rustworkx")] - pub struct $name { - vf2: Vf2Algorithm<$Ty, Option, Option>, - } - - impl $name { - pub fn new( - py: Python, - g0: &StablePyGraph<$Ty>, - g1: &StablePyGraph<$Ty>, - node_match: Option, - edge_match: Option, - id_order: bool, - ordering: Ordering, - induced: bool, - call_limit: Option, - ) -> Self { - let vf2 = Vf2Algorithm::new( - py, g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, - ); - $name { vf2 } - } - } - - #[pymethods] - impl $name { - fn __iter__(slf: PyRef) -> Py<$name> { - slf.into() - } - - fn __next__(mut slf: PyRefMut) -> PyResult> { - Python::with_gil(|py| match slf.vf2.next(py)? { - Some(mapping) => Ok(Some(mapping)), - None => Ok(None), - }) - } - - fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { - for j in 0..2 { - for node in self.vf2.st[j].graph.node_weights() { - visit.call(node)?; - } - for edge in self.vf2.st[j].graph.edge_weights() { - visit.call(edge)?; - } - } - if let Some(ref obj) = self.vf2.node_match { - visit.call(obj)?; - } - if let Some(ref obj) = self.vf2.edge_match { - visit.call(obj)?; - } - Ok(()) - } - - fn __clear__(&mut self) { - self.vf2.st[0].graph = StablePyGraph::<$Ty>::default(); - self.vf2.st[1].graph = StablePyGraph::<$Ty>::default(); - self.vf2.node_match = None; - self.vf2.edge_match = None; - } - } - }; -} - -vf2_mapping_impl!(DiGraphVf2Mapping, Directed); -vf2_mapping_impl!(GraphVf2Mapping, Undirected); diff --git a/src/isomorphism/vf2_mapping.rs b/src/isomorphism/vf2_mapping.rs new file mode 100644 index 000000000..62b849c73 --- /dev/null +++ b/src/isomorphism/vf2_mapping.rs @@ -0,0 +1,178 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +#![allow(clippy::too_many_arguments)] +// This module was originally forked from petgraph's isomorphism module @ v0.5.0 +// to support StableGraph inputs instead of petgraph's generic Graph. However it has +// since diverged significantly from the original petgraph implementation. + +use std::cmp::Ordering; +use std::iter::Iterator; + +use pyo3::gc::PyVisit; +use pyo3::prelude::*; +use pyo3::PyTraverseError; + +use petgraph::stable_graph::{EdgeIndex, NodeIndex}; +use petgraph::EdgeType; +use petgraph::{Directed, Undirected}; + +use crate::iterators::NodeMap; +use crate::StablePyGraph; + +use rustworkx_core::isomorphism::vf2; + +struct PyMatcher(Option); + +impl vf2::NodeMatcher, StablePyGraph> for PyMatcher { + type Error = PyErr; + + fn enabled(&self) -> bool { + self.0.is_some() + } + + fn eq( + &mut self, + g0: &StablePyGraph, + g1: &StablePyGraph, + n0: NodeIndex, + n1: NodeIndex, + ) -> Result { + if let (Some(a), Some(b)) = (g0.node_weight(n0), g1.node_weight(n1)) { + unsafe { + // Note: we can assume this since we'll have the GIL whenever we're + // accessing the (Di|)GraphVF2Mapping pyclass methods. + let py = Python::assume_gil_acquired(); + let res = self.0.as_ref().unwrap().call1(py, (a, b))?; + res.is_truthy(py) + } + } else { + Ok(false) + } + } +} + +impl vf2::EdgeMatcher, StablePyGraph> for PyMatcher { + type Error = PyErr; + + fn enabled(&self) -> bool { + self.0.is_some() + } + + fn eq( + &mut self, + g0: &StablePyGraph, + g1: &StablePyGraph, + e0: EdgeIndex, + e1: EdgeIndex, + ) -> Result { + let w0 = g0.edge_weight(e0); + let w1 = g1.edge_weight(e1); + if let (Some(a), Some(b)) = (w0, w1) { + unsafe { + // Note: we can assume this since we'll have the GIL whenever we're + // accessing the (Di|)GraphVF2Mapping pyclass methods. + let py = Python::assume_gil_acquired(); + let res = self.0.as_ref().unwrap().call1(py, (a, b))?; + res.is_truthy(py) + } + } else { + Ok(false) + } + } +} + +macro_rules! vf2_mapping_impl { + ($name:ident, $Ty:ty) => { + #[pyclass(module = "rustworkx")] + pub struct $name { + vf2: vf2::Vf2Algorithm, StablePyGraph<$Ty>, PyMatcher, PyMatcher>, + } + + impl $name { + pub fn new( + _py: Python, + g0: &StablePyGraph<$Ty>, + g1: &StablePyGraph<$Ty>, + node_match: Option, + edge_match: Option, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, + ) -> Self { + let vf2 = vf2::Vf2Algorithm::new( + g0, + g1, + PyMatcher(node_match), + PyMatcher(edge_match), + id_order, + ordering, + induced, + call_limit, + ); + $name { vf2 } + } + } + + #[pymethods] + impl $name { + fn __iter__(slf: PyRef) -> Py<$name> { + slf.into() + } + + fn __next__(mut slf: PyRefMut) -> PyResult> { + Python::with_gil(|_py| match slf.vf2.next() { + Some(mapping) => Ok(Some(NodeMap { + node_map: mapping.map_err(|e| match e { + vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + vf2::IsIsomorphicError::EdgeMatcherErr(e) => e, + })?, + })), + None => Ok(None), + }) + } + + fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { + for node in self.vf2.st.0.graph.node_weights() { + visit.call(node)?; + } + for edge in self.vf2.st.0.graph.edge_weights() { + visit.call(edge)?; + } + for node in self.vf2.st.1.graph.node_weights() { + visit.call(node)?; + } + for edge in self.vf2.st.1.graph.edge_weights() { + visit.call(edge)?; + } + if let Some(ref obj) = self.vf2.node_match.0 { + visit.call(obj)?; + } + if let Some(ref obj) = self.vf2.edge_match.0 { + visit.call(obj)?; + } + Ok(()) + } + + fn __clear__(&mut self) { + self.vf2.st.0.graph = StablePyGraph::<$Ty>::default(); + self.vf2.st.0.graph = StablePyGraph::<$Ty>::default(); + self.vf2.node_match.0 = None; + self.vf2.edge_match.0 = None; + } + } + }; +} + +vf2_mapping_impl!(DiGraphVf2Mapping, Directed); +vf2_mapping_impl!(GraphVf2Mapping, Undirected);