From faeee489d2ab617f96d620fc8c177cca69421a44 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Jun 2024 00:42:25 -0400 Subject: [PATCH 01/15] Backup. --- rustworkx-core/src/isomorphism/mod.rs | 17 + rustworkx-core/src/isomorphism/vf2.rs | 1269 +++++++++++++++++++++++++ rustworkx-core/src/lib.rs | 1 + 3 files changed, 1287 insertions(+) create mode 100644 rustworkx-core/src/isomorphism/mod.rs create mode 100644 rustworkx-core/src/isomorphism/vf2.rs diff --git a/rustworkx-core/src/isomorphism/mod.rs b/rustworkx-core/src/isomorphism/mod.rs new file mode 100644 index 000000000..00d5212c7 --- /dev/null +++ b/rustworkx-core/src/isomorphism/mod.rs @@ -0,0 +1,17 @@ +// 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)] + +pub mod vf2; + +use std::cmp::Ordering; diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs new file mode 100644 index 000000000..691e1f56c --- /dev/null +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -0,0 +1,1269 @@ +// 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 std::marker::PhantomData; + +use hashbrown::HashMap; +use crate::dictmap::*; + +use petgraph::stable_graph::NodeIndex; +use petgraph::visit::{Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, NodeIndexable}; +use petgraph::EdgeType; +use petgraph::{Directed, Incoming, Outgoing, Undirected}; +use petgraph::data::{Build, Create, DataMap}; + +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: &[T], ys: &[T], matcher: F) -> bool +where + F: Fn(T, T) -> bool, +{ + 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 false; + } + } + + 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<'a, G> +where + G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, +{ + type OutputGraph: GraphBase + Create + Data; + + fn sort(&self, _: &'a G) -> Vec; + + fn reorder( + &self, + graph: &'a 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 { + _phantom: PhantomData, +} + +impl DefaultIdSorter { + pub fn new() -> Self { + Self { + _phantom: PhantomData::default(), + } + } +} + +impl<'a, G, GS> NodeSorter<'a, G> for DefaultIdSorter +where + G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + GS: GraphBase + Create + Data, +{ + type OutputGraph = GS; + fn sort(&self, graph: &'a G) -> Vec { + graph.node_identifiers().collect() + } +} + +/// Sort nodes based on VF2++ heuristic. +struct Vf2ppSorter { + _phantom: PhantomData, +} + +impl Vf2ppSorter { + pub fn new() -> Self { + Self { + _phantom: PhantomData::default(), + } + } +} + +impl<'a, G, GS> NodeSorter<'a, G> for Vf2ppSorter +where + G: GraphProp + GraphBase + DataMap + NodeCount + NodeIndexable + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers + IntoNeighborsDirected, + G::NodeWeight: Clone, + G::EdgeWeight: Clone, + GS: GraphBase + Create + Data, +{ + type OutputGraph = GS; + fn sort(&self, graph: &'a 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)] +struct Vf2State<'a, G> +{ + graph: &'a 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<'a, G> Vf2State<'a, G> +where + G: GraphProp + GraphBase + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, +{ + pub fn new(graph: &'a 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) + } +} + +pub struct NoSemanticMatch; + +pub trait NodeMatcher { + fn enabled() -> bool; + fn eq(&mut self, _g0: &G0, _g1: &G1, _n0: G0::NodeId, _n1: G1::NodeId) -> bool; +} + +impl NodeMatcher for NoSemanticMatch { + #[inline] + fn enabled() -> bool { + false + } + #[inline] + fn eq(&mut self, _g0: &G0, _g1: &G1, _n0: G0::NodeId, _n1: G1::NodeId) -> bool { + true + } +} + +impl NodeMatcher for F + where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> bool, +{ + #[inline] + fn enabled() -> bool { + true + } + #[inline] + fn eq(&mut self, g0: &G0, g1: &G1, n0: G0::NodeId, n1: G1::NodeId) -> bool { + if let (Some(x), Some(y)) = (g0.node_weight(n0), g1.node_weight(n1)) { + self(x, y) + } else { + false + } + } +} + +pub trait EdgeMatcher { + fn enabled() -> bool; + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + e0: (G0::NodeId, G0::NodeId), + e1: (G1::NodeId, G1::NodeId), + ) -> bool; +} + +impl EdgeMatcher for NoSemanticMatch { + #[inline] + fn enabled() -> bool { + false + } + #[inline] + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _e0: (G0::NodeId, G0::NodeId), + _e1: (G1::NodeId, G1::NodeId), + ) -> bool { + true + } +} + +impl EdgeMatcher for F + where + G0: GraphBase + DataMap + IntoEdgesDirected, + G1: GraphBase + DataMap + IntoEdgesDirected, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> bool, +{ + #[inline] + fn enabled() -> bool { + true + } + #[inline] + fn eq( + &mut self, + g0: &G0, + g1: &G1, + e0: (G0::NodeId, G0::NodeId), + e1: (G1::NodeId, G1::NodeId), + ) -> bool { + let w0 = g0 + .edges_directed(e0.0, Outgoing) + .find(|edge| edge.target() == e0.1) + .and_then(|edge| g0.edge_weight(edge.id())); + let w1 = g1 + .edges_directed(e1.0, Outgoing) + .find(|edge| edge.target() == e1.1) + .and_then(|edge| g1.edge_weight(edge.id())); + if let (Some(x), Some(y)) = (w0, w1) { + self(x, y) + } else { + 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<'a, G0, G1, NM, EM>( + g0: &'a G0, + g1: &'a G1, + node_match: NM, + edge_match: EM, + id_order: bool, + ordering: Ordering, + induced: bool, + call_limit: Option, +) -> bool + where + G0: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + G1: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + 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 false; + } + + let mut vf2 = Vf2Algorithm::new( + g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, + ); + if vf2.next().is_some() { + return true; + } + 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<'a, G0, G1, NM, EM> +where + G0: GraphBase, + G1: GraphBase, + NM: NodeMatcher, + EM: EdgeMatcher, +{ + st: (Vf2State<'a, G0>, Vf2State<'a, G1>), + node_match: NM, + edge_match: EM, + ordering: Ordering, + induced: bool, + node_map_g0: HashMap, + node_map_g1: HashMap, + stack: Vec>, + call_limit: Option, + _counter: usize, +} + +impl<'a, G0, G1, NM, EM> Vf2Algorithm<'a, G0, G1, NM, EM> +where + G0: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + G1: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + NM: NodeMatcher, + EM: EdgeMatcher, +{ + pub fn new( + g0: &'a G0, + g1: &'a 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<'a, G0>, Vf2State<'a, G1>)) -> 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<'a, G0>, Vf2State<'a, G1>), + 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<'a, G0>, Vf2State<'a, G1>), nodes: [NodeIndex; 2]) { + // Restore state. + st.0.pop_mapping(nodes[0]); + st.1.pop_mapping(nodes[1]); + } + + fn push_state(st: &mut (Vf2State<'a, G0>, Vf2State<'a, G1>), 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<'a, G0>, Vf2State<'a, G1>), + nodes: [NodeIndex; 2], + node_match: &mut NM, + edge_match: &mut EM, + ordering: Ordering, + induced: bool, + ) -> bool { + // 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 && 0 == 0 { + 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 false; + } + } + + for n_neigh in st.1.graph.neighbors(nodes[1]) { + succ_count[1] += 1; + if !induced && 1 == 0 { + continue; + } + // 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 false; + } + } + if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { + return 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 && 0 == 0 { + 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 false; + } + } + + for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { + pred_count[1] += 1; + if !induced && 1 == 0 { + continue; + } + // 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 false; + } + } + if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { + return 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 false; + } + if st.0.graph.is_directed() + && rule!(out, 0, Incoming) + .cmp(&rule!(out, 1, Incoming)) + .then(ordering) + != ordering + { + return false; + } + // R_in + if st.0.graph.is_directed() { + if rule!(ins, 0, Outgoing) + .cmp(&rule!(ins, 1, Outgoing)) + .then(ordering) + != ordering + { + return false; + } + + if rule!(ins, 0, Incoming) + .cmp(&rule!(ins, 1, Incoming)) + .then(ordering) + != ordering + { + return 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 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 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]) + { + return false; + } + // semantic feasibility: compare associated data for edges + if edge_match.enabled() { + let matcher = + |a: (NodeIndex, (NodeIndex, NodeIndex)), b: (NodeIndex, (NodeIndex, NodeIndex))| -> bool { + let (nx, n_edge) = a; + let (mx, m_edge) = b; + if nx == mx && edge_match.eq(st.0.graph, st.1.graph, n_edge, m_edge)? { + return true; + } + false + }; + + // outgoing edges + if induced { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 + .graph + .edges(nodes[1]) + .map(|edge| (edge.target(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, matcher)? { + return false; + }; + + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 + .graph + .edges(nodes[0]) + .map(|edge| (edge.target(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, matcher)? { + return false; + }; + } else { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 + .graph + .edges(nodes[0]) + .map(|edge| (edge.target(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, matcher)? { + return false; + }; + } + + // incoming edges + if st.0.graph.is_directed() { + if induced { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 + .graph + .edges_directed(nodes[1], Incoming) + .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, matcher)? { + return false; + }; + + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 + .graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, matcher)? { + return false; + }; + } else { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 + .graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, matcher)? { + return false; + }; + } + } + } + true + } + + /// 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::<'a, G0, G1, NM, EM>::pop_state(&mut self.st, nodes); + + match Vf2Algorithm::<'a, G0, G1, NM, EM>::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::<'a, G0, G1, NM, EM>::next_candidate(&mut self.st) { + None => { + if self.st.1.is_complete() { + return 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::<'a, G0, G1, NM, EM>::is_feasible( + &mut self.st, + nodes, + &mut self.node_match, + &mut self.edge_match, + self.ordering, + self.induced, + ) { + Vf2Algorithm::<'a, G0, G1, NM, EM>::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::<'a, G0, G1, NM, EM>::pop_state(&mut self.st, nodes); + } + match Vf2Algorithm::<'a, G0, G1, NM, EM>::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; From 452a77b155bf91aa3fa98b77a9ac0f2bc92995c0 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Jun 2024 12:14:57 -0400 Subject: [PATCH 02/15] Compiling. --- rustworkx-core/src/isomorphism/vf2.rs | 130 +++++++++++++------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 691e1f56c..04cfa547d 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -24,7 +24,7 @@ use hashbrown::HashMap; use crate::dictmap::*; use petgraph::stable_graph::NodeIndex; -use petgraph::visit::{Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, NodeIndexable}; +use petgraph::visit::{Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, IntoEdgesDirected, IntoNeighbors, IntoNodeIdentifiers, NodeCount, NodeIndexable}; use petgraph::EdgeType; use petgraph::{Directed, Incoming, Outgoing, Undirected}; use petgraph::data::{Build, Create, DataMap}; @@ -34,9 +34,9 @@ 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: &[T], ys: &[T], matcher: F) -> bool +fn is_subset(xs: &[T], ys: &[T], matcher: &mut F) -> bool where - F: Fn(T, T) -> bool, + F: FnMut(T, T) -> bool, { let mut valid = vec![true; ys.len()]; for &a in xs { @@ -149,52 +149,45 @@ where } /// Sort nodes based on node ids. -struct DefaultIdSorter { - _phantom: PhantomData, -} +struct DefaultIdSorter {} -impl DefaultIdSorter { +impl DefaultIdSorter { pub fn new() -> Self { Self { - _phantom: PhantomData::default(), } } } -impl<'a, G, GS> NodeSorter<'a, G> for DefaultIdSorter +impl<'a, G> NodeSorter<'a, G> for DefaultIdSorter where - G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers, + G: GraphBase + Create + DataMap + NodeCount + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers, G::NodeWeight: Clone, G::EdgeWeight: Clone, - GS: GraphBase + Create + Data, { - type OutputGraph = GS; + type OutputGraph = G; fn sort(&self, graph: &'a G) -> Vec { graph.node_identifiers().collect() } } /// Sort nodes based on VF2++ heuristic. -struct Vf2ppSorter { - _phantom: PhantomData, +struct Vf2ppSorter { } -impl Vf2ppSorter { +impl Vf2ppSorter { pub fn new() -> Self { Self { - _phantom: PhantomData::default(), } } } -impl<'a, G, GS> NodeSorter<'a, G> for Vf2ppSorter +impl<'a, G> NodeSorter<'a, G> for Vf2ppSorter where - G: GraphProp + GraphBase + DataMap + NodeCount + NodeIndexable + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers + IntoNeighborsDirected, + G: GraphProp + GraphBase + Create + DataMap + NodeCount + NodeIndexable + EdgeCount + IntoNodeIdentifiers + IntoEdgesDirected, G::NodeWeight: Clone, G::EdgeWeight: Clone, - GS: GraphBase + Create + Data, { - type OutputGraph = GS; + type OutputGraph = G; fn sort(&self, graph: &'a G) -> Vec { let n = graph.node_bound(); @@ -298,9 +291,9 @@ where } #[derive(Debug)] -struct Vf2State<'a, G> +struct Vf2State { - graph: &'a G, + graph: G, /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, /// NodeIndex::end() for no mapping. mapping: Vec, @@ -320,11 +313,11 @@ struct Vf2State<'a, G> _etype: marker::PhantomData, } -impl<'a, G> Vf2State<'a, G> +impl Vf2State where - G: GraphProp + GraphBase + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + G: GraphProp + GraphBase + NodeCount + EdgeCount + IntoEdgesDirected , { - pub fn new(graph: &'a G) -> Self { + pub fn new(graph: G) -> Self { let c0 = graph.node_count(); let is_directed = graph.is_directed(); let adjacency_matrix = adjacency_matrix(&graph); @@ -544,8 +537,12 @@ pub fn is_isomorphic<'a, G0, G1, NM, EM>( call_limit: Option, ) -> bool where - G0: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, - G1: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + G0: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, NM: NodeMatcher, EM: EdgeMatcher, { @@ -578,14 +575,14 @@ enum Frame { Unwind { nodes: [N; 2], open_list: OpenList }, } -struct Vf2Algorithm<'a, G0, G1, NM, EM> +struct Vf2Algorithm where G0: GraphBase, G1: GraphBase, NM: NodeMatcher, EM: EdgeMatcher, { - st: (Vf2State<'a, G0>, Vf2State<'a, G1>), + st: (Vf2State, Vf2State), node_match: NM, edge_match: EM, ordering: Ordering, @@ -597,16 +594,20 @@ where _counter: usize, } -impl<'a, G0, G1, NM, EM> Vf2Algorithm<'a, G0, G1, NM, EM> +impl Vf2Algorithm where - G0: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, - G1: GraphProp + GraphBase + Data + NodeCount + EdgeCount + IntoNeighborsDirected + IntoEdgeReferences, + G0: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, + G1::NodeWeight: Clone, + G1::EdgeWeight: Clone, NM: NodeMatcher, EM: EdgeMatcher, { pub fn new( - g0: &'a G0, - g1: &'a G1, + g0: &G0, + g1: &G1, node_match: NM, edge_match: EM, id_order: bool, @@ -615,15 +616,15 @@ where call_limit: Option, ) -> Self { let (g0, node_map_g0) = if id_order { - DefaultIdSorter::::new().reorder(g0) + DefaultIdSorter::new().reorder(g0) } else { - Vf2ppSorter::::new().reorder(g0) + Vf2ppSorter::new().reorder(g0) }; let (g1, node_map_g1) = if id_order { - DefaultIdSorter::::new().reorder(g1) + DefaultIdSorter::new().reorder(g1) } else { - Vf2ppSorter::::new().reorder(g1) + Vf2ppSorter::new().reorder(g1) }; let st = (Vf2State::new(g0), Vf2State::new(g1)); @@ -654,7 +655,7 @@ where mapping } - fn next_candidate(st: &mut (Vf2State<'a, G0>, Vf2State<'a, G1>)) -> Option<(NodeIndex, NodeIndex, OpenList)> { + 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; @@ -689,7 +690,7 @@ where } fn next_from_ix( - st: &mut (Vf2State<'a, G0>, Vf2State<'a, G1>), + st: &mut (Vf2State, Vf2State), nx: NodeIndex, open_list: OpenList, ) -> Option { @@ -710,20 +711,20 @@ where } } - fn pop_state(st: &mut (Vf2State<'a, G0>, Vf2State<'a, G1>), nodes: [NodeIndex; 2]) { + 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<'a, G0>, Vf2State<'a, G1>), nodes: [NodeIndex; 2]) { + 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<'a, G0>, Vf2State<'a, G1>), + st: &mut (Vf2State, Vf2State), nodes: [NodeIndex; 2], node_match: &mut NM, edge_match: &mut EM, @@ -750,7 +751,7 @@ where let mut succ_count = [0, 0]; for n_neigh in st.0.graph.neighbors(nodes[0]) { succ_count[0] += 1; - if !induced && 0 == 0 { + if !induced { continue; } // handle the self loop case; it's not in the mapping (yet) @@ -813,7 +814,7 @@ where let mut pred_count = [0, 0]; for n_neigh in st.0.graph.neighbors_directed(nodes[0], Incoming) { pred_count[0] += 1; - if !induced && 0 == 0 { + if !induced { continue; } // the self loop case is handled in outgoing @@ -838,9 +839,6 @@ where for n_neigh in st.1.graph.neighbors_directed(nodes[1], Incoming) { pred_count[1] += 1; - if !induced && 1 == 0 { - continue; - } // the self loop case is handled in outgoing let m_neigh = st.1.mapping[n_neigh.index()]; if m_neigh == end { @@ -962,18 +960,18 @@ where } } // semantic feasibility: compare associated data for nodes - if node_match.enabled() - && !node_match.eq(st.0.graph, st.1.graph, nodes[0], nodes[1]) + if NM::enabled() + && !node_match.eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) { return false; } // semantic feasibility: compare associated data for edges - if edge_match.enabled() { - let matcher = + if EM::enabled() { + let mut matcher = |a: (NodeIndex, (NodeIndex, NodeIndex)), b: (NodeIndex, (NodeIndex, NodeIndex))| -> bool { let (nx, n_edge) = a; let (mx, m_edge) = b; - if nx == mx && edge_match.eq(st.0.graph, st.1.graph, n_edge, m_edge)? { + if nx == mx && edge_match.eq(&st.0.graph, &st.1.graph, n_edge, m_edge) { return true; } false @@ -1004,7 +1002,7 @@ where .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, matcher)? { + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; @@ -1031,7 +1029,7 @@ where .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, matcher)? { + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; } else { @@ -1058,7 +1056,7 @@ where .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, matcher)? { + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; } @@ -1089,7 +1087,7 @@ where .map(|edge| (edge.source(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, matcher)? { + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; @@ -1116,7 +1114,7 @@ where .map(|edge| (edge.source(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, matcher)? { + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; } else { @@ -1143,7 +1141,7 @@ where .map(|edge| (edge.source(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, matcher)? { + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; } @@ -1181,9 +1179,9 @@ where nodes, open_list: ol, } => { - Vf2Algorithm::<'a, G0, G1, NM, EM>::pop_state(&mut self.st, nodes); + Vf2Algorithm::::pop_state(&mut self.st, nodes); - match Vf2Algorithm::<'a, G0, G1, NM, EM>::next_from_ix(&mut self.st, nodes[0], ol) { + match Vf2Algorithm::::next_from_ix(&mut self.st, nodes[0], ol) { None => continue, Some(nx) => { let f = Frame::Inner { @@ -1194,7 +1192,7 @@ where } } } - Frame::Outer => match Vf2Algorithm::<'a, G0, G1, NM, EM>::next_candidate(&mut self.st) { + Frame::Outer => match Vf2Algorithm::::next_candidate(&mut self.st) { None => { if self.st.1.is_complete() { return Some(self.mapping()); @@ -1213,7 +1211,7 @@ where nodes, open_list: ol, } => { - if Vf2Algorithm::<'a, G0, G1, NM, EM>::is_feasible( + if Vf2Algorithm::::is_feasible( &mut self.st, nodes, &mut self.node_match, @@ -1221,7 +1219,7 @@ where self.ordering, self.induced, ) { - Vf2Algorithm::<'a, G0, G1, NM, EM>::push_state(&mut self.st, nodes); + Vf2Algorithm::::push_state(&mut self.st, nodes); // Check cardinalities of Tin, Tout sets if self.st.0 .out_size @@ -1249,9 +1247,9 @@ where self.stack.push(Frame::Outer); continue; } - Vf2Algorithm::<'a, G0, G1, NM, EM>::pop_state(&mut self.st, nodes); + Vf2Algorithm::::pop_state(&mut self.st, nodes); } - match Vf2Algorithm::<'a, G0, G1, NM, EM>::next_from_ix(&mut self.st, nodes[0], ol) { + match Vf2Algorithm::::next_from_ix(&mut self.st, nodes[0], ol) { None => continue, Some(nx) => { let f = Frame::Inner { From 59a4f3b107210e9d3bde2d800e95892800877f50 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Jun 2024 12:16:55 -0400 Subject: [PATCH 03/15] Run formatting. --- rustworkx-core/src/isomorphism/vf2.rs | 508 ++++++++++++++------------ 1 file changed, 270 insertions(+), 238 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 04cfa547d..bc9b683fb 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -18,20 +18,20 @@ use std::cmp::{Ordering, Reverse}; use std::iter::Iterator; use std::marker; -use std::marker::PhantomData; -use hashbrown::HashMap; use crate::dictmap::*; +use hashbrown::HashMap; -use petgraph::stable_graph::NodeIndex; -use petgraph::visit::{Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, IntoEdgesDirected, IntoNeighbors, IntoNodeIdentifiers, NodeCount, NodeIndexable}; -use petgraph::EdgeType; -use petgraph::{Directed, Incoming, Outgoing, Undirected}; use petgraph::data::{Build, Create, DataMap}; +use petgraph::stable_graph::NodeIndex; +use petgraph::visit::{ + Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, + IntoEdgesDirected, IntoNeighbors, 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: &[T], ys: &[T], matcher: &mut F) -> bool @@ -67,10 +67,9 @@ fn sorted(x: &mut (N, N)) { /// 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 +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() { @@ -90,8 +89,9 @@ fn edge_multiplicity( matrix: &HashMap<(NodeIndex, NodeIndex), usize>, a: NodeIndex, b: NodeIndex, -) -> usize where - G: GraphProp + GraphBase +) -> usize +where + G: GraphProp + GraphBase, { let mut item = (a, b); if !graph.is_directed() { @@ -108,7 +108,10 @@ fn is_adjacent( a: NodeIndex, b: NodeIndex, val: usize, -) -> bool where G: GraphProp + GraphBase { +) -> bool +where + G: GraphProp + GraphBase, +{ edge_multiplicity(graph, matrix, a, b) >= val } @@ -118,17 +121,17 @@ where G::NodeWeight: Clone, G::EdgeWeight: Clone, { - type OutputGraph: GraphBase + Create + Data; + type OutputGraph: GraphBase + + Create + + Data; fn sort(&self, _: &'a G) -> Vec; - fn reorder( - &self, - graph: &'a G, - ) -> (Self::OutputGraph, HashMap) { + fn reorder(&self, graph: &'a 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 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(); @@ -153,14 +156,19 @@ struct DefaultIdSorter {} impl DefaultIdSorter { pub fn new() -> Self { - Self { - } + Self {} } } impl<'a, G> NodeSorter<'a, G> for DefaultIdSorter where - G: GraphBase + Create + DataMap + NodeCount + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers, + G: GraphBase + + Create + + DataMap + + NodeCount + + EdgeCount + + IntoEdgeReferences + + IntoNodeIdentifiers, G::NodeWeight: Clone, G::EdgeWeight: Clone, { @@ -171,19 +179,25 @@ where } /// Sort nodes based on VF2++ heuristic. -struct Vf2ppSorter { -} +struct Vf2ppSorter {} impl Vf2ppSorter { pub fn new() -> Self { - Self { - } + Self {} } } impl<'a, G> NodeSorter<'a, G> for Vf2ppSorter where - G: GraphProp + GraphBase + Create + DataMap + NodeCount + NodeIndexable + EdgeCount + IntoNodeIdentifiers + IntoEdgesDirected, + G: GraphProp + + GraphBase + + Create + + DataMap + + NodeCount + + NodeIndexable + + EdgeCount + + IntoNodeIdentifiers + + IntoEdgesDirected, G::NodeWeight: Clone, G::EdgeWeight: Clone, { @@ -278,7 +292,8 @@ where } }; - let mut sorted_nodes: Vec = graph.node_identifiers().map(|node| node.index()).collect(); + 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(); @@ -291,8 +306,7 @@ where } #[derive(Debug)] -struct Vf2State -{ +struct Vf2State { graph: G, /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, /// NodeIndex::end() for no mapping. @@ -315,7 +329,7 @@ struct Vf2State impl Vf2State where - G: GraphProp + GraphBase + NodeCount + EdgeCount + IntoEdgesDirected , + G: GraphProp + GraphBase + NodeCount + EdgeCount + IntoEdgesDirected, { pub fn new(graph: G) -> Self { let c0 = graph.node_count(); @@ -439,10 +453,10 @@ impl NodeMatcher for NoSemanticMatch { } impl NodeMatcher for F - where - G0: GraphBase + DataMap, - G1: GraphBase + DataMap, - F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> bool, +where + G0: GraphBase + DataMap, + G1: GraphBase + DataMap, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> bool, { #[inline] fn enabled() -> bool { @@ -487,10 +501,10 @@ impl EdgeMatcher for NoSemanticMatch { } impl EdgeMatcher for F - where - G0: GraphBase + DataMap + IntoEdgesDirected, - G1: GraphBase + DataMap + IntoEdgesDirected, - F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> bool, +where + G0: GraphBase + DataMap + IntoEdgesDirected, + G1: GraphBase + DataMap + IntoEdgesDirected, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> bool, { #[inline] fn enabled() -> bool { @@ -536,15 +550,31 @@ pub fn is_isomorphic<'a, G0, G1, NM, EM>( induced: bool, call_limit: Option, ) -> bool - where - G0: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, - G0::NodeWeight: Clone, - G0::EdgeWeight: Clone, - G1: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, - G1::NodeWeight: Clone, - G1::EdgeWeight: Clone, - NM: NodeMatcher, - EM: EdgeMatcher, +where + G0: GraphProp + + GraphBase + + Create + + DataMap + + NodeCount + + EdgeCount + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, + G0::NodeWeight: Clone, + G0::EdgeWeight: Clone, + G1: GraphProp + + GraphBase + + Create + + DataMap + + NodeCount + + EdgeCount + + 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) @@ -596,10 +626,26 @@ where impl Vf2Algorithm where - G0: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, + G0: GraphProp + + GraphBase + + Create + + DataMap + + NodeCount + + EdgeCount + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, G0::NodeWeight: Clone, G0::EdgeWeight: Clone, - G1: GraphProp + GraphBase + Create + DataMap + NodeCount + EdgeCount + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, + G1: GraphProp + + GraphBase + + Create + + DataMap + + NodeCount + + EdgeCount + + NodeIndexable + + IntoEdgesDirected + + IntoNodeIdentifiers, G1::NodeWeight: Clone, G1::EdgeWeight: Clone, NM: NodeMatcher, @@ -644,7 +690,8 @@ where fn mapping(&self) -> DictMap { let mut mapping: DictMap = DictMap::new(); - self.st.1 + self.st + .1 .mapping .iter() .enumerate() @@ -655,7 +702,9 @@ where mapping } - fn next_candidate(st: &mut (Vf2State, Vf2State)) -> Option<(NodeIndex, NodeIndex, OpenList)> { + 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; @@ -763,16 +812,9 @@ where 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, - ); + 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 false; } @@ -792,16 +834,9 @@ where 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, - ); + 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 false; } @@ -822,16 +857,10 @@ where 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, - ); + 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 false; } @@ -844,16 +873,10 @@ where 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, - ); + 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 false; } @@ -960,115 +983,29 @@ where } } // semantic feasibility: compare associated data for nodes - if NM::enabled() - && !node_match.eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) - { + if NM::enabled() && !node_match.eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) { return false; } // semantic feasibility: compare associated data for edges if EM::enabled() { - let mut matcher = - |a: (NodeIndex, (NodeIndex, NodeIndex)), b: (NodeIndex, (NodeIndex, NodeIndex))| -> bool { - let (nx, n_edge) = a; - let (mx, m_edge) = b; - if nx == mx && edge_match.eq(&st.0.graph, &st.1.graph, n_edge, m_edge) { - return true; - } - false - }; + let mut matcher = |a: (NodeIndex, (NodeIndex, NodeIndex)), + b: (NodeIndex, (NodeIndex, NodeIndex))| + -> bool { + let (nx, n_edge) = a; + let (mx, m_edge) = b; + if nx == mx && edge_match.eq(&st.0.graph, &st.1.graph, n_edge, m_edge) { + return true; + } + false + }; // outgoing edges if induced { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) - }) - .collect(); - - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 - .graph - .edges(nodes[1]) - .map(|edge| (edge.target(), (edge.source(), edge.target()))) - .collect(); - - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; - }; - - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) - }) - .collect(); - - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 - .graph - .edges(nodes[0]) - .map(|edge| (edge.target(), (edge.source(), edge.target()))) - .collect(); - - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; - }; - } else { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = 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.source(), edge.target()))) - }) - .collect(); - - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 - .graph - .edges(nodes[0]) - .map(|edge| (edge.target(), (edge.source(), edge.target()))) - .collect(); - - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; - }; - } - - // incoming edges - if st.0.graph.is_directed() { - if induced { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 - .graph - .edges_directed(nodes[0], Incoming) + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.0.graph + .edges(nodes[0]) .filter_map(|edge| { - let n_neigh = edge.source(); + let n_neigh = edge.target(); let m_neigh = if nodes[0] != n_neigh { st.0.mapping[n_neigh.index()] } else { @@ -1081,21 +1018,21 @@ where }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 - .graph - .edges_directed(nodes[1], Incoming) - .map(|edge| (edge.source(), (edge.source(), edge.target()))) + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.1.graph + .edges(nodes[1]) + .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; - }; + if !is_subset(&e_first, &e_second, &mut matcher) { + return false; + }; - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 - .graph - .edges_directed(nodes[1], Incoming) + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.1.graph + .edges(nodes[1]) .filter_map(|edge| { - let n_neigh = edge.source(); + let n_neigh = edge.target(); let m_neigh = if nodes[1] != n_neigh { st.1.mapping[n_neigh.index()] } else { @@ -1108,21 +1045,21 @@ where }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 - .graph - .edges_directed(nodes[0], Incoming) - .map(|edge| (edge.source(), (edge.source(), edge.target()))) + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.0.graph + .edges(nodes[0]) + .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; - }; - } else { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.1 - .graph - .edges_directed(nodes[1], Incoming) + if !is_subset(&e_first, &e_second, &mut matcher) { + return false; + }; + } else { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.1.graph + .edges(nodes[1]) .filter_map(|edge| { - let n_neigh = edge.source(); + let n_neigh = edge.target(); let m_neigh = if nodes[1] != n_neigh { st.1.mapping[n_neigh.index()] } else { @@ -1135,12 +1072,97 @@ where }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = st.0 - .graph - .edges_directed(nodes[0], Incoming) - .map(|edge| (edge.source(), (edge.source(), edge.target()))) + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.0.graph + .edges(nodes[0]) + .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); + if !is_subset(&e_first, &e_second, &mut matcher) { + return false; + }; + } + + // incoming edges + if st.0.graph.is_directed() { + if induced { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.1.graph + .edges_directed(nodes[1], Incoming) + .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher) { + return false; + }; + + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.1.graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .collect(); + + if !is_subset(&e_first, &e_second, &mut matcher) { + return false; + }; + } else { + let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + 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.source(), edge.target()))) + }) + .collect(); + + let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + st.0.graph + .edges_directed(nodes[0], Incoming) + .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .collect(); + if !is_subset(&e_first, &e_second, &mut matcher) { return false; }; @@ -1152,13 +1174,17 @@ where /// Return Some(mapping) if isomorphism is decided, else None. fn next(&mut self) -> Option> { - if (self.st.0 + if (self + .st + .0 .graph .node_count() .cmp(&self.st.1.graph.node_count()) .then(self.ordering) != self.ordering) - || (self.st.0 + || (self + .st + .0 .graph .edge_count() .cmp(&self.st.1.graph.edge_count()) @@ -1192,21 +1218,23 @@ where } } } - Frame::Outer => match Vf2Algorithm::::next_candidate(&mut self.st) { - None => { - if self.st.1.is_complete() { - return Some(self.mapping()); + Frame::Outer => { + match Vf2Algorithm::::next_candidate(&mut self.st) { + None => { + if self.st.1.is_complete() { + return Some(self.mapping()); + } + continue; + } + Some((nx, mx, ol)) => { + let f = Frame::Inner { + nodes: [nx, mx], + open_list: ol, + }; + self.stack.push(f); } - 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, @@ -1221,12 +1249,16 @@ where ) { Vf2Algorithm::::push_state(&mut self.st, nodes); // Check cardinalities of Tin, Tout sets - if self.st.0 + if self + .st + .0 .out_size .cmp(&self.st.1.out_size) .then(self.ordering) == self.ordering - && self.st.0 + && self + .st + .0 .ins_size .cmp(&self.st.1.ins_size) .then(self.ordering) From c0f94a2ba83771fcfd30552dc78e83889cc5212a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Jun 2024 13:52:32 -0400 Subject: [PATCH 04/15] Add generic IsIsomorphismError to support callback failures. --- rustworkx-core/src/isomorphism/vf2.rs | 177 ++++++++++++++++---------- 1 file changed, 109 insertions(+), 68 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index bc9b683fb..6e8e8ee1e 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -16,6 +16,7 @@ // since diverged significantly from the original petgraph implementation. use std::cmp::{Ordering, Reverse}; +use std::convert::Infallible; use std::iter::Iterator; use std::marker; @@ -34,15 +35,15 @@ 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: &[T], ys: &[T], matcher: &mut F) -> bool +fn is_subset(xs: &[T], ys: &[T], matcher: &mut F) -> Result where - F: FnMut(T, T) -> bool, + F: FnMut(T, T) -> 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) { + if *free && matcher(a, b)? { found = true; *free = false; break; @@ -50,11 +51,11 @@ where } if !found { - return false; + return Ok(false); } } - true + Ok(true) } #[inline] @@ -434,45 +435,72 @@ where } } +pub enum IsIsomorphicError { + NodeMatcherErr(NME), + EdgeMatcherErr(EME), +} + pub struct NoSemanticMatch; pub trait NodeMatcher { + type Error; fn enabled() -> bool; - fn eq(&mut self, _g0: &G0, _g1: &G1, _n0: G0::NodeId, _n1: G1::NodeId) -> 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() -> bool { false } #[inline] - fn eq(&mut self, _g0: &G0, _g1: &G1, _n0: G0::NodeId, _n1: G1::NodeId) -> bool { - true + fn eq( + &mut self, + _g0: &G0, + _g1: &G1, + _n0: G0::NodeId, + _n1: G1::NodeId, + ) -> Result { + Ok(true) } } -impl NodeMatcher for F +impl NodeMatcher for F where G0: GraphBase + DataMap, G1: GraphBase + DataMap, - F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> bool, + F: FnMut(&G0::NodeWeight, &G1::NodeWeight) -> Result, { + type Error = E; #[inline] fn enabled() -> bool { true } #[inline] - fn eq(&mut self, g0: &G0, g1: &G1, n0: G0::NodeId, n1: G1::NodeId) -> bool { + 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 { - false + Ok(false) } } } pub trait EdgeMatcher { + type Error; fn enabled() -> bool; fn eq( &mut self, @@ -480,10 +508,11 @@ pub trait EdgeMatcher { _g1: &G1, e0: (G0::NodeId, G0::NodeId), e1: (G1::NodeId, G1::NodeId), - ) -> bool; + ) -> Result; } impl EdgeMatcher for NoSemanticMatch { + type Error = Infallible; #[inline] fn enabled() -> bool { false @@ -495,17 +524,18 @@ impl EdgeMatcher for NoSemanticMatch { _g1: &G1, _e0: (G0::NodeId, G0::NodeId), _e1: (G1::NodeId, G1::NodeId), - ) -> bool { - true + ) -> Result { + Ok(true) } } -impl EdgeMatcher for F +impl EdgeMatcher for F where G0: GraphBase + DataMap + IntoEdgesDirected, G1: GraphBase + DataMap + IntoEdgesDirected, - F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> bool, + F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, { + type Error = E; #[inline] fn enabled() -> bool { true @@ -517,7 +547,7 @@ where g1: &G1, e0: (G0::NodeId, G0::NodeId), e1: (G1::NodeId, G1::NodeId), - ) -> bool { + ) -> Result { let w0 = g0 .edges_directed(e0.0, Outgoing) .find(|edge| edge.target() == e0.1) @@ -529,7 +559,7 @@ where if let (Some(x), Some(y)) = (w0, w1) { self(x, y) } else { - false + Ok(false) } } } @@ -549,7 +579,7 @@ pub fn is_isomorphic<'a, G0, G1, NM, EM>( ordering: Ordering, induced: bool, call_limit: Option, -) -> bool +) -> Result> where G0: GraphProp + GraphBase @@ -579,16 +609,16 @@ where if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) { - return false; + return Ok(false); } let mut vf2 = Vf2Algorithm::new( g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, ); - if vf2.next().is_some() { - return true; + if vf2.next()?.is_some() { + return Ok(true); } - false + Ok(false) } #[derive(Copy, Clone, PartialEq, Debug)] @@ -779,7 +809,7 @@ where edge_match: &mut EM, ordering: Ordering, induced: bool, - ) -> bool { + ) -> Result> { // Check syntactic feasibility of mapping by ensuring adjacencies // of nx map to adjacencies of mx. // @@ -816,7 +846,7 @@ where let has_edge = is_adjacent(&st.1.graph, &st.1.adjacency_matrix, nodes[1], m_neigh, val); if !has_edge { - return false; + return Ok(false); } } @@ -838,11 +868,11 @@ where let has_edge = is_adjacent(&st.0.graph, &st.0.adjacency_matrix, nodes[0], m_neigh, val); if !has_edge { - return false; + return Ok(false); } } if succ_count[0].cmp(&succ_count[1]).then(ordering) != ordering { - return false; + return Ok(false); } // R_pred if st.0.graph.is_directed() { @@ -862,7 +892,7 @@ where let has_edge = is_adjacent(&st.1.graph, &st.1.adjacency_matrix, m_neigh, nodes[1], val); if !has_edge { - return false; + return Ok(false); } } @@ -878,11 +908,11 @@ where let has_edge = is_adjacent(&st.0.graph, &st.0.adjacency_matrix, m_neigh, nodes[0], val); if !has_edge { - return false; + return Ok(false); } } if pred_count[0].cmp(&pred_count[1]).then(ordering) != ordering { - return false; + return Ok(false); } } macro_rules! field { @@ -917,7 +947,7 @@ where .then(ordering) != ordering { - return false; + return Ok(false); } if st.0.graph.is_directed() && rule!(out, 0, Incoming) @@ -925,7 +955,7 @@ where .then(ordering) != ordering { - return false; + return Ok(false); } // R_in if st.0.graph.is_directed() { @@ -934,7 +964,7 @@ where .then(ordering) != ordering { - return false; + return Ok(false); } if rule!(ins, 0, Incoming) @@ -942,7 +972,7 @@ where .then(ordering) != ordering { - return false; + return Ok(false); } } // R_new @@ -961,7 +991,7 @@ where } } if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { - return false; + return Ok(false); } if st.0.graph.is_directed() { let mut new_count = [0, 0]; @@ -978,26 +1008,35 @@ where } } if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { - return false; + return Ok(false); } } } // semantic feasibility: compare associated data for nodes - if NM::enabled() && !node_match.eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) { - return false; + if NM::enabled() + && !node_match + .eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) + .map_err(|e| IsIsomorphicError::NodeMatcherErr(e))? + { + return Ok(false); } // semantic feasibility: compare associated data for edges if EM::enabled() { - let mut matcher = |a: (NodeIndex, (NodeIndex, NodeIndex)), - b: (NodeIndex, (NodeIndex, NodeIndex))| - -> bool { - let (nx, n_edge) = a; - let (mx, m_edge) = b; - if nx == mx && edge_match.eq(&st.0.graph, &st.1.graph, n_edge, m_edge) { - return true; - } - false - }; + let mut matcher = + |a: (NodeIndex, (NodeIndex, NodeIndex)), + b: (NodeIndex, (NodeIndex, NodeIndex))| + -> Result> { + let (nx, n_edge) = a; + let (mx, m_edge) = b; + if nx == mx + && edge_match + .eq(&st.0.graph, &st.1.graph, n_edge, m_edge) + .map_err(|e| IsIsomorphicError::EdgeMatcherErr(e))? + { + return Ok(true); + } + Ok(false) + }; // outgoing edges if induced { @@ -1024,8 +1063,8 @@ where .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); }; let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = @@ -1051,8 +1090,8 @@ where .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); }; } else { let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = @@ -1078,8 +1117,8 @@ where .map(|edge| (edge.target(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); }; } @@ -1109,8 +1148,8 @@ where .map(|edge| (edge.source(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); }; let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = @@ -1136,8 +1175,8 @@ where .map(|edge| (edge.source(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); }; } else { let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = @@ -1163,17 +1202,19 @@ where .map(|edge| (edge.source(), (edge.source(), edge.target()))) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher) { - return false; + if !is_subset(&e_first, &e_second, &mut matcher)? { + return Ok(false); }; } } } - true + Ok(true) } /// Return Some(mapping) if isomorphism is decided, else None. - fn next(&mut self) -> Option> { + fn next( + &mut self, + ) -> Result>, IsIsomorphicError> { if (self .st .0 @@ -1191,7 +1232,7 @@ where .then(self.ordering) != self.ordering) { - return None; + return Ok(None); } // A "depth first" search of a valid mapping from graph 1 to graph 2 @@ -1222,7 +1263,7 @@ where match Vf2Algorithm::::next_candidate(&mut self.st) { None => { if self.st.1.is_complete() { - return Some(self.mapping()); + return Ok(Some(self.mapping())); } continue; } @@ -1246,7 +1287,7 @@ where &mut self.edge_match, self.ordering, self.induced, - ) { + )? { Vf2Algorithm::::push_state(&mut self.st, nodes); // Check cardinalities of Tin, Tout sets if self @@ -1267,7 +1308,7 @@ where self._counter += 1; if let Some(limit) = self.call_limit { if self._counter > limit { - return None; + return Ok(None); } } let f0 = Frame::Unwind { @@ -1294,6 +1335,6 @@ where } } } - None + Ok(None) } } From 19202cb6c81a1072361e03f260f8bb316920c75f Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Jun 2024 18:06:15 -0400 Subject: [PATCH 05/15] Compiling! --- rustworkx-core/src/isomorphism/vf2.rs | 108 ++++++++------ src/isomorphism/mod.rs | 159 ++++++++++++++------- src/isomorphism/{vf2.rs => vf2_mapping.rs} | 0 3 files changed, 173 insertions(+), 94 deletions(-) rename src/isomorphism/{vf2.rs => vf2_mapping.rs} (100%) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 6e8e8ee1e..a4846c4c7 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -17,8 +17,11 @@ 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; @@ -27,7 +30,8 @@ use petgraph::data::{Build, Create, DataMap}; use petgraph::stable_graph::NodeIndex; use petgraph::visit::{ Data, EdgeCount, EdgeRef, GraphBase, GraphProp, IntoEdgeReferences, IntoEdges, - IntoEdgesDirected, IntoNeighbors, IntoNodeIdentifiers, NodeCount, NodeIndexable, + IntoEdgesDirected, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, + NodeIndexable, }; use petgraph::{Directed, Incoming, Outgoing}; @@ -68,7 +72,7 @@ fn sorted(x: &mut (N, N)) { /// 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> +fn adjacency_matrix(graph: G) -> HashMap<(NodeIndex, NodeIndex), usize> where G: GraphProp + GraphBase + EdgeCount + IntoEdgeReferences, { @@ -116,7 +120,7 @@ where edge_multiplicity(graph, matrix, a, b) >= val } -trait NodeSorter<'a, G> +trait NodeSorter where G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, G::NodeWeight: Clone, @@ -126,9 +130,9 @@ where + Create + Data; - fn sort(&self, _: &'a G) -> Vec; + fn sort(&self, _: G) -> Vec; - fn reorder(&self, graph: &'a G) -> (Self::OutputGraph, HashMap) { + fn reorder(&self, graph: G) -> (Self::OutputGraph, HashMap) { let order = self.sort(graph); let mut new_graph = @@ -161,20 +165,23 @@ impl DefaultIdSorter { } } -impl<'a, G> NodeSorter<'a, G> for DefaultIdSorter +impl NodeSorter for DefaultIdSorter where - G: GraphBase - + Create + G: Deref + + GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences + IntoNodeIdentifiers, + G::Target: GraphBase + + Data + + Create, G::NodeWeight: Clone, G::EdgeWeight: Clone, { - type OutputGraph = G; - fn sort(&self, graph: &'a G) -> Vec { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { graph.node_identifiers().collect() } } @@ -188,22 +195,25 @@ impl Vf2ppSorter { } } -impl<'a, G> NodeSorter<'a, G> for Vf2ppSorter +impl NodeSorter for Vf2ppSorter where - G: GraphProp + G: Deref + + GraphProp + GraphBase - + Create + DataMap + NodeCount + NodeIndexable + EdgeCount + IntoNodeIdentifiers + IntoEdgesDirected, + G::Target: GraphBase + + Data + + Create, G::NodeWeight: Clone, G::EdgeWeight: Clone, { - type OutputGraph = G; - fn sort(&self, graph: &'a G) -> Vec { + type OutputGraph = G::Target; + fn sort(&self, graph: G) -> Vec { let n = graph.node_bound(); let dout: Vec = (0..n) @@ -330,7 +340,9 @@ struct Vf2State { impl Vf2State where - G: GraphProp + GraphBase + NodeCount + EdgeCount + IntoEdgesDirected, + 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(); @@ -435,11 +447,27 @@ where } } +#[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 { @@ -531,8 +559,10 @@ impl EdgeMatcher for NoSemanticMatch { impl EdgeMatcher for F where - G0: GraphBase + DataMap + IntoEdgesDirected, - G1: GraphBase + DataMap + IntoEdgesDirected, + G0: GraphBase + DataMap, + for<'a> &'a G0: GraphBase + IntoEdgesDirected, + G1: GraphBase + DataMap, + for<'a> &'a G1: GraphBase + IntoEdgesDirected, F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, { type Error = E; @@ -570,9 +600,9 @@ where /// graph isomorphism (graph structure and matching node and edge weights). /// /// The graphs should not be multigraphs. -pub fn is_isomorphic<'a, G0, G1, NM, EM>( - g0: &'a G0, - g1: &'a G1, +pub fn is_isomorphic( + g0: &G0, + g1: &G1, node_match: NM, edge_match: EM, id_order: bool, @@ -581,23 +611,17 @@ pub fn is_isomorphic<'a, G0, G1, NM, EM>( call_limit: Option, ) -> Result> where - G0: GraphProp - + GraphBase - + Create - + DataMap - + NodeCount - + EdgeCount + 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 - + Create - + DataMap - + NodeCount - + EdgeCount + G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, @@ -656,23 +680,17 @@ where impl Vf2Algorithm where - G0: GraphProp - + GraphBase - + Create - + DataMap - + NodeCount - + EdgeCount + 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 - + Create - + DataMap - + NodeCount - + EdgeCount + G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, + for<'a> &'a G1: GraphBase + + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 6e48f1f10..eaebf746a 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -12,14 +12,16 @@ #![allow(clippy::too_many_arguments)] -mod vf2; +mod vf2_mapping; use crate::{digraph, graph}; +use rustworkx_core::isomorphism::vf2; use std::cmp::Ordering; use pyo3::prelude::*; use pyo3::Python; +use rustworkx_core::isomorphism::vf2::{IsIsomorphicError, NoSemanticMatch}; /// Determine if 2 directed graphs are isomorphic /// @@ -72,17 +74,73 @@ pub fn digraph_is_isomorphic( id_order: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( - py, - &first.graph, - &second.graph, - node_matcher, - edge_matcher, - id_order, - Ordering::Equal, - true, - call_limit, - ) + 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.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + Ordering::Equal, + true, + call_limit, + ) + .map_err(|e| match e { + IsIsomorphicError::NodeMatcherErr(e) => e, + IsIsomorphicError::EdgeMatcherErr(e) => e, + })?, + (Some(node_matcher), None) => vf2::is_isomorphic( + &first.graph, + &second.graph, + node_matcher, + NoSemanticMatch, + id_order, + Ordering::Equal, + true, + call_limit, + ) + .map_err(|e| match e { + IsIsomorphicError::NodeMatcherErr(e) => e, + _ => unreachable!(), + })?, + (None, Some(edge_matcher)) => vf2::is_isomorphic( + &first.graph, + &second.graph, + NoSemanticMatch, + edge_matcher, + id_order, + Ordering::Equal, + true, + call_limit, + ) + .map_err(|e| match e { + IsIsomorphicError::EdgeMatcherErr(e) => e, + _ => unreachable!(), + })?, + (None, None) => vf2::is_isomorphic( + &first.graph, + &second.graph, + NoSemanticMatch, + NoSemanticMatch, + id_order, + Ordering::Equal, + true, + call_limit, + ) + .unwrap(), + }; + Ok(result) } /// Determine if 2 undirected graphs are isomorphic @@ -136,17 +194,18 @@ pub fn graph_is_isomorphic( id_order: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( - py, - &first.graph, - &second.graph, - node_matcher, - edge_matcher, - id_order, - Ordering::Equal, - true, - call_limit, - ) + // vf2::is_isomorphic( + // py, + // &first.graph, + // &second.graph, + // node_matcher, + // edge_matcher, + // id_order, + // Ordering::Equal, + // true, + // call_limit, + // ) + todo!() } /// Determine if 2 directed graphs are subgraph - isomorphic @@ -208,17 +267,18 @@ pub fn digraph_is_subgraph_isomorphic( induced: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( - py, - &first.graph, - &second.graph, - node_matcher, - edge_matcher, - id_order, - Ordering::Greater, - induced, - call_limit, - ) + // vf2::is_isomorphic( + // py, + // &first.graph, + // &second.graph, + // node_matcher, + // edge_matcher, + // id_order, + // Ordering::Greater, + // induced, + // call_limit, + // ) + todo!() } /// Determine if 2 undirected graphs are subgraph - isomorphic @@ -280,17 +340,18 @@ pub fn graph_is_subgraph_isomorphic( induced: bool, call_limit: Option, ) -> PyResult { - vf2::is_isomorphic( - py, - &first.graph, - &second.graph, - node_matcher, - edge_matcher, - id_order, - Ordering::Greater, - induced, - call_limit, - ) + // vf2::is_isomorphic( + // py, + // &first.graph, + // &second.graph, + // node_matcher, + // edge_matcher, + // id_order, + // Ordering::Greater, + // induced, + // call_limit, + // ) + todo!() } /// Return an iterator over all vf2 mappings between two :class:`~rustworkx.PyDiGraph` objects @@ -352,14 +413,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 +491,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_mapping.rs similarity index 100% rename from src/isomorphism/vf2.rs rename to src/isomorphism/vf2_mapping.rs From dd587984dd384f6a28c104c749828080cc506588 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Sat, 29 Jun 2024 18:29:00 -0400 Subject: [PATCH 06/15] Hooked up, isomorphism tests passing! --- src/isomorphism/mod.rs | 237 ++++++++++++++++++++++++----------------- 1 file changed, 139 insertions(+), 98 deletions(-) diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index eaebf746a..06302df37 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -17,63 +17,43 @@ mod vf2_mapping; use crate::{digraph, graph}; use rustworkx_core::isomorphism::vf2; +use petgraph::data::{Create, DataMap}; +use petgraph::prelude::*; +use petgraph::visit::{ + Data, EdgeCount, GraphBase, GraphProp, IntoEdgesDirected, IntoNodeIdentifiers, NodeCount, + NodeIndexable, +}; use std::cmp::Ordering; use pyo3::prelude::*; use pyo3::Python; use rustworkx_core::isomorphism::vf2::{IsIsomorphicError, NoSemanticMatch}; -/// Determine if 2 directed graphs are isomorphic -/// -/// This checks if 2 graphs are isomorphic both structurally and also -/// comparing the node data and edge data using the provided matcher functions. -/// The matcher function takes in 2 data objects and will compare them. A simple -/// example that checks if they're just equal would be:: -/// -/// graph_a = rustworkx.PyDiGraph() -/// graph_b = rustworkx.PyDiGraph() -/// rustworkx.is_isomorphic(graph_a, graph_b, -/// lambda x, y: x == y) -/// -/// .. note:: -/// -/// For better performance on large graphs, consider setting `id_order=False`. -/// -/// :param PyDiGraph first: The first graph to compare -/// :param PyDiGraph second: The second graph to compare -/// :param callable node_matcher: A python callable object that takes 2 positional -/// one for each node data object. If the return of this -/// function evaluates to True then the nodes passed to it are vieded -/// as matching. -/// :param callable edge_matcher: A python callable object that takes 2 positional -/// one for each edge data object. If the return of this -/// function evaluates to True then the edges passed to it are vieded -/// as matching. -/// :param bool id_order: If set to ``False`` this function will use a -/// heuristic matching order based on [VF2]_ paper. Otherwise it will -/// default to matching the nodes in order specified by their ids. -/// :param int call_limit: An optional bound on the number of states that VF2 algorithm -/// visits while searching for a solution. If it exceeds this limit, the algorithm -/// will stop and return ``False``. -/// -/// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are -/// not. -/// :rtype: bool -#[pyfunction] -#[pyo3( - signature=(first, second, node_matcher=None, edge_matcher=None, id_order=true, call_limit=None), - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, - id_order=True, call_limit=None)" -)] -pub fn digraph_is_isomorphic( +fn is_isomorphic( py: Python, - first: &digraph::PyDiGraph, - second: &digraph::PyDiGraph, + first: &G, + second: &G, node_matcher: Option, edge_matcher: Option, id_order: bool, + ordering: Ordering, + induced: bool, call_limit: Option, -) -> PyResult { +) -> 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, @@ -87,13 +67,13 @@ pub fn digraph_is_isomorphic( 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.graph, - &second.graph, + first, + second, node_matcher, edge_matcher, id_order, - Ordering::Equal, - true, + ordering, + induced, call_limit, ) .map_err(|e| match e { @@ -101,13 +81,13 @@ pub fn digraph_is_isomorphic( IsIsomorphicError::EdgeMatcherErr(e) => e, })?, (Some(node_matcher), None) => vf2::is_isomorphic( - &first.graph, - &second.graph, + first, + second, node_matcher, NoSemanticMatch, id_order, - Ordering::Equal, - true, + ordering, + induced, call_limit, ) .map_err(|e| match e { @@ -115,13 +95,13 @@ pub fn digraph_is_isomorphic( _ => unreachable!(), })?, (None, Some(edge_matcher)) => vf2::is_isomorphic( - &first.graph, - &second.graph, + first, + second, NoSemanticMatch, edge_matcher, id_order, - Ordering::Equal, - true, + ordering, + induced, call_limit, ) .map_err(|e| match e { @@ -129,13 +109,13 @@ pub fn digraph_is_isomorphic( _ => unreachable!(), })?, (None, None) => vf2::is_isomorphic( - &first.graph, - &second.graph, + first, + second, NoSemanticMatch, NoSemanticMatch, id_order, - Ordering::Equal, - true, + ordering, + induced, call_limit, ) .unwrap(), @@ -143,6 +123,70 @@ pub fn digraph_is_isomorphic( Ok(result) } +/// Determine if 2 directed graphs are isomorphic +/// +/// This checks if 2 graphs are isomorphic both structurally and also +/// comparing the node data and edge data using the provided matcher functions. +/// The matcher function takes in 2 data objects and will compare them. A simple +/// example that checks if they're just equal would be:: +/// +/// graph_a = rustworkx.PyDiGraph() +/// graph_b = rustworkx.PyDiGraph() +/// rustworkx.is_isomorphic(graph_a, graph_b, +/// lambda x, y: x == y) +/// +/// .. note:: +/// +/// For better performance on large graphs, consider setting `id_order=False`. +/// +/// :param PyDiGraph first: The first graph to compare +/// :param PyDiGraph second: The second graph to compare +/// :param callable node_matcher: A python callable object that takes 2 positional +/// one for each node data object. If the return of this +/// function evaluates to True then the nodes passed to it are vieded +/// as matching. +/// :param callable edge_matcher: A python callable object that takes 2 positional +/// one for each edge data object. If the return of this +/// function evaluates to True then the edges passed to it are vieded +/// as matching. +/// :param bool id_order: If set to ``False`` this function will use a +/// heuristic matching order based on [VF2]_ paper. Otherwise it will +/// default to matching the nodes in order specified by their ids. +/// :param int call_limit: An optional bound on the number of states that VF2 algorithm +/// visits while searching for a solution. If it exceeds this limit, the algorithm +/// will stop and return ``False``. +/// +/// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are +/// not. +/// :rtype: bool +#[pyfunction] +#[pyo3( + signature=(first, second, node_matcher=None, edge_matcher=None, id_order=true, call_limit=None), + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, call_limit=None)" +)] +pub fn digraph_is_isomorphic( + py: Python, + first: &digraph::PyDiGraph, + second: &digraph::PyDiGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + call_limit: Option, +) -> PyResult { + is_isomorphic( + py, + &first.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + Ordering::Equal, + true, + call_limit, + ) +} + /// Determine if 2 undirected graphs are isomorphic /// /// This checks if 2 graphs are isomorphic both structurally and also @@ -194,18 +238,17 @@ pub fn graph_is_isomorphic( id_order: bool, call_limit: Option, ) -> PyResult { - // vf2::is_isomorphic( - // py, - // &first.graph, - // &second.graph, - // node_matcher, - // edge_matcher, - // id_order, - // Ordering::Equal, - // true, - // call_limit, - // ) - todo!() + is_isomorphic( + py, + &first.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + Ordering::Equal, + true, + call_limit, + ) } /// Determine if 2 directed graphs are subgraph - isomorphic @@ -267,18 +310,17 @@ pub fn digraph_is_subgraph_isomorphic( induced: bool, call_limit: Option, ) -> PyResult { - // vf2::is_isomorphic( - // py, - // &first.graph, - // &second.graph, - // node_matcher, - // edge_matcher, - // id_order, - // Ordering::Greater, - // induced, - // call_limit, - // ) - todo!() + is_isomorphic( + py, + &first.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + Ordering::Greater, + induced, + call_limit, + ) } /// Determine if 2 undirected graphs are subgraph - isomorphic @@ -340,18 +382,17 @@ pub fn graph_is_subgraph_isomorphic( induced: bool, call_limit: Option, ) -> PyResult { - // vf2::is_isomorphic( - // py, - // &first.graph, - // &second.graph, - // node_matcher, - // edge_matcher, - // id_order, - // Ordering::Greater, - // induced, - // call_limit, - // ) - todo!() + is_isomorphic( + py, + &first.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + Ordering::Greater, + induced, + call_limit, + ) } /// Return an iterator over all vf2 mappings between two :class:`~rustworkx.PyDiGraph` objects From b4002a53c8551d22509573c762f4cada414fc4a2 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Sat, 29 Jun 2024 23:42:12 -0400 Subject: [PATCH 07/15] Port VF2Mapping stuff. --- rustworkx-core/src/isomorphism/mod.rs | 4 +- rustworkx-core/src/isomorphism/vf2.rs | 87 ++- src/isomorphism/mod.rs | 20 +- src/isomorphism/vf2_mapping.rs | 1030 ++----------------------- 4 files changed, 155 insertions(+), 986 deletions(-) diff --git a/rustworkx-core/src/isomorphism/mod.rs b/rustworkx-core/src/isomorphism/mod.rs index 00d5212c7..25d5073ce 100644 --- a/rustworkx-core/src/isomorphism/mod.rs +++ b/rustworkx-core/src/isomorphism/mod.rs @@ -10,8 +10,6 @@ // License for the specific language governing permissions and limitations // under the License. -#![allow(clippy::too_many_arguments)] +//! Module for graph isomorphism. pub mod vf2; - -use std::cmp::Ordering; diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index a4846c4c7..bf172a917 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -317,8 +317,8 @@ where } #[derive(Debug)] -struct Vf2State { - graph: G, +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, @@ -472,7 +472,7 @@ pub struct NoSemanticMatch; pub trait NodeMatcher { type Error; - fn enabled() -> bool; + fn enabled(&self) -> bool; fn eq( &mut self, _g0: &G0, @@ -485,7 +485,7 @@ pub trait NodeMatcher { impl NodeMatcher for NoSemanticMatch { type Error = Infallible; #[inline] - fn enabled() -> bool { + fn enabled(&self) -> bool { false } #[inline] @@ -508,7 +508,7 @@ where { type Error = E; #[inline] - fn enabled() -> bool { + fn enabled(&self) -> bool { true } #[inline] @@ -529,7 +529,7 @@ where pub trait EdgeMatcher { type Error; - fn enabled() -> bool; + fn enabled(&self) -> bool; fn eq( &mut self, _g0: &G0, @@ -542,7 +542,7 @@ pub trait EdgeMatcher { impl EdgeMatcher for NoSemanticMatch { type Error = Infallible; #[inline] - fn enabled() -> bool { + fn enabled(&self) -> bool { false } #[inline] @@ -567,7 +567,7 @@ where { type Error = E; #[inline] - fn enabled() -> bool { + fn enabled(&self) -> bool { true } #[inline] @@ -639,10 +639,12 @@ where let mut vf2 = Vf2Algorithm::new( g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, ); - if vf2.next()?.is_some() { - return Ok(true); + + match vf2.next() { + Some(Ok(_)) => Ok(true), + Some(Err(e)) => Err(e), + None => Ok(false), } - Ok(false) } #[derive(Copy, Clone, PartialEq, Debug)] @@ -659,16 +661,16 @@ enum Frame { Unwind { nodes: [N; 2], open_list: OpenList }, } -struct Vf2Algorithm +pub struct Vf2Algorithm where G0: GraphBase, G1: GraphBase, NM: NodeMatcher, EM: EdgeMatcher, { - st: (Vf2State, Vf2State), - node_match: NM, - edge_match: EM, + pub st: (Vf2State, Vf2State), + pub node_match: NM, + pub edge_match: EM, ordering: Ordering, induced: bool, node_map_g0: HashMap, @@ -1031,15 +1033,15 @@ where } } // semantic feasibility: compare associated data for nodes - if NM::enabled() + if node_match.enabled() && !node_match .eq(&st.0.graph, &st.1.graph, nodes[0], nodes[1]) - .map_err(|e| IsIsomorphicError::NodeMatcherErr(e))? + .map_err(IsIsomorphicError::NodeMatcherErr)? { return Ok(false); } // semantic feasibility: compare associated data for edges - if EM::enabled() { + if edge_match.enabled() { let mut matcher = |a: (NodeIndex, (NodeIndex, NodeIndex)), b: (NodeIndex, (NodeIndex, NodeIndex))| @@ -1049,7 +1051,7 @@ where if nx == mx && edge_match .eq(&st.0.graph, &st.1.graph, n_edge, m_edge) - .map_err(|e| IsIsomorphicError::EdgeMatcherErr(e))? + .map_err(IsIsomorphicError::EdgeMatcherErr)? { return Ok(true); } @@ -1228,11 +1230,33 @@ where } 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, - ) -> Result>, IsIsomorphicError> { + fn next(&mut self) -> Option { if (self .st .0 @@ -1250,7 +1274,7 @@ where .then(self.ordering) != self.ordering) { - return Ok(None); + return None; } // A "depth first" search of a valid mapping from graph 1 to graph 2 @@ -1281,7 +1305,7 @@ where match Vf2Algorithm::::next_candidate(&mut self.st) { None => { if self.st.1.is_complete() { - return Ok(Some(self.mapping())); + return Some(Ok(self.mapping())); } continue; } @@ -1298,14 +1322,21 @@ where nodes, open_list: ol, } => { - if Vf2Algorithm::::is_feasible( + 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 @@ -1326,7 +1357,7 @@ where self._counter += 1; if let Some(limit) = self.call_limit { if self._counter > limit { - return Ok(None); + return None; } } let f0 = Frame::Unwind { @@ -1353,6 +1384,6 @@ where } } } - Ok(None) + None } } diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 06302df37..4576154e7 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -15,7 +15,6 @@ mod vf2_mapping; use crate::{digraph, graph}; -use rustworkx_core::isomorphism::vf2; use petgraph::data::{Create, DataMap}; use petgraph::prelude::*; @@ -23,11 +22,12 @@ 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; -use rustworkx_core::isomorphism::vf2::{IsIsomorphicError, NoSemanticMatch}; fn is_isomorphic( py: Python, @@ -77,27 +77,27 @@ where call_limit, ) .map_err(|e| match e { - IsIsomorphicError::NodeMatcherErr(e) => e, - IsIsomorphicError::EdgeMatcherErr(e) => e, + vf2::IsIsomorphicError::NodeMatcherErr(e) => e, + vf2::IsIsomorphicError::EdgeMatcherErr(e) => e, })?, (Some(node_matcher), None) => vf2::is_isomorphic( first, second, node_matcher, - NoSemanticMatch, + vf2::NoSemanticMatch, id_order, ordering, induced, call_limit, ) .map_err(|e| match e { - IsIsomorphicError::NodeMatcherErr(e) => e, + vf2::IsIsomorphicError::NodeMatcherErr(e) => e, _ => unreachable!(), })?, (None, Some(edge_matcher)) => vf2::is_isomorphic( first, second, - NoSemanticMatch, + vf2::NoSemanticMatch, edge_matcher, id_order, ordering, @@ -105,14 +105,14 @@ where call_limit, ) .map_err(|e| match e { - IsIsomorphicError::EdgeMatcherErr(e) => e, + vf2::IsIsomorphicError::EdgeMatcherErr(e) => e, _ => unreachable!(), })?, (None, None) => vf2::is_isomorphic( first, second, - NoSemanticMatch, - NoSemanticMatch, + vf2::NoSemanticMatch, + vf2::NoSemanticMatch, id_order, ordering, induced, diff --git a/src/isomorphism/vf2_mapping.rs b/src/isomorphism/vf2_mapping.rs index f3e77c7c1..18ccc7ddd 100644 --- a/src/isomorphism/vf2_mapping.rs +++ b/src/isomorphism/vf2_mapping.rs @@ -15,962 +15,86 @@ // 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::cmp::Ordering; 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::visit::EdgeRef; use petgraph::EdgeType; -use petgraph::{Directed, Incoming, Outgoing, Undirected}; - -use rayon::slice::ParallelSliceMut; +use petgraph::{Directed, Outgoing, Undirected}; 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); +use rustworkx_core::isomorphism::vf2; - 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 - }; +struct PyMatcher(Option); - let mut seen: Vec = vec![false; n]; +impl vf2::NodeMatcher, StablePyGraph> for PyMatcher { + type Error = PyErr; - // 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; - } - } - } + fn enabled(&self) -> bool { + self.0.is_some() } - /// 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; - } + 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) } } - - /// 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 vf2::EdgeMatcher, StablePyGraph> for PyMatcher { + type Error = PyErr; -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); + self.0.is_some() } - 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, + fn eq( + &mut self, 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); - } - } - } + e0: (NodeIndex, NodeIndex), + e1: (NodeIndex, NodeIndex), + ) -> Result { + let w0 = g0 + .edges_directed(e0.0, Outgoing) + .find(|edge| edge.target() == e0.1) + .and_then(|edge| g0.edge_weight(edge.id())); + let w1 = g1 + .edges_directed(e1.0, Outgoing) + .find(|edge| edge.target() == e1.1) + .and_then(|edge| g1.edge_weight(edge.id())); + 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) } - Ok(None) } } @@ -978,12 +102,12 @@ macro_rules! vf2_mapping_impl { ($name:ident, $Ty:ty) => { #[pyclass(module = "rustworkx")] pub struct $name { - vf2: Vf2Algorithm<$Ty, Option, Option>, + vf2: vf2::Vf2Algorithm, StablePyGraph<$Ty>, PyMatcher, PyMatcher>, } impl $name { pub fn new( - py: Python, + _py: Python, g0: &StablePyGraph<$Ty>, g1: &StablePyGraph<$Ty>, node_match: Option, @@ -993,8 +117,15 @@ macro_rules! vf2_mapping_impl { induced: bool, call_limit: Option, ) -> Self { - let vf2 = Vf2Algorithm::new( - py, g0, g1, node_match, edge_match, id_order, ordering, induced, call_limit, + let vf2 = vf2::Vf2Algorithm::new( + g0, + g1, + PyMatcher(node_match), + PyMatcher(edge_match), + id_order, + ordering, + induced, + call_limit, ); $name { vf2 } } @@ -1007,35 +138,44 @@ macro_rules! vf2_mapping_impl { } fn __next__(mut slf: PyRefMut) -> PyResult> { - Python::with_gil(|py| match slf.vf2.next(py)? { - Some(mapping) => Ok(Some(mapping)), + 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 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)?; - } + 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 { + if let Some(ref obj) = self.vf2.node_match.0 { visit.call(obj)?; } - if let Some(ref obj) = self.vf2.edge_match { + 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[1].graph = StablePyGraph::<$Ty>::default(); - self.vf2.node_match = None; - self.vf2.edge_match = None; + 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; } } }; From a57fd907e978ed81aa73b3ad9ccd0cedae349452 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 1 Jul 2024 03:10:27 -0400 Subject: [PATCH 08/15] Fix edge match regression. --- rustworkx-core/src/isomorphism/vf2.rs | 199 +++++++++++++++++--------- src/isomorphism/vf2_mapping.rs | 21 +-- 2 files changed, 136 insertions(+), 84 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index bf172a917..220d130a6 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -39,9 +39,9 @@ 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: &[T], ys: &[T], matcher: &mut F) -> Result +fn is_subset(xs: &[T1], ys: &[T2], matcher: &mut F) -> Result where - F: FnMut(T, T) -> Result, + F: FnMut(T1, T2) -> Result, { let mut valid = vec![true; ys.len()]; for &a in xs { @@ -124,7 +124,7 @@ trait NodeSorter where G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, G::NodeWeight: Clone, - G::EdgeWeight: Clone, + G::EdgeWeight: Clone + Debug, { type OutputGraph: GraphBase + Create @@ -178,7 +178,7 @@ where + Data + Create, G::NodeWeight: Clone, - G::EdgeWeight: Clone, + G::EdgeWeight: Clone + Debug, { type OutputGraph = G::Target; fn sort(&self, graph: G) -> Vec { @@ -210,7 +210,7 @@ where + Data + Create, G::NodeWeight: Clone, - G::EdgeWeight: Clone, + G::EdgeWeight: Clone + Debug, { type OutputGraph = G::Target; fn sort(&self, graph: G) -> Vec { @@ -534,8 +534,8 @@ pub trait EdgeMatcher { &mut self, _g0: &G0, _g1: &G1, - e0: (G0::NodeId, G0::NodeId), - e1: (G1::NodeId, G1::NodeId), + e0: G0::EdgeId, + e1: G1::EdgeId, ) -> Result; } @@ -550,8 +550,8 @@ impl EdgeMatcher for NoSemanticMatch { &mut self, _g0: &G0, _g1: &G1, - _e0: (G0::NodeId, G0::NodeId), - _e1: (G1::NodeId, G1::NodeId), + _e0: G0::EdgeId, + _e1: G1::EdgeId, ) -> Result { Ok(true) } @@ -560,9 +560,7 @@ impl EdgeMatcher for NoSemanticMatch { impl EdgeMatcher for F where G0: GraphBase + DataMap, - for<'a> &'a G0: GraphBase + IntoEdgesDirected, G1: GraphBase + DataMap, - for<'a> &'a G1: GraphBase + IntoEdgesDirected, F: FnMut(&G0::EdgeWeight, &G1::EdgeWeight) -> Result, { type Error = E; @@ -575,18 +573,10 @@ where &mut self, g0: &G0, g1: &G1, - e0: (G0::NodeId, G0::NodeId), - e1: (G1::NodeId, G1::NodeId), + e0: G0::EdgeId, + e1: G1::EdgeId, ) -> Result { - let w0 = g0 - .edges_directed(e0.0, Outgoing) - .find(|edge| edge.target() == e0.1) - .and_then(|edge| g0.edge_weight(edge.id())); - let w1 = g1 - .edges_directed(e1.0, Outgoing) - .find(|edge| edge.target() == e1.1) - .and_then(|edge| g1.edge_weight(edge.id())); - if let (Some(x), Some(y)) = (w0, w1) { + if let (Some(x), Some(y)) = (g0.edge_weight(e0), g1.edge_weight(e1)) { self(x, y) } else { Ok(false) @@ -612,21 +602,21 @@ pub fn is_isomorphic( ) -> Result> where G0: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, - for<'a> &'a G0: GraphBase + for<'a> &'a G0: GraphBase + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, G0::NodeWeight: Clone, - G0::EdgeWeight: Clone, + G0::EdgeWeight: Clone + Debug, G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, - for<'a> &'a G1: GraphBase + for<'a> &'a G1: GraphBase + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, G1::NodeWeight: Clone, - G1::EdgeWeight: Clone, + G1::EdgeWeight: Clone + Debug, NM: NodeMatcher, EM: EdgeMatcher, { @@ -663,8 +653,10 @@ enum Frame { pub struct Vf2Algorithm where - G0: GraphBase, - G1: GraphBase, + G0: GraphBase + Data, + G1: GraphBase + Data, + G0::EdgeWeight: Debug, + G1::EdgeWeight: Debug, NM: NodeMatcher, EM: EdgeMatcher, { @@ -683,21 +675,21 @@ where impl Vf2Algorithm where G0: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, - for<'a> &'a G0: GraphBase + for<'a> &'a G0: GraphBase + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, G0::NodeWeight: Clone, - G0::EdgeWeight: Clone, + G0::EdgeWeight: Clone + Debug, G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, - for<'a> &'a G1: GraphBase + for<'a> &'a G1: GraphBase + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, G1::NodeWeight: Clone, - G1::EdgeWeight: Clone, + G1::EdgeWeight: Clone + Debug, NM: NodeMatcher, EM: EdgeMatcher, { @@ -1043,14 +1035,14 @@ where // semantic feasibility: compare associated data for edges if edge_match.enabled() { let mut matcher = - |a: (NodeIndex, (NodeIndex, NodeIndex)), - b: (NodeIndex, (NodeIndex, NodeIndex))| + |g0_edge: (NodeIndex, G0::EdgeId), + g1_edge: (NodeIndex, G1::EdgeId)| -> Result> { - let (nx, n_edge) = a; - let (mx, m_edge) = b; + let (nx, e0) = g0_edge; + let (mx, e1) = g1_edge; if nx == mx && edge_match - .eq(&st.0.graph, &st.1.graph, n_edge, m_edge) + .eq(&st.0.graph, &st.1.graph, e0, e1) .map_err(IsIsomorphicError::EdgeMatcherErr)? { return Ok(true); @@ -1058,9 +1050,19 @@ where 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, (NodeIndex, NodeIndex))> = + let e_first: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges(nodes[0]) .filter_map(|edge| { @@ -1073,21 +1075,21 @@ where if m_neigh == end { return None; } - Some((m_neigh, (edge.source(), edge.target()))) + Some((m_neigh, edge.id())) }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_second: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges(nodes[1]) - .map(|edge| (edge.target(), (edge.source(), edge.target()))) + .map(|edge| (edge.target(), edge.id())) .collect(); if !is_subset(&e_first, &e_second, &mut matcher)? { return Ok(false); }; - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_first: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges(nodes[1]) .filter_map(|edge| { @@ -1100,21 +1102,22 @@ where if m_neigh == end { return None; } - Some((m_neigh, (edge.source(), edge.target()))) + Some((m_neigh, edge.id())) }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_second: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges(nodes[0]) - .map(|edge| (edge.target(), (edge.source(), edge.target()))) + .map(|edge| (edge.target(), edge.id())) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher)? { + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { return Ok(false); }; } else { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + println!("[1117]"); + let e_first: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges(nodes[1]) .filter_map(|edge| { @@ -1127,17 +1130,22 @@ where if m_neigh == end { return None; } - Some((m_neigh, (edge.source(), edge.target()))) + println!("[1131] First edge: {:?}", (edge.source(), edge.target(), edge.weight())); + Some((m_neigh, edge.id())) }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_second: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges(nodes[0]) - .map(|edge| (edge.target(), (edge.source(), edge.target()))) + .map(|edge| { + println!("[1140] Second edge: {:?}", (edge.source(), edge.target(), edge.weight())); + (edge.target(), edge.id()) + }) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher)? { + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + println!("[1146] NOT SUBSET!"); return Ok(false); }; } @@ -1145,7 +1153,7 @@ where // incoming edges if st.0.graph.is_directed() { if induced { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_first: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges_directed(nodes[0], Incoming) .filter_map(|edge| { @@ -1158,21 +1166,21 @@ where if m_neigh == end { return None; } - Some((m_neigh, (edge.source(), edge.target()))) + Some((m_neigh, edge.id())) }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_second: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges_directed(nodes[1], Incoming) - .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .map(|edge| (edge.source(), edge.id())) .collect(); if !is_subset(&e_first, &e_second, &mut matcher)? { return Ok(false); }; - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_first: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges_directed(nodes[1], Incoming) .filter_map(|edge| { @@ -1185,21 +1193,22 @@ where if m_neigh == end { return None; } - Some((m_neigh, (edge.source(), edge.target()))) + Some((m_neigh, edge.id())) }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = - st.1.graph + let e_second: Vec<(NodeIndex, G0::EdgeId)> = + st.0.graph .edges_directed(nodes[0], Incoming) - .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .map(|edge| (edge.source(), edge.id())) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher)? { + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { return Ok(false); }; } else { - let e_first: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + println!("[1208]"); + let e_first: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges_directed(nodes[1], Incoming) .filter_map(|edge| { @@ -1212,17 +1221,22 @@ where if m_neigh == end { return None; } - Some((m_neigh, (edge.source(), edge.target()))) + println!("[1222] First edge: {:?}", (edge.source(), edge.target(), edge.weight())); + Some((m_neigh, edge.id())) }) .collect(); - let e_second: Vec<(NodeIndex, (NodeIndex, NodeIndex))> = + let e_second: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges_directed(nodes[0], Incoming) - .map(|edge| (edge.source(), (edge.source(), edge.target()))) + .map(|edge| { + println!("[1231] Second edge: {:?}", (edge.source(), edge.target(), edge.weight())); + (edge.source(), edge.id()) + }) .collect(); - if !is_subset(&e_first, &e_second, &mut matcher)? { + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + println!("[1237] NOT SUBSET!"); return Ok(false); }; } @@ -1235,21 +1249,21 @@ where impl Iterator for Vf2Algorithm where G0: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, - for<'a> &'a G0: GraphBase + for<'a> &'a G0: GraphBase + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, G0::NodeWeight: Clone, - G0::EdgeWeight: Clone, + G0::EdgeWeight: Clone + Debug, G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, - for<'a> &'a G1: GraphBase + for<'a> &'a G1: GraphBase + Data + NodeIndexable + IntoEdgesDirected + IntoNodeIdentifiers, G1::NodeWeight: Clone, - G1::EdgeWeight: Clone, + G1::EdgeWeight: Clone + Debug, NM: NodeMatcher, EM: EdgeMatcher, { @@ -1387,3 +1401,48 @@ where None } } + +#[cfg(test)] +mod vf2_test { + use petgraph::prelude::StableGraph; + use super::*; + use petgraph::stable_graph::StableDiGraph; + + #[test] + fn it_works() { + // first = rustworkx.PyGraph() + // first.extend_from_weighted_edge_list([(0, 1, "a"), (1, 2, "b"), (2, 0, "c")]) + // second = rustworkx.PyGraph() + // second.extend_from_weighted_edge_list([(0, 1, "a"), (1, 2, "b")]) + // + // self.assertTrue( + // rustworkx.is_subgraph_isomorphic( + // first, second, induced=False, edge_matcher=lambda x, y: x == y + // ) + // ) + let mut first: StableDiGraph = StableDiGraph::new(); + let mut second: StableDiGraph = StableDiGraph::new(); + first.extend_with_edges([(0, 1, 'a'), (1, 2, 'b'), (2, 0, 'c')]); + second.extend_with_edges([(0, 1, 'a'), (1, 2, 'b')]); + + let node_match = |n1: &usize, n2: &usize| -> Result { + Ok(n1 == n2) + }; + let edge_match = |e1: &char, e2: &char| -> Result { + Ok(e1 == e2) + }; + + let result = is_isomorphic( + &first, + &second, + node_match, + edge_match, + false, + Ordering::Greater, + false, + None, + ) + .expect("Should work."); + assert!(result) + } +} diff --git a/src/isomorphism/vf2_mapping.rs b/src/isomorphism/vf2_mapping.rs index 18ccc7ddd..62b849c73 100644 --- a/src/isomorphism/vf2_mapping.rs +++ b/src/isomorphism/vf2_mapping.rs @@ -12,7 +12,7 @@ #![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 +// 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; @@ -22,10 +22,9 @@ use pyo3::gc::PyVisit; use pyo3::prelude::*; use pyo3::PyTraverseError; -use petgraph::stable_graph::NodeIndex; -use petgraph::visit::EdgeRef; +use petgraph::stable_graph::{EdgeIndex, NodeIndex}; use petgraph::EdgeType; -use petgraph::{Directed, Outgoing, Undirected}; +use petgraph::{Directed, Undirected}; use crate::iterators::NodeMap; use crate::StablePyGraph; @@ -73,17 +72,11 @@ impl vf2::EdgeMatcher, StablePyGraph> for Py &mut self, g0: &StablePyGraph, g1: &StablePyGraph, - e0: (NodeIndex, NodeIndex), - e1: (NodeIndex, NodeIndex), + e0: EdgeIndex, + e1: EdgeIndex, ) -> Result { - let w0 = g0 - .edges_directed(e0.0, Outgoing) - .find(|edge| edge.target() == e0.1) - .and_then(|edge| g0.edge_weight(edge.id())); - let w1 = g1 - .edges_directed(e1.0, Outgoing) - .find(|edge| edge.target() == e1.1) - .and_then(|edge| g1.edge_weight(edge.id())); + 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 From e17c3e7632e0e37bc6d647bea7be2868799cc87f Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 1 Jul 2024 17:16:40 -0400 Subject: [PATCH 09/15] Remove debug printing. --- rustworkx-core/src/isomorphism/vf2.rs | 53 ++++++++------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 220d130a6..3659fefde 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -124,7 +124,7 @@ trait NodeSorter where G: GraphBase + DataMap + NodeCount + EdgeCount + IntoEdgeReferences, G::NodeWeight: Clone, - G::EdgeWeight: Clone + Debug, + G::EdgeWeight: Clone, { type OutputGraph: GraphBase + Create @@ -178,7 +178,7 @@ where + Data + Create, G::NodeWeight: Clone, - G::EdgeWeight: Clone + Debug, + G::EdgeWeight: Clone, { type OutputGraph = G::Target; fn sort(&self, graph: G) -> Vec { @@ -210,7 +210,7 @@ where + Data + Create, G::NodeWeight: Clone, - G::EdgeWeight: Clone + Debug, + G::EdgeWeight: Clone, { type OutputGraph = G::Target; fn sort(&self, graph: G) -> Vec { @@ -608,7 +608,7 @@ where + IntoEdgesDirected + IntoNodeIdentifiers, G0::NodeWeight: Clone, - G0::EdgeWeight: Clone + Debug, + G0::EdgeWeight: Clone, G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, for<'a> &'a G1: GraphBase + Data @@ -616,7 +616,7 @@ where + IntoEdgesDirected + IntoNodeIdentifiers, G1::NodeWeight: Clone, - G1::EdgeWeight: Clone + Debug, + G1::EdgeWeight: Clone, NM: NodeMatcher, EM: EdgeMatcher, { @@ -655,8 +655,6 @@ pub struct Vf2Algorithm where G0: GraphBase + Data, G1: GraphBase + Data, - G0::EdgeWeight: Debug, - G1::EdgeWeight: Debug, NM: NodeMatcher, EM: EdgeMatcher, { @@ -681,7 +679,7 @@ where + IntoEdgesDirected + IntoNodeIdentifiers, G0::NodeWeight: Clone, - G0::EdgeWeight: Clone + Debug, + G0::EdgeWeight: Clone, G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, for<'a> &'a G1: GraphBase + Data @@ -689,7 +687,7 @@ where + IntoEdgesDirected + IntoNodeIdentifiers, G1::NodeWeight: Clone, - G1::EdgeWeight: Clone + Debug, + G1::EdgeWeight: Clone, NM: NodeMatcher, EM: EdgeMatcher, { @@ -864,9 +862,6 @@ where for n_neigh in st.1.graph.neighbors(nodes[1]) { succ_count[1] += 1; - if !induced && 1 == 0 { - continue; - } // 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()] @@ -1054,8 +1049,8 @@ where // when checking G1 subset of G0. #[inline] fn reverse_args(mut f: F) -> impl FnMut(T2, T1) -> R - where - F: FnMut(T1, T2) -> R, + where + F: FnMut(T1, T2) -> R, { move |y, x| f(x, y) } @@ -1116,7 +1111,6 @@ where return Ok(false); }; } else { - println!("[1117]"); let e_first: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges(nodes[1]) @@ -1130,7 +1124,6 @@ where if m_neigh == end { return None; } - println!("[1131] First edge: {:?}", (edge.source(), edge.target(), edge.weight())); Some((m_neigh, edge.id())) }) .collect(); @@ -1138,14 +1131,10 @@ where let e_second: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges(nodes[0]) - .map(|edge| { - println!("[1140] Second edge: {:?}", (edge.source(), edge.target(), edge.weight())); - (edge.target(), edge.id()) - }) + .map(|edge| (edge.target(), edge.id())) .collect(); if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { - println!("[1146] NOT SUBSET!"); return Ok(false); }; } @@ -1207,7 +1196,6 @@ where return Ok(false); }; } else { - println!("[1208]"); let e_first: Vec<(NodeIndex, G1::EdgeId)> = st.1.graph .edges_directed(nodes[1], Incoming) @@ -1221,7 +1209,6 @@ where if m_neigh == end { return None; } - println!("[1222] First edge: {:?}", (edge.source(), edge.target(), edge.weight())); Some((m_neigh, edge.id())) }) .collect(); @@ -1229,14 +1216,10 @@ where let e_second: Vec<(NodeIndex, G0::EdgeId)> = st.0.graph .edges_directed(nodes[0], Incoming) - .map(|edge| { - println!("[1231] Second edge: {:?}", (edge.source(), edge.target(), edge.weight())); - (edge.source(), edge.id()) - }) + .map(|edge| (edge.source(), edge.id())) .collect(); if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { - println!("[1237] NOT SUBSET!"); return Ok(false); }; } @@ -1255,7 +1238,7 @@ where + IntoEdgesDirected + IntoNodeIdentifiers, G0::NodeWeight: Clone, - G0::EdgeWeight: Clone + Debug, + G0::EdgeWeight: Clone, G1: GraphProp + GraphBase + DataMap + Create + NodeCount + EdgeCount, for<'a> &'a G1: GraphBase + Data @@ -1263,7 +1246,7 @@ where + IntoEdgesDirected + IntoNodeIdentifiers, G1::NodeWeight: Clone, - G1::EdgeWeight: Clone + Debug, + G1::EdgeWeight: Clone, NM: NodeMatcher, EM: EdgeMatcher, { @@ -1404,8 +1387,8 @@ where #[cfg(test)] mod vf2_test { - use petgraph::prelude::StableGraph; use super::*; + use petgraph::prelude::StableGraph; use petgraph::stable_graph::StableDiGraph; #[test] @@ -1425,12 +1408,8 @@ mod vf2_test { first.extend_with_edges([(0, 1, 'a'), (1, 2, 'b'), (2, 0, 'c')]); second.extend_with_edges([(0, 1, 'a'), (1, 2, 'b')]); - let node_match = |n1: &usize, n2: &usize| -> Result { - Ok(n1 == n2) - }; - let edge_match = |e1: &char, e2: &char| -> Result { - Ok(e1 == e2) - }; + let node_match = |n1: &usize, n2: &usize| -> Result { Ok(n1 == n2) }; + let edge_match = |e1: &char, e2: &char| -> Result { Ok(e1 == e2) }; let result = is_isomorphic( &first, From c3e445d2e4984a1ea23f2b90796c908a109e566f Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 1 Jul 2024 17:23:37 -0400 Subject: [PATCH 10/15] Simplify redundant code after unrolling. --- rustworkx-core/src/isomorphism/vf2.rs | 154 +++++++++----------------- 1 file changed, 50 insertions(+), 104 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 3659fefde..46f2666b9 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -1083,61 +1083,34 @@ where 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); - }; - } else { - 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_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(); + 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); - }; - } + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; // incoming edges if st.0.graph.is_directed() { @@ -1168,61 +1141,34 @@ where 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); - }; - } else { - 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_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(); + 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); - }; - } + if !is_subset(&e_first, &e_second, &mut reverse_args(&mut matcher))? { + return Ok(false); + }; } } Ok(true) From 080b4848fc13945afbacd54b1f36beadb0a32e03 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 1 Jul 2024 17:42:59 -0400 Subject: [PATCH 11/15] Remove test used for debug. --- rustworkx-core/src/isomorphism/vf2.rs | 41 --------------------------- 1 file changed, 41 deletions(-) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 46f2666b9..154b3abf4 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -1330,44 +1330,3 @@ where None } } - -#[cfg(test)] -mod vf2_test { - use super::*; - use petgraph::prelude::StableGraph; - use petgraph::stable_graph::StableDiGraph; - - #[test] - fn it_works() { - // first = rustworkx.PyGraph() - // first.extend_from_weighted_edge_list([(0, 1, "a"), (1, 2, "b"), (2, 0, "c")]) - // second = rustworkx.PyGraph() - // second.extend_from_weighted_edge_list([(0, 1, "a"), (1, 2, "b")]) - // - // self.assertTrue( - // rustworkx.is_subgraph_isomorphic( - // first, second, induced=False, edge_matcher=lambda x, y: x == y - // ) - // ) - let mut first: StableDiGraph = StableDiGraph::new(); - let mut second: StableDiGraph = StableDiGraph::new(); - first.extend_with_edges([(0, 1, 'a'), (1, 2, 'b'), (2, 0, 'c')]); - second.extend_with_edges([(0, 1, 'a'), (1, 2, 'b')]); - - let node_match = |n1: &usize, n2: &usize| -> Result { Ok(n1 == n2) }; - let edge_match = |e1: &char, e2: &char| -> Result { Ok(e1 == e2) }; - - let result = is_isomorphic( - &first, - &second, - node_match, - edge_match, - false, - Ordering::Greater, - false, - None, - ) - .expect("Should work."); - assert!(result) - } -} From d4ea09f0e8c52335930bdbb0d3c89bc0fe8b6fb6 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 1 Jul 2024 18:24:00 -0400 Subject: [PATCH 12/15] Add release note. --- .../notes/graph-ext-95e9657afce2efc2.yaml | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml diff --git a/rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml b/rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml new file mode 100644 index 000000000..5e93448a3 --- /dev/null +++ b/rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml @@ -0,0 +1,65 @@ +--- +prelude: > + Replace this text with content to appear at the top of the section for this + release. All of the prelude content is merged together and then rendered + separately from the items listed in other parts of the file, so the text + needs to be worded so that both the prelude and the other items make sense + when read independently. This may mean repeating some details. Not every + release note requires a prelude. Usually only notes describing major + features or adding release theme details should have a prelude. +features: + - | + List new features here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +issues: + - | + List known issues here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +upgrade: + - | + List upgrade notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +deprecations: + - | + List deprecations notes here, or remove this section. All of the list + items in this section are combined when the release notes are rendered, so + the text needs to be worded so that it does not depend on any information + only available in another section, such as the prelude. This may mean + repeating some details. +critical: + - | + Add critical notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +security: + - | + Add security notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +fixes: + - | + Add normal bug fixes here, or remove this section. All of the list items + in this section are combined when the release notes are rendered, so the + text needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +other: + - | + Add other notes here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. From 313e4fc92b1dc0f500da88dc49edcc621428ac06 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 1 Jul 2024 18:29:38 -0400 Subject: [PATCH 13/15] Add doc string for Vf2Algorithm. --- rustworkx-core/src/isomorphism/vf2.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rustworkx-core/src/isomorphism/vf2.rs b/rustworkx-core/src/isomorphism/vf2.rs index 154b3abf4..ea4cb05e8 100644 --- a/rustworkx-core/src/isomorphism/vf2.rs +++ b/rustworkx-core/src/isomorphism/vf2.rs @@ -651,6 +651,11 @@ enum Frame { 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, From a993fd6e47325500aca11a9faa1ef11dc0f05db1 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 2 Jul 2024 00:41:25 -0400 Subject: [PATCH 14/15] Add the correct release note. --- .../notes/vf2-core-ccfb903a5cfb34b1.yaml | 12 ++++ .../notes/graph-ext-95e9657afce2efc2.yaml | 65 ------------------- 2 files changed, 12 insertions(+), 65 deletions(-) create mode 100644 releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml delete mode 100644 rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml diff --git a/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml b/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml new file mode 100644 index 000000000..64d7be26a --- /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 + func:`.is_isomorphic` for isomorphism testing in addition to the + ``Vf2Algorithm` struct, which can be used to iterate over valid + isomorphisms 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/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml b/rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml deleted file mode 100644 index 5e93448a3..000000000 --- a/rustworkx-core/releasenotes/notes/graph-ext-95e9657afce2efc2.yaml +++ /dev/null @@ -1,65 +0,0 @@ ---- -prelude: > - Replace this text with content to appear at the top of the section for this - release. All of the prelude content is merged together and then rendered - separately from the items listed in other parts of the file, so the text - needs to be worded so that both the prelude and the other items make sense - when read independently. This may mean repeating some details. Not every - release note requires a prelude. Usually only notes describing major - features or adding release theme details should have a prelude. -features: - - | - List new features here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -issues: - - | - List known issues here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -upgrade: - - | - List upgrade notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -deprecations: - - | - List deprecations notes here, or remove this section. All of the list - items in this section are combined when the release notes are rendered, so - the text needs to be worded so that it does not depend on any information - only available in another section, such as the prelude. This may mean - repeating some details. -critical: - - | - Add critical notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -security: - - | - Add security notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -fixes: - - | - Add normal bug fixes here, or remove this section. All of the list items - in this section are combined when the release notes are rendered, so the - text needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. -other: - - | - Add other notes here, or remove this section. All of the list items in - this section are combined when the release notes are rendered, so the text - needs to be worded so that it does not depend on any information only - available in another section, such as the prelude. This may mean repeating - some details. From 8a19298733282282545cb0917a48676b3b26cb28 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 2 Jul 2024 13:52:07 -0400 Subject: [PATCH 15/15] Fix release note Sphinx issues. --- releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml b/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml index 64d7be26a..01c9f4627 100644 --- a/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml +++ b/releasenotes/notes/vf2-core-ccfb903a5cfb34b1.yaml @@ -2,11 +2,11 @@ features: - | Graph isomorphism using VF2(++) is now supported in ``rustworkx-core`` - for `petgraph` graph types ``StableGraph`` and ``Graph`` (it may also + 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 - func:`.is_isomorphic` for isomorphism testing in addition to the - ``Vf2Algorithm` struct, which can be used to iterate over valid - isomorphisms of two graphs. It also provides traits `EdgeMatcher` - and `NodeMatcher`, which allow a user-provided implementation of + 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.