From abf1dba1f20b72f2933ac576631839d380fab8d5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 17 Jun 2021 15:23:22 -0400 Subject: [PATCH 01/27] Add vf2 mapping functions This commit adds a new function vf2_mapping which is used to get the isomorphic node mapping between 2 graphs. It works in the same way as is_isomorphic() but instead of simply returning a boolean whether the graphs are isomorphic it returns the node id mapping from first to second for the matching. --- docs/source/api.rst | 3 + .../notes/vf2-mapping-6fd49ab8b1b552c2.yaml | 16 ++ retworkx/__init__.py | 79 +++++++++ src/isomorphism.rs | 100 ++++++++++- src/lib.rs | 156 ++++++++++++++++++ tests/digraph/test_isomorphic.py | 74 +++++++++ tests/graph/test_isomorphic.py | 81 +++++++++ 7 files changed, 502 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml diff --git a/docs/source/api.rst b/docs/source/api.rst index 039c168d3c..b12b52c116 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -86,6 +86,7 @@ Isomorphism retworkx.is_isomorphic retworkx.is_subgraph_isomorphic retworkx.is_isomorphic_node_match + retworkx.vf2_mapping .. _matching: @@ -203,6 +204,7 @@ the functions from the explicitly typed based on the data type. retworkx.digraph_is_isomorphic retworkx.digraph_is_subgraph_isomorphic + retworkx.digraph_vf2_mapping retworkx.digraph_distance_matrix retworkx.digraph_floyd_warshall_numpy retworkx.digraph_adjacency_matrix @@ -241,6 +243,7 @@ typed API based on the data type. retworkx.graph_is_isomorphic retworkx.graph_is_subgraph_isomorphic + retworkx.graph_vf2_mapping retworkx.graph_distance_matrix retworkx.graph_floyd_warshall_numpy retworkx.graph_adjacency_matrix diff --git a/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml b/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml new file mode 100644 index 0000000000..ea9e2fb3d8 --- /dev/null +++ b/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Added a new function, :func:`retworkx.vf2_mapping`, which will use the + vf2 isomorphism algorithm (which is also used for + :func:`retworkx.is_isomorphic` and :func:`retworkx.is_subgraph_isomorphic`) + to return an isomorphic mapping between two graphs. For example: + + .. jupyter-execute:: + + import retworkx + + graph = retworkx.generators.directed_grid_graph(10, 10) + other_graph = retworkx.generators.directed_grid_graph(4, 4) + mapping = retworkx.vf2_mapping(graph, other_graph, subgraph=True) + print(mapping) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 547d4a401a..5bfec97bb3 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1259,3 +1259,82 @@ def _graph_spiral_layout( resolution=resolution, equidistant=equidistant, ) + + +@functools.singledispatch +def vf2_mapping( + first, + second, + node_matcher=None, + edge_matcher=None, + id_order=True, + subgraph=False, +): + """ + Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects + + This funcion will run the vf2 algorithm used from + :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` + but instead of returning a boolean it will return the mapping of node ids + found from ``first`` to ``second``. If the graphs are not isomorphic than + ``None`` will be returned. + + :param PyDiGraph first: The first graph to find the mapping for + :param PyDiGraph second: The second graph to find the mapping for + :param node_matcher: An optional python callable object that takes 2 + positional arguments, one for each node data object in either graph. + If the return of this function evaluates to True then the nodes + passed to it are vieded as matching. + :param 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 bool subgraph: If set to ``True`` the function will return the + subgraph isomorphic found between the graphs. + + :returns: A dicitonary of node indices from ``first`` to node indices in + ``second`` representing the mapping found. + :rtype: dict + """ + raise TypeError("Invalid Input Type %s for graph" % type(first)) + + +@vf2_mapping.register(PyDiGraph) +def _digraph_vf2_mapping( + first, + second, + node_matcher=None, + edge_matcher=None, + id_order=True, + subgraph=False, +): + return digraph_vf2_mapping( + first, + second, + node_matcher=node_matcher, + edge_matcher=edge_matcher, + id_order=id_order, + subgraph=subgraph, + ) + + +@vf2_mapping.register(PyGraph) +def _graph_vf2_mapping( + first, + second, + node_matcher=None, + edge_matcher=None, + id_order=True, + subgraph=False, +): + return graph_vf2_mapping( + first, + second, + node_matcher=node_matcher, + edge_matcher=edge_matcher, + id_order=id_order, + subgraph=subgraph, + ) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index ffd9a18b9b..eafc4566b6 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -10,6 +10,7 @@ // License for the specific language governing permissions and limitations // under the License. +#![allow(clippy::too_many_arguments)] // This module is a forked version of petgraph's isomorphism module @ 0.5.0. // It has then been modified to function with PyDiGraph inputs instead of Graph. @@ -47,6 +48,7 @@ where &self, py: Python, graph: &StablePyGraph, + mut mapping: Option<&mut HashMap>, ) -> StablePyGraph { let order = self.sort(graph); @@ -66,7 +68,11 @@ where let c_index = graph.from_index(c); new_graph.add_edge(p_index, c_index, edge_w.clone_ref(py)); } - + if mapping.is_some() { + for (new_index, old_value) in id_map.iter().enumerate() { + mapping.as_mut().unwrap().insert(*old_value, new_index); + } + } new_graph } } @@ -319,7 +325,10 @@ where } } -fn reindex_graph(py: Python, graph: &StablePyGraph) -> StablePyGraph +fn reindex_graph( + py: Python, + graph: &StablePyGraph, +) -> (StablePyGraph, HashMap) where Ty: EdgeType, { @@ -344,7 +353,10 @@ where new_graph.add_edge(p_index, c_index, edge_w.clone_ref(py)); } - new_graph + ( + new_graph, + id_map.iter().map(|(k, v)| (v.index(), k.index())).collect(), + ) } trait SemanticMatcher { @@ -381,6 +393,7 @@ pub fn is_isomorphic( mut edge_match: Option, id_order: bool, ordering: Ordering, + mut mapping: Option<&mut HashMap>, ) -> PyResult where Ty: EdgeType, @@ -389,16 +402,24 @@ where { let mut inner_temp_g0: StablePyGraph; let mut inner_temp_g1: StablePyGraph; + let mut node_map_g0: Option>; + let mut node_map_g1: Option>; let g0_out = if g0.nodes_removed() { - inner_temp_g0 = reindex_graph(py, g0); + let res = reindex_graph(py, g0); + inner_temp_g0 = res.0; + node_map_g0 = Some(res.1); &inner_temp_g0 } else { + node_map_g0 = None; g0 }; let g1_out = if g1.nodes_removed() { - inner_temp_g1 = reindex_graph(py, g1); + let res = reindex_graph(py, g1); + inner_temp_g1 = res.0; + node_map_g1 = Some(res.1); &inner_temp_g1 } else { + node_map_g1 = None; g1 }; @@ -411,14 +432,48 @@ where } let g0 = if !id_order { - inner_temp_g0 = Vf2ppSorter.reorder(py, g0_out); + inner_temp_g0 = if mapping.is_some() { + let mut vf2pp_map: HashMap = + HashMap::with_capacity(g0_out.node_count()); + let temp = Vf2ppSorter.reorder(py, g0_out, Some(&mut vf2pp_map)); + match node_map_g0 { + Some(ref mut g0_map) => { + let mut temp_map = HashMap::with_capacity(g0_map.len()); + for (new_index, old_index) in g0_map.iter_mut() { + temp_map.insert(vf2pp_map[&new_index], *old_index); + } + *g0_map = temp_map; + } + None => node_map_g0 = Some(vf2pp_map), + }; + temp + } else { + Vf2ppSorter.reorder(py, g0_out, None) + }; &inner_temp_g0 } else { g0_out }; let g1 = if !id_order { - inner_temp_g1 = Vf2ppSorter.reorder(py, g1_out); + inner_temp_g1 = if mapping.is_some() { + let mut vf2pp_map: HashMap = + HashMap::with_capacity(g1_out.node_count()); + let temp = Vf2ppSorter.reorder(py, g1_out, Some(&mut vf2pp_map)); + match node_map_g1 { + Some(ref mut g1_map) => { + let mut temp_map = HashMap::with_capacity(g1_map.len()); + for (new_index, old_index) in g1_map.iter_mut() { + temp_map.insert(vf2pp_map[&new_index], *old_index); + } + *g1_map = temp_map; + } + None => node_map_g1 = Some(vf2pp_map), + }; + temp + } else { + Vf2ppSorter.reorder(py, g1_out, None) + }; &inner_temp_g1 } else { g1_out @@ -427,6 +482,37 @@ where let mut st = [Vf2State::new(g0), Vf2State::new(g1)]; let res = try_match(&mut st, g0, g1, &mut node_match, &mut edge_match, ordering)?; + + if mapping.is_some() { + for (index, val) in st[1].mapping.iter().enumerate() { + match node_map_g1 { + Some(ref g1_map) => { + let node_index = g1_map[&index]; + match node_map_g0 { + Some(ref g0_map) => mapping + .as_mut() + .unwrap() + .insert(g0_map[&val.index()], node_index), + None => mapping + .as_mut() + .unwrap() + .insert(val.index(), node_index), + }; + } + None => { + match node_map_g0 { + Some(ref g0_map) => mapping + .as_mut() + .unwrap() + .insert(g0_map[&val.index()], index), + None => { + mapping.as_mut().unwrap().insert(val.index(), index) + } + }; + } + }; + } + } Ok(res.unwrap_or(false)) } diff --git a/src/lib.rs b/src/lib.rs index 2c6a998b14..cf73bf531c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -356,6 +356,7 @@ fn digraph_is_isomorphic( compare_edges, id_order, Ordering::Equal, + None, )?; Ok(res) } @@ -425,6 +426,7 @@ fn graph_is_isomorphic( compare_edges, id_order, Ordering::Equal, + None, )?; Ok(res) } @@ -494,6 +496,7 @@ fn digraph_is_subgraph_isomorphic( compare_edges, id_order, Ordering::Greater, + None, )?; Ok(res) } @@ -563,10 +566,161 @@ fn graph_is_subgraph_isomorphic( compare_edges, id_order, Ordering::Greater, + None, )?; Ok(res) } +/// Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects +/// +/// This funcion will run the vf2 algorithm used from +/// :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` +/// but instead of returning a boolean it will return the mapping of node ids +/// found from ``first`` to ``second``. If the graphs are not isomorphic than +/// ``None`` will be returned. +/// +/// :param PyDiGraph first: The first graph to find the mapping for +/// :param PyDiGraph second: The second graph to find the mapping for +/// :param node_matcher: An optional python callable object that takes 2 +/// positional arguments, one for each node data object in either graph. +/// If the return of this function evaluates to True then the nodes +/// passed to it are vieded as matching. +/// :param 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 bool subgraph: If set to ``True`` the function will return the +/// subgraph isomorphic found between the graphs.// +/// +/// :returns: A dicitonary of node indices from ``first`` to node indices in +/// ``second`` representing the mapping found. +/// :rtype: dict +#[pyfunction(id_order = "true", subgraph = "false")] +fn digraph_vf2_mapping( + py: Python, + first: &digraph::PyDiGraph, + second: &digraph::PyDiGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + subgraph: bool, +) -> PyResult>> { + let compare_nodes = node_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let compare_edges = edge_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + let ordering = if subgraph { + Ordering::Greater + } else { + Ordering::Equal + }; + let mut mapping: HashMap = HashMap::with_capacity( + first.graph.node_count().min(second.graph.node_count()), + ); + let res = isomorphism::is_isomorphic( + py, + &first.graph, + &second.graph, + compare_nodes, + compare_edges, + id_order, + ordering, + Some(&mut mapping), + )?; + if res { + Ok(Some(mapping)) + } else { + Ok(None) + } +} + +/// Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects +/// +/// This funcion will run the vf2 algorithm used from +/// :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` +/// but instead of returning a boolean it will return the mapping of node ids +/// found from ``first`` to ``second``. If the graphs are not isomorphic than +/// ``None`` will be returned. +/// +/// :param PyDiGraph first: The first graph to find the mapping for +/// :param PyDiGraph second: The second graph to find the mapping for +/// :param node_matcher: An optional python callable object that takes 2 +/// positional arguments, one for each node data object in either graph. +/// If the return of this function evaluates to True then the nodes +/// passed to it are vieded as matching. +/// :param 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 bool subgraph: If set to ``True`` the function will return the +/// subgraph isomorphic found between the graphs. +/// +/// :returns: A dicitonary of node indices from ``first`` to node indices in +/// ``second`` representing the mapping found. +/// :rtype: dict +#[pyfunction(id_order = "true", subgraph = "false")] +fn graph_vf2_mapping( + py: Python, + first: &graph::PyGraph, + second: &graph::PyGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + subgraph: bool, +) -> PyResult>> { + let compare_nodes = node_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let compare_edges = edge_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + let ordering = if subgraph { + Ordering::Greater + } else { + Ordering::Equal + }; + let mut mapping: HashMap = HashMap::with_capacity( + first.graph.node_count().min(second.graph.node_count()), + ); + let res = isomorphism::is_isomorphic( + py, + &first.graph, + &second.graph, + compare_nodes, + compare_edges, + id_order, + ordering, + Some(&mut mapping), + )?; + if res { + Ok(Some(mapping)) + } else { + Ok(None) + } +} + /// Return the topological sort of node indexes from the provided graph /// /// :param PyDiGraph graph: The DAG to get the topological sort on @@ -4478,6 +4632,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(graph_is_isomorphic))?; m.add_wrapped(wrap_pyfunction!(digraph_is_subgraph_isomorphic))?; m.add_wrapped(wrap_pyfunction!(graph_is_subgraph_isomorphic))?; + m.add_wrapped(wrap_pyfunction!(digraph_vf2_mapping))?; + m.add_wrapped(wrap_pyfunction!(graph_vf2_mapping))?; m.add_wrapped(wrap_pyfunction!(digraph_union))?; m.add_wrapped(wrap_pyfunction!(topological_sort))?; m.add_wrapped(wrap_pyfunction!(descendants))?; diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index 3bb74fa1da..1872b88934 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -222,3 +222,77 @@ def test_isomorphic_compare_nodes_with_removals_deepcopy(self): id_order=id_order, ) ) + + def test_digraph_vf2_mapping_identical(self): + graph = retworkx.generators.directed_grid_graph(2, 2) + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping(graph, second_graph) + self.assertEqual(mapping, {0: 0, 1: 1, 2: 2, 3: 3}) + + def test_digraph_vf2_mapping_identical_removals(self): + graph = retworkx.generators.directed_path_graph(2) + second_graph = retworkx.generators.directed_path_graph(4) + second_graph.remove_nodes_from([1, 2]) + second_graph.add_edge(0, 3, None) + mapping = retworkx.digraph_vf2_mapping(graph, second_graph) + self.assertEqual({0: 0, 1: 3}, mapping) + + def test_digraph_vf2_mapping_identical_removals_first(self): + second_graph = retworkx.generators.directed_path_graph(2) + graph = retworkx.generators.directed_path_graph(4) + graph.remove_nodes_from([1, 2]) + graph.add_edge(0, 3, None) + mapping = retworkx.digraph_vf2_mapping(graph, second_graph) + self.assertEqual({0: 0, 3: 1}, mapping) + + def test_subgraph_vf2_mapping(self): + graph = retworkx.generators.directed_grid_graph(10, 10) + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True + ) + self.assertEqual(mapping, {0: 0, 1: 1, 10: 2, 11: 3}) + + def test_digraph_vf2_mapping_identical_vf2pp(self): + graph = retworkx.generators.directed_grid_graph(2, 2) + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, id_order=False + ) + valid_mappings = [ + {0: 0, 1: 1, 2: 2, 3: 3}, + {0: 0, 1: 2, 2: 1, 3: 3}, + ] + self.assertIn(mapping, valid_mappings) + + def test_graph_vf2_mapping_identical_removals_vf2pp(self): + graph = retworkx.generators.directed_path_graph(2) + second_graph = retworkx.generators.directed_path_graph(4) + second_graph.remove_nodes_from([1, 2]) + second_graph.add_edge(0, 3, None) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, id_order=False + ) + self.assertEqual({0: 0, 1: 3}, mapping) + + def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): + second_graph = retworkx.generators.directed_path_graph(2) + graph = retworkx.generators.directed_path_graph(4) + graph.remove_nodes_from([1, 2]) + graph.add_edge(0, 3, None) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, id_order=False + ) + self.assertEqual({0: 0, 3: 1}, mapping) + + def test_subgraph_vf2_mapping_vf2pp(self): + graph = retworkx.generators.directed_grid_graph(3, 3) + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + valid_mappings = [ + {8: 3, 5: 2, 7: 1, 4: 0}, + {7: 2, 5: 1, 4: 0, 8: 3}, + ] + self.assertIn(mapping, valid_mappings) diff --git a/tests/graph/test_isomorphic.py b/tests/graph/test_isomorphic.py index d7fcccf9a3..df65bfd4c1 100644 --- a/tests/graph/test_isomorphic.py +++ b/tests/graph/test_isomorphic.py @@ -228,3 +228,84 @@ def test_same_degrees_non_isomorphic(self): self.assertFalse( retworkx.is_isomorphic(g_a, g_b, id_order=id_order) ) + + def test_graph_vf2_mapping_identical(self): + graph = retworkx.generators.grid_graph(2, 2) + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping(graph, second_graph) + self.assertEqual(mapping, {0: 0, 1: 1, 2: 2, 3: 3}) + + def test_graph_vf2_mapping_identical_removals(self): + graph = retworkx.generators.path_graph(2) + second_graph = retworkx.generators.path_graph(4) + second_graph.remove_nodes_from([1, 2]) + second_graph.add_edge(0, 3, None) + mapping = retworkx.graph_vf2_mapping(graph, second_graph) + self.assertEqual({0: 0, 1: 3}, mapping) + + def test_graph_vf2_mapping_identical_removals_first(self): + second_graph = retworkx.generators.path_graph(2) + graph = retworkx.generators.path_graph(4) + graph.remove_nodes_from([1, 2]) + graph.add_edge(0, 3, None) + mapping = retworkx.graph_vf2_mapping( + graph, + second_graph, + ) + self.assertEqual({0: 0, 3: 1}, mapping) + + def test_subgraph_vf2_mapping(self): + graph = retworkx.generators.grid_graph(10, 10) + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping(graph, second_graph, subgraph=True) + self.assertEqual(mapping, {0: 0, 1: 1, 10: 2, 11: 3}) + + def test_graph_vf2_mapping_identical_vf2pp(self): + graph = retworkx.generators.grid_graph(2, 2) + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, id_order=False + ) + valid_mappings = [ + {0: 0, 1: 1, 2: 2, 3: 3}, + {0: 0, 1: 2, 2: 1, 3: 3}, + ] + self.assertIn(mapping, valid_mappings) + + def test_graph_vf2_mapping_identical_removals_vf2pp(self): + graph = retworkx.generators.path_graph(2) + second_graph = retworkx.generators.path_graph(4) + second_graph.remove_nodes_from([1, 2]) + second_graph.add_edge(0, 3, None) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, id_order=False + ) + self.assertEqual({0: 0, 1: 3}, mapping) + + def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): + second_graph = retworkx.generators.path_graph(2) + graph = retworkx.generators.path_graph(4) + graph.remove_nodes_from([1, 2]) + graph.add_edge(0, 3, None) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, id_order=False + ) + self.assertEqual({0: 0, 3: 1}, mapping) + + def test_subgraph_vf2_mapping_vf2pp(self): + graph = retworkx.generators.grid_graph(3, 3) + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + valid_mappings = [ + {3: 2, 4: 3, 6: 0, 7: 1}, + {3: 1, 4: 3, 6: 0, 7: 2}, + {4: 3, 5: 1, 7: 2, 8: 0}, + {0: 0, 1: 1, 3: 2, 4: 3}, + {7: 1, 8: 0, 4: 3, 5: 2}, + {5: 1, 2: 0, 1: 2, 4: 3}, + {3: 1, 0: 0, 4: 3, 1: 2}, + {1: 1, 2: 0, 4: 3, 5: 2}, + ] + self.assertIn(mapping, valid_mappings) From 4301835047e0d540af412023a53286dc08a9d188 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 21 Jun 2021 09:22:53 -0400 Subject: [PATCH 02/27] Fix reverse mapping construction with vf2pp mapping This commit fixes the construction of the reverse map when id_order=False. The previous versions of this did not correctly build the map in all cases. Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- src/isomorphism.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index eafc4566b6..2b64e485b2 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -69,8 +69,8 @@ where new_graph.add_edge(p_index, c_index, edge_w.clone_ref(py)); } if mapping.is_some() { - for (new_index, old_value) in id_map.iter().enumerate() { - mapping.as_mut().unwrap().insert(*old_value, new_index); + for (old_value, new_index) in id_map.iter().enumerate() { + mapping.as_mut().unwrap().insert(*new_index, old_value); } } new_graph @@ -438,11 +438,10 @@ where let temp = Vf2ppSorter.reorder(py, g0_out, Some(&mut vf2pp_map)); match node_map_g0 { Some(ref mut g0_map) => { - let mut temp_map = HashMap::with_capacity(g0_map.len()); - for (new_index, old_index) in g0_map.iter_mut() { - temp_map.insert(vf2pp_map[&new_index], *old_index); + for (_, old_index) in vf2pp_map.iter_mut() { + *old_index = g0_map[old_index]; } - *g0_map = temp_map; + *g0_map = vf2pp_map; } None => node_map_g0 = Some(vf2pp_map), }; @@ -462,11 +461,10 @@ where let temp = Vf2ppSorter.reorder(py, g1_out, Some(&mut vf2pp_map)); match node_map_g1 { Some(ref mut g1_map) => { - let mut temp_map = HashMap::with_capacity(g1_map.len()); - for (new_index, old_index) in g1_map.iter_mut() { - temp_map.insert(vf2pp_map[&new_index], *old_index); + for (_, old_index) in vf2pp_map.iter_mut() { + *old_index = g1_map[old_index]; } - *g1_map = temp_map; + *g1_map = vf2pp_map; } None => node_map_g1 = Some(vf2pp_map), }; From f1960d73432fe8408a4634c6885cca4e0e7ae9fa Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 21 Jun 2021 10:17:53 -0400 Subject: [PATCH 03/27] Add vf2pp remapping failure test case --- tests/digraph/test_isomorphic.py | 19 +++++++++++++++++++ tests/graph/test_isomorphic.py | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index 1872b88934..0e71034cf7 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -296,3 +296,22 @@ def test_subgraph_vf2_mapping_vf2pp(self): {7: 2, 5: 1, 4: 0, 8: 3}, ] self.assertIn(mapping, valid_mappings) + + def test_vf2pp_remapping(self): + temp = retworkx.generators.directed_grid_graph(3, 3) + + graph = retworkx.PyDiGraph() + dummy = graph.add_node(0) + + graph.compose(temp, dict()) + graph.remove_node(dummy) + + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + expected_mappings = [ + {6: 1, 5: 0, 8: 2, 9: 3}, + {6: 2, 5: 0, 9: 3, 8: 1}, + ] + self.assertIn(mapping, expected_mappings) diff --git a/tests/graph/test_isomorphic.py b/tests/graph/test_isomorphic.py index df65bfd4c1..8bc3c0fe04 100644 --- a/tests/graph/test_isomorphic.py +++ b/tests/graph/test_isomorphic.py @@ -309,3 +309,28 @@ def test_subgraph_vf2_mapping_vf2pp(self): {1: 1, 2: 0, 4: 3, 5: 2}, ] self.assertIn(mapping, valid_mappings) + + def test_vf2pp_remapping(self): + temp = retworkx.generators.grid_graph(3, 3) + + graph = retworkx.PyGraph() + dummy = graph.add_node(0) + + graph.compose(temp, dict()) + graph.remove_node(dummy) + + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + expected_mappings = [ + {2: 2, 3: 0, 5: 3, 6: 1}, + {5: 3, 6: 1, 8: 2, 9: 0}, + {2: 1, 5: 3, 6: 2, 3: 0}, + {2: 2, 1: 0, 5: 3, 4: 1}, + {5: 3, 6: 2, 8: 1, 9: 0}, + {4: 2, 1: 0, 5: 3, 2: 1}, + {5: 3, 8: 1, 4: 2, 7: 0}, + {5: 3, 4: 1, 7: 0, 8: 2}, + ] + self.assertIn(mapping, expected_mappings) From 815c925e87cd0de834f0c2109861ba1d4e508e8d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 23 Jun 2021 07:55:09 -0400 Subject: [PATCH 04/27] Don't remap indices if no match found --- src/isomorphism.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index 2b64e485b2..fbcdc13f6b 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -481,7 +481,7 @@ where let res = try_match(&mut st, g0, g1, &mut node_match, &mut edge_match, ordering)?; - if mapping.is_some() { + if mapping.is_some() && res == Some(true) { for (index, val) in st[1].mapping.iter().enumerate() { match node_map_g1 { Some(ref g1_map) => { From 1962edf118d112effa97ee108925de1c0654b1d6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Jun 2021 08:14:16 -0400 Subject: [PATCH 05/27] Fix clippy failures --- src/isomorphism.rs | 1 - src/lib.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index 1977e78a37..419daebcf4 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -385,7 +385,6 @@ where /// graph isomorphism (graph structure and matching node and edge weights). /// /// The graphs should not be multigraphs. -#[allow(clippy::too_many_arguments)] pub fn is_isomorphic( py: Python, g0: &StablePyGraph, diff --git a/src/lib.rs b/src/lib.rs index 100937bbc3..228a4620e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ // under the License. #![allow(clippy::float_cmp)] +#![allow(clippy::too_many_arguments)] mod astar; mod digraph; From 43e8aa0c91cfd5b6b1015e5889f05456d708c66a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Jun 2021 17:05:25 -0400 Subject: [PATCH 06/27] Use NodeMap for return type --- retworkx/__init__.py | 2 +- src/lib.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index e833982a26..bb2dd4c259 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1323,7 +1323,7 @@ def vf2_mapping( :returns: A dicitonary of node indices from ``first`` to node indices in ``second`` representing the mapping found. - :rtype: dict + :rtype: NodeMap """ raise TypeError("Invalid Input Type %s for graph" % type(first)) diff --git a/src/lib.rs b/src/lib.rs index b2b18ebbfe..5c0564dbb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ use rayon::prelude::*; use crate::generators::PyInit_generators; use crate::iterators::{ - AllPairsPathLengthMapping, AllPairsPathMapping, EdgeList, NodeIndices, + AllPairsPathLengthMapping, AllPairsPathMapping, EdgeList, NodeIndices, NodeMap, PathLengthMapping, PathMapping, Pos2DMapping, WeightedEdgeList, }; @@ -621,7 +621,7 @@ fn graph_is_subgraph_isomorphic( /// /// :returns: A dicitonary of node indices from ``first`` to node indices in /// ``second`` representing the mapping found. -/// :rtype: dict +/// :rtype: NodeMap #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] fn digraph_vf2_mapping( py: Python, @@ -632,7 +632,7 @@ fn digraph_vf2_mapping( id_order: bool, subgraph: bool, induced: bool, -) -> PyResult>> { +) -> PyResult> { let compare_nodes = node_matcher.map(|f| { move |a: &PyObject, b: &PyObject| -> PyResult { let res = f.call1(py, (a, b))?; @@ -666,7 +666,7 @@ fn digraph_vf2_mapping( Some(&mut mapping), )?; if res { - Ok(Some(mapping)) + Ok(Some(NodeMap {node_map: mapping})) } else { Ok(None) } @@ -701,7 +701,7 @@ fn digraph_vf2_mapping( /// /// :returns: A dicitonary of node indices from ``first`` to node indices in /// ``second`` representing the mapping found. -/// :rtype: dict +/// :rtype: NodeMap #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] fn graph_vf2_mapping( py: Python, @@ -712,7 +712,7 @@ fn graph_vf2_mapping( id_order: bool, subgraph: bool, induced: bool, -) -> PyResult>> { +) -> PyResult> { let compare_nodes = node_matcher.map(|f| { move |a: &PyObject, b: &PyObject| -> PyResult { let res = f.call1(py, (a, b))?; @@ -746,7 +746,7 @@ fn graph_vf2_mapping( Some(&mut mapping), )?; if res { - Ok(Some(mapping)) + Ok(Some(NodeMap{ node_map: mapping})) } else { Ok(None) } From 90aa85083ca7d6b97b886d11300b5bf15ddf0ffc Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Jun 2021 17:14:06 -0400 Subject: [PATCH 07/27] Run cargo fmt --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5c0564dbb0..8e9298dd44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,8 +60,8 @@ use rayon::prelude::*; use crate::generators::PyInit_generators; use crate::iterators::{ - AllPairsPathLengthMapping, AllPairsPathMapping, EdgeList, NodeIndices, NodeMap, - PathLengthMapping, PathMapping, Pos2DMapping, WeightedEdgeList, + AllPairsPathLengthMapping, AllPairsPathMapping, EdgeList, NodeIndices, + NodeMap, PathLengthMapping, PathMapping, Pos2DMapping, WeightedEdgeList, }; trait NodesRemoved { @@ -666,7 +666,7 @@ fn digraph_vf2_mapping( Some(&mut mapping), )?; if res { - Ok(Some(NodeMap {node_map: mapping})) + Ok(Some(NodeMap { node_map: mapping })) } else { Ok(None) } @@ -746,7 +746,7 @@ fn graph_vf2_mapping( Some(&mut mapping), )?; if res { - Ok(Some(NodeMap{ node_map: mapping})) + Ok(Some(NodeMap { node_map: mapping })) } else { Ok(None) } From 80f70f584f2de8108e2c9065f90ca3d1c9410617 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Fri, 2 Jul 2021 14:20:54 +0300 Subject: [PATCH 08/27] return an iterator over all valid vf2 mappings --- retworkx/__init__.py | 27 +- src/isomorphism.rs | 850 +++++++++++++++++-------------- src/lib.rs | 217 +++----- tests/digraph/test_isomorphic.py | 18 +- tests/graph/test_isomorphic.py | 18 +- 5 files changed, 561 insertions(+), 569 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 703a461e63..99e9fb4336 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1319,16 +1319,25 @@ def vf2_mapping( induced=True, ): """ - Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects + Return an iterator over all vf2 mappings between two graphs. This funcion will run the vf2 algorithm used from :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` - but instead of returning a boolean it will return the mapping of node ids - found from ``first`` to ``second``. If the graphs are not isomorphic than - ``None`` will be returned. - - :param PyDiGraph first: The first graph to find the mapping for - :param PyDiGraph second: The second graph to find the mapping for + but instead of returning a boolean it will return an iterator over all possible + mapping of node ids found from ``first`` to ``second``. If the graphs are not + isomorphic then the iterator will be empty. A simple example that retrieves + one mapping would be:: + + graph_a = retworkx.generators.path_graph(3) + graph_b = retworkx.generators.path_graph(2) + vf2 = retworkx.vf2_mapping(graph_a, graph_b, subgraph=True) + try: + mapping = next(vf2) + except: + pass + + :param first: The first graph to find the mapping for + :param second: The second graph to find the mapping for :param node_matcher: An optional python callable object that takes 2 positional arguments, one for each node data object in either graph. If the return of this function evaluates to True then the nodes @@ -1346,9 +1355,9 @@ def vf2_mapping( of a node-induced subgraph of first isomorphic to second graph. Default: ``True``. - :returns: A dicitonary of node indices from ``first`` to node indices in + :returns: An iterator over dicitonaries of node indices from ``first`` to node indices in ``second`` representing the mapping found. - :rtype: NodeMap + :rtype: Iterable[NodeMap] """ raise TypeError("Invalid Input Type %s for graph" % type(first)) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index 419daebcf4..346b56a2dd 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -16,14 +16,15 @@ use fixedbitset::FixedBitSet; use std::cmp::Ordering; -use std::iter::FromIterator; +use std::iter::Iterator; use std::marker; -use hashbrown::{HashMap, HashSet}; - -use super::NodesRemoved; +use hashbrown::HashMap; +use pyo3::class::iter::{IterNextOutput, PyIterProtocol}; +use pyo3::gc::{PyGCProtocol, PyVisit}; use pyo3::prelude::*; +use pyo3::PyTraverseError; use petgraph::stable_graph::NodeIndex; use petgraph::stable_graph::StableGraph; @@ -31,60 +32,68 @@ use petgraph::visit::{ EdgeRef, GetAdjacencyMatrix, IntoEdgeReferences, NodeIndexable, }; use petgraph::EdgeType; -use petgraph::{Directed, Incoming, Outgoing}; +use petgraph::{Directed, Incoming, Outgoing, Undirected}; use rayon::slice::ParallelSliceMut; +use crate::iterators::NodeMap; + type StablePyGraph = StableGraph; -// NOTE: assumes contiguous node ids. trait NodeSorter where Ty: EdgeType, { - fn sort(&self, _: &StablePyGraph) -> Vec; + fn sort(&self, _: &StablePyGraph) -> Vec; fn reorder( &self, py: Python, graph: &StablePyGraph, - mut mapping: Option<&mut HashMap>, - ) -> StablePyGraph { + ) -> (StablePyGraph, HashMap) { let order = self.sort(graph); let mut new_graph = StablePyGraph::::default(); - let mut id_map: Vec = vec![0; graph.node_count()]; - for node in order { - let node_index = graph.from_index(node); + let mut id_map: HashMap = HashMap::new(); + 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[node] = graph.to_index(new_index); + id_map.insert(node_index, new_index); } for edge in graph.edge_references() { let edge_w = edge.weight(); - let p = id_map[graph.to_index(edge.source())]; - let c = id_map[graph.to_index(edge.target())]; - let p_index = graph.from_index(p); - let c_index = graph.from_index(c); + 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)); } - if mapping.is_some() { - for (old_value, new_index) in id_map.iter().enumerate() { - mapping.as_mut().unwrap().insert(*new_index, old_value); - } - } - new_graph + ( + 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_count(); + fn sort(&self, graph: &StablePyGraph) -> Vec { + let n = graph.node_bound(); let dout: Vec = (0..n) .map(|idx| { @@ -108,7 +117,7 @@ where let mut conn_in: Vec = vec![0; n]; let mut conn_out: Vec = vec![0; n]; - let mut order: Vec = Vec::with_capacity(n); + let mut order: Vec = Vec::with_capacity(n); // Process BFS level let mut process = |mut vd: Vec| -> Vec { @@ -123,7 +132,7 @@ where .unwrap(); vd.swap(i, i + index); - order.push(item); + order.push(NodeIndex::new(item)); for neigh in graph.neighbors_directed(graph.from_index(item), Outgoing) @@ -150,15 +159,15 @@ where return; } - let mut next_level: HashSet = HashSet::new(); + let mut next_level: Vec = Vec::new(); seen[root] = true; - next_level.insert(root); + next_level.push(root); while !next_level.is_empty() { - let this_level = Vec::from_iter(next_level); + let this_level = next_level; let this_level = process(this_level); - next_level = HashSet::new(); + next_level = Vec::new(); for bfs_node in this_level { for neighbor in graph.neighbors_directed( graph.from_index(bfs_node), @@ -167,15 +176,16 @@ where let neigh = graph.to_index(neighbor); if !seen[neigh] { seen[neigh] = true; - next_level.insert(neigh); + next_level.push(neigh); } } } } }; - let mut sorted_nodes: Vec = (0..n).collect(); - sorted_nodes.par_sort_unstable_by_key(|&node| (dout[node], din[node])); + let mut sorted_nodes: Vec = + graph.node_indices().map(|node| node.index()).collect(); + sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node])); sorted_nodes.reverse(); for node in sorted_nodes { @@ -186,21 +196,12 @@ where } } -impl<'a, Ty> NodesRemoved for &'a StablePyGraph -where - Ty: EdgeType, -{ - fn nodes_removed(&self) -> bool { - self.node_bound() != self.node_count() - } -} - #[derive(Debug)] -struct Vf2State<'a, Ty> +struct Vf2State where Ty: EdgeType, { - graph: &'a StablePyGraph, + graph: StablePyGraph, /// The current mapping M(s) of nodes from G0 → G1 and G1 → G0, /// NodeIndex::end() for no mapping. mapping: Vec, @@ -220,20 +221,22 @@ where _etype: marker::PhantomData, } -impl<'a, Ty> Vf2State<'a, Ty> +impl Vf2State where Ty: EdgeType, { - pub fn new(g: &'a StablePyGraph) -> Self { - let c0 = g.node_count(); + pub fn new(graph: StablePyGraph) -> Self { + let c0 = graph.node_count(); + let is_directed = graph.is_directed(); + let adjacency_matrix = graph.adjacency_matrix(); Vf2State { - graph: g, + graph, mapping: vec![NodeIndex::end(); c0], out: vec![0; c0], - ins: vec![0; c0 * (g.is_directed() as usize)], + ins: vec![0; c0 * (is_directed as usize)], out_size: 0, ins_size: 0, - adjacency_matrix: g.adjacency_matrix(), + adjacency_matrix, generation: 0, _etype: marker::PhantomData, } @@ -325,57 +328,20 @@ where } } -fn reindex_graph( - py: Python, - graph: &StablePyGraph, -) -> (StablePyGraph, HashMap) -where - Ty: EdgeType, -{ - // NOTE: this is a hacky workaround to handle non-contiguous node ids in - // VF2. The code which was forked from petgraph was written assuming the - // Graph type and not StableGraph so it makes an implicit assumption on - // node_bound() == node_count() which isn't true with removals on - // StableGraph. This compacts the node ids as a workaround until VF2State - // and try_match can be rewitten to handle this (and likely contributed - // upstream to petgraph too). - let mut new_graph = StablePyGraph::::default(); - let mut id_map: HashMap = HashMap::new(); - for node_index in graph.node_indices() { - 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(), - ) -} - trait SemanticMatcher { fn enabled(&self) -> bool; - fn eq(&mut self, _: &T, _: &T) -> PyResult; + fn eq(&mut self, _: Python, _: &T, _: &T) -> PyResult; } -impl SemanticMatcher for Option -where - F: FnMut(&T, &T) -> PyResult, -{ +impl SemanticMatcher for Option { #[inline] fn enabled(&self) -> bool { self.is_some() } #[inline] - fn eq(&mut self, a: &T, b: &T) -> PyResult { - let res = (self.as_mut().unwrap())(a, b)?; - Ok(res) + fn eq(&mut self, py: Python, a: &PyObject, b: &PyObject) -> PyResult { + let res = self.as_mut().unwrap().call1(py, (a, b))?; + res.is_true(py) } } @@ -385,225 +351,162 @@ where /// graph isomorphism (graph structure and matching node and edge weights). /// /// The graphs should not be multigraphs. -pub fn is_isomorphic( +pub fn is_isomorphic( py: Python, g0: &StablePyGraph, g1: &StablePyGraph, - mut node_match: Option, - mut edge_match: Option, + node_match: Option, + edge_match: Option, id_order: bool, ordering: Ordering, induced: bool, - mut mapping: Option<&mut HashMap>, -) -> PyResult -where - Ty: EdgeType, - F: FnMut(&PyObject, &PyObject) -> PyResult, - G: FnMut(&PyObject, &PyObject) -> PyResult, -{ - let mut inner_temp_g0: StablePyGraph; - let mut inner_temp_g1: StablePyGraph; - let mut node_map_g0: Option>; - let mut node_map_g1: Option>; - let g0_out = if g0.nodes_removed() { - let res = reindex_graph(py, g0); - inner_temp_g0 = res.0; - node_map_g0 = Some(res.1); - &inner_temp_g0 - } else { - node_map_g0 = None; - g0 - }; - let g1_out = if g1.nodes_removed() { - let res = reindex_graph(py, g1); - inner_temp_g1 = res.0; - node_map_g1 = Some(res.1); - &inner_temp_g1 - } else { - node_map_g1 = None; - g1 - }; - - if (g0_out.node_count().cmp(&g1_out.node_count()).then(ordering) - != ordering) - || (g0_out.edge_count().cmp(&g1_out.edge_count()).then(ordering) - != ordering) +) -> PyResult { + if (g0.node_count().cmp(&g1.node_count()).then(ordering) != ordering) + || (g0.edge_count().cmp(&g1.edge_count()).then(ordering) != ordering) { return Ok(false); } - let g0 = if !id_order { - inner_temp_g0 = if mapping.is_some() { - let mut vf2pp_map: HashMap = - HashMap::with_capacity(g0_out.node_count()); - let temp = Vf2ppSorter.reorder(py, g0_out, Some(&mut vf2pp_map)); - match node_map_g0 { - Some(ref mut g0_map) => { - for (_, old_index) in vf2pp_map.iter_mut() { - *old_index = g0_map[old_index]; - } - *g0_map = vf2pp_map; - } - None => node_map_g0 = Some(vf2pp_map), - }; - temp - } else { - Vf2ppSorter.reorder(py, g0_out, None) - }; - &inner_temp_g0 - } else { - g0_out - }; + let mut vf2 = Vf2Algorithm::new( + py, g0, g1, node_match, edge_match, id_order, ordering, induced, + ); + if vf2.next(py)?.is_some() { + return Ok(true); + } + Ok(false) +} - let g1 = if !id_order { - inner_temp_g1 = if mapping.is_some() { - let mut vf2pp_map: HashMap = - HashMap::with_capacity(g1_out.node_count()); - let temp = Vf2ppSorter.reorder(py, g1_out, Some(&mut vf2pp_map)); - match node_map_g1 { - Some(ref mut g1_map) => { - for (_, old_index) in vf2pp_map.iter_mut() { - *old_index = g1_map[old_index]; - } - *g1_map = vf2pp_map; - } - None => node_map_g1 = Some(vf2pp_map), - }; - temp - } else { - Vf2ppSorter.reorder(py, g1_out, None) - }; - &inner_temp_g1 - } else { - g1_out - }; +#[derive(Copy, Clone, PartialEq, Debug)] +enum OpenList { + Out, + In, + Other, +} - let mut st = [Vf2State::new(g0), Vf2State::new(g1)]; - let res = try_match( - &mut st, - g0, - g1, - &mut node_match, - &mut edge_match, - ordering, - induced, - )?; - if mapping.is_some() && res == Some(true) { - for (index, val) in st[1].mapping.iter().enumerate() { - match node_map_g1 { - Some(ref g1_map) => { - let node_index = g1_map[&index]; - match node_map_g0 { - Some(ref g0_map) => mapping - .as_mut() - .unwrap() - .insert(g0_map[&val.index()], node_index), - None => mapping - .as_mut() - .unwrap() - .insert(val.index(), node_index), - }; - } - None => { - match node_map_g0 { - Some(ref g0_map) => mapping - .as_mut() - .unwrap() - .insert(g0_map[&val.index()], index), - None => { - mapping.as_mut().unwrap().insert(val.index(), index) - } - }; - } - }; - } - } - Ok(res.unwrap_or(false)) +#[derive(Clone, PartialEq, Debug)] +enum Frame { + Outer, + Inner { nodes: [N; 2], open_list: OpenList }, + Unwind { nodes: [N; 2], open_list: OpenList }, } -/// Return Some(bool) if isomorphism is decided, else None. -fn try_match( - mut st: &mut [Vf2State; 2], - g0: &StablePyGraph, - g1: &StablePyGraph, - node_match: &mut F, - edge_match: &mut G, +struct Vf2Algorithm +where + Ty: EdgeType, + F: SemanticMatcher, + G: SemanticMatcher, +{ + st: [Vf2State; 2], + node_match: F, + edge_match: G, ordering: Ordering, induced: bool, -) -> PyResult> + node_map_g0: HashMap, + node_map_g1: HashMap, + stack: Vec>, +} + +impl Vf2Algorithm where Ty: EdgeType, F: SemanticMatcher, G: SemanticMatcher, { - if st[1].is_complete() { - return Ok(Some(true)); - } - - let g = [g0, g1]; - let graph_indices = 0..2; - let end = NodeIndex::end(); - - // A "depth first" search of a valid mapping from graph 1 to graph 2 + pub fn new( + py: Python, + g0: &StablePyGraph, + g1: &StablePyGraph, + node_match: F, + edge_match: G, + id_order: bool, + ordering: Ordering, + induced: bool, + ) -> Self { + let (g0, node_map_g0) = if id_order { + DefaultIdSorter.reorder(py, g0) + } else { + Vf2ppSorter.reorder(py, g0) + }; - // F(s, n, m) -- evaluate state s and add mapping n <-> m + let (g1, node_map_g1) = if id_order { + DefaultIdSorter.reorder(py, g1) + } else { + Vf2ppSorter.reorder(py, g1) + }; - // Find least T1out node (in st.out[1] but not in M[1]) - #[derive(Copy, Clone, PartialEq, Debug)] - enum OpenList { - Out, - In, - Other, + 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], + } } - #[derive(Clone, PartialEq, Debug)] - enum Frame { - Outer, - Inner { nodes: [N; 2], open_list: OpenList }, - Unwind { nodes: [N; 2], open_list: OpenList }, + fn mapping(&self) -> NodeMap { + let mut mapping: HashMap = HashMap::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 } } - let next_candidate = - |st: &mut [Vf2State<'_, Ty>; 2]| -> Option<(NodeIndex, NodeIndex, OpenList)> { - let mut to_index; - let mut from_index = None; - let mut open_list = OpenList::Out; - // Try the out list - to_index = st[1].next_out_index(0); + fn next_candidate( + st: &mut [Vf2State; 2], + ) -> Option<(NodeIndex, NodeIndex, OpenList)> { + let mut to_index; + let mut from_index = None; + let mut open_list = OpenList::Out; + // Try the out list + to_index = st[1].next_out_index(0); + + 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_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; - } + 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; - } + } + // 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, + } + match (from_index, to_index) { + (Some(n), Some(m)) => { + Some((NodeIndex::new(n), NodeIndex::new(m), open_list)) } - }; - let next_from_ix = |st: &mut [Vf2State<'_, Ty>; 2], - nx: NodeIndex, - open_list: OpenList| - -> Option { + // 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 { @@ -619,25 +522,29 @@ where Some(NodeIndex::new(ix)) } } - }; - //fn pop_state(nodes: [NodeIndex; 2]) { - let pop_state = |st: &mut [Vf2State<'_, Ty>; 2], nodes: [NodeIndex; 2]| { + } + + fn pop_state(st: &mut [Vf2State; 2], nodes: [NodeIndex; 2]) { // Restore state. - for j in graph_indices.clone() { - st[j].pop_mapping(nodes[j]); - } - }; - //fn push_state(nodes: [NodeIndex; 2]) { - let push_state = |st: &mut [Vf2State<'_, Ty>; 2], nodes: [NodeIndex; 2]| { + 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 - for j in graph_indices.clone() { - st[j].push_mapping(nodes[j], nodes[1 - j]); - } - }; - //fn is_feasible(nodes: [NodeIndex; 2]) -> bool { - let mut is_feasible = |st: &mut [Vf2State<'_, Ty>; 2], - nodes: [NodeIndex; 2]| - -> PyResult { + 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. // @@ -654,10 +561,10 @@ where // R_in: Same with Tin // R_new: Equal for G0, G1: Ñ n Pred(G, n); both Succ and Pred, // Ñ is G0 - M - Tin - Tout - // last attempt to add these did not speed up any of the testcases + let end = NodeIndex::end(); let mut succ_count = [0, 0]; - for j in graph_indices.clone() { - for n_neigh in g[j].neighbors(nodes[j]) { + for j in 0..2 { + for n_neigh in st[j].graph.neighbors(nodes[j]) { succ_count[j] += 1; if !induced && j == 0 { continue; @@ -671,7 +578,7 @@ where if m_neigh == end { continue; } - let has_edge = g[1 - j].is_adjacent( + let has_edge = st[1 - j].graph.is_adjacent( &st[1 - j].adjacency_matrix, nodes[1 - j], m_neigh, @@ -685,10 +592,12 @@ where return Ok(false); } // R_pred - if g[0].is_directed() { + if st[0].graph.is_directed() { let mut pred_count = [0, 0]; - for j in graph_indices.clone() { - for n_neigh in g[j].neighbors_directed(nodes[j], Incoming) { + 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; @@ -698,7 +607,7 @@ where if m_neigh == end { continue; } - let has_edge = g[1 - j].is_adjacent( + let has_edge = st[1 - j].graph.is_adjacent( &st[1 - j].adjacency_matrix, m_neigh, nodes[1 - j], @@ -715,7 +624,8 @@ where macro_rules! rule { ($arr:ident, $j:expr, $dir:expr) => {{ let mut count = 0; - for n_neigh in g[$j].neighbors_directed(nodes[$j], $dir) { + 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; @@ -732,7 +642,7 @@ where { return Ok(false); } - if g[0].is_directed() + if st[0].graph.is_directed() && rule!(out, 0, Incoming) .cmp(&rule!(out, 1, Incoming)) .then(ordering) @@ -741,7 +651,7 @@ where return Ok(false); } // R_in - if g[0].is_directed() { + if st[0].graph.is_directed() { if rule!(ins, 0, Outgoing) .cmp(&rule!(ins, 1, Outgoing)) .then(ordering) @@ -761,8 +671,8 @@ where // R_new if induced { let mut new_count = [0, 0]; - for j in graph_indices.clone() { - for n_neigh in g[j].neighbors(nodes[j]) { + 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) @@ -774,10 +684,12 @@ where if new_count[0].cmp(&new_count[1]).then(ordering) != ordering { return Ok(false); } - if g[0].is_directed() { + if st[0].graph.is_directed() { let mut new_count = [0, 0]; - for j in graph_indices.clone() { - for n_neigh in g[j].neighbors_directed(nodes[j], Incoming) { + 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; @@ -791,16 +703,20 @@ where } // semantic feasibility: compare associated data for nodes if node_match.enabled() - && !node_match.eq(&g[0][nodes[0]], &g[1][nodes[1]])? + && !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() { // outgoing edges - for j in graph_indices.clone() { - let mut edges = g[j].neighbors(nodes[j]).detach(); - while let Some((n_edge, n_neigh)) = edges.next(g[j]) { + for j in 0..2 { + let mut edges = st[j].graph.neighbors(nodes[j]).detach(); + while let Some((n_edge, n_neigh)) = edges.next(&st[j].graph) { // 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()] @@ -810,10 +726,13 @@ where if m_neigh == end { continue; } - match g[1 - j].find_edge(nodes[1 - j], m_neigh) { + match st[1 - j].graph.find_edge(nodes[1 - j], m_neigh) { Some(m_edge) => { - let match_result = edge_match - .eq(&g[j][n_edge], &g[1 - j][m_edge])?; + let match_result = edge_match.eq( + py, + &st[j].graph[n_edge], + &st[1 - j].graph[m_edge], + )?; if !match_result { return Ok(false); } @@ -823,20 +742,26 @@ where } } // incoming edges - if g[0].is_directed() { - for j in graph_indices.clone() { - let mut edges = - g[j].neighbors_directed(nodes[j], Incoming).detach(); - while let Some((n_edge, n_neigh)) = edges.next(g[j]) { + if st[0].graph.is_directed() { + for j in 0..2 { + let mut edges = st[j] + .graph + .neighbors_directed(nodes[j], Incoming) + .detach(); + while let Some((n_edge, n_neigh)) = edges.next(&st[j].graph) + { // the self loop case is handled in outgoing let m_neigh = st[j].mapping[n_neigh.index()]; if m_neigh == end { continue; } - match g[1 - j].find_edge(m_neigh, nodes[1 - j]) { + match st[1 - j].graph.find_edge(m_neigh, nodes[1 - j]) { Some(m_edge) => { - let match_result = edge_match - .eq(&g[j][n_edge], &g[1 - j][m_edge])?; + let match_result = edge_match.eq( + py, + &st[j].graph[n_edge], + &st[1 - j].graph[m_edge], + )?; if !match_result { return Ok(false); } @@ -848,76 +773,221 @@ where } } Ok(true) - }; - let mut stack: Vec> = vec![Frame::Outer]; - - while let Some(frame) = stack.pop() { - match frame { - Frame::Unwind { - nodes, - open_list: ol, - } => { - pop_state(&mut st, nodes); - - match next_from_ix(&mut st, nodes[0], ol) { - None => continue, - Some(nx) => { - let f = Frame::Inner { - nodes: [nx, nodes[1]], - open_list: ol, - }; - stack.push(f); + } + + /// 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 next_candidate(&mut st) { - None => continue, - Some((nx, mx, ol)) => { - let f = Frame::Inner { - nodes: [nx, mx], - open_list: ol, - }; - stack.push(f); - } - }, - Frame::Inner { - nodes, - open_list: ol, - } => { - let feasible = is_feasible(&mut st, nodes)?; - if feasible { - push_state(&mut st, nodes); - if st[1].is_complete() { - return Ok(Some(true)); - } - // Check cardinalities of Tin, Tout sets - if st[0].out_size.cmp(&st[1].out_size).then(ordering) - == ordering - && st[0].ins_size.cmp(&st[1].ins_size).then(ordering) - == ordering + Frame::Outer => { + match Vf2Algorithm::::next_candidate(&mut self.st) { - let f0 = Frame::Unwind { - nodes, - open_list: ol, - }; - stack.push(f0); - stack.push(Frame::Outer); - continue; + None => continue, + Some((nx, mx, ol)) => { + let f = Frame::Inner { + nodes: [nx, mx], + open_list: ol, + }; + self.stack.push(f); + } } - pop_state(&mut st, nodes); } - match next_from_ix(&mut st, nodes[0], ol) { - None => continue, - Some(nx) => { - let f = Frame::Inner { - nodes: [nx, nodes[1]], - open_list: ol, - }; - 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, + ); + if self.st[1].is_complete() { + let f0 = Frame::Unwind { + nodes, + open_list: ol, + }; + self.stack.push(f0); + return Ok(Some(self.mapping())); + } + // 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 + { + let f0 = Frame::Unwind { + nodes, + open_list: ol, + }; + + self.stack.push(f0); + self.stack.push(Frame::Outer); + continue; + } + Vf2Algorithm::::pop_state( + &mut self.st, + nodes, + ); + } + match Vf2Algorithm::::next_from_ix( + &mut self.st, + nodes[0], + ol, + ) { + None => continue, + Some(nx) => { + let f = Frame::Inner { + nodes: [nx, nodes[1]], + open_list: ol, + }; + self.stack.push(f); + } } } } } + Ok(None) } - Ok(None) } + +macro_rules! vf2_mapping_impl { + ($name:ident, $Ty:ty) => { + #[pyclass(module = "retworkx", gc)] + pub struct $name { + vf2: Vf2Algorithm<$Ty, Option, Option>, + } + + impl $name { + pub fn new( + py: Python, + g0: &StablePyGraph<$Ty>, + g1: &StablePyGraph<$Ty>, + node_match: Option, + edge_match: Option, + id_order: bool, + ordering: Ordering, + induced: bool, + ) -> Self { + let vf2 = Vf2Algorithm::new( + py, g0, g1, node_match, edge_match, id_order, ordering, + induced, + ); + $name { vf2 } + } + } + + #[pyproto] + impl PyIterProtocol for $name { + fn __iter__(slf: PyRef) -> Py<$name> { + slf.into() + } + + fn __next__( + mut slf: PyRefMut, + ) -> PyResult> { + Python::with_gil(|py| match slf.vf2.next(py)? { + Some(mapping) => Ok(IterNextOutput::Yield(mapping)), + None => Ok(IterNextOutput::Return("Ended")), + }) + } + } + + #[pyproto] + impl PyGCProtocol for $name { + fn __traverse__( + &self, + visit: PyVisit, + ) -> Result<(), PyTraverseError> { + for j in 0..2 { + for node in + self.vf2.st[j].graph.node_indices().map(|node| { + self.vf2.st[j].graph.node_weight(node).unwrap() + }) + { + visit.call(node)?; + } + for edge in + self.vf2.st[j].graph.edge_indices().map(|edge| { + self.vf2.st[j].graph.edge_weight(edge).unwrap() + }) + { + visit.call(edge)?; + } + } + if let Some(ref obj) = self.vf2.node_match { + visit.call(obj)?; + } + if let Some(ref obj) = self.vf2.edge_match { + visit.call(obj)?; + } + Ok(()) + } + + fn __clear__(&mut self) { + self.vf2.st[0].graph = StablePyGraph::<$Ty>::default(); + self.vf2.st[1].graph = StablePyGraph::<$Ty>::default(); + self.vf2.node_match = None; + self.vf2.edge_match = None; + } + } + }; +} + +vf2_mapping_impl!(DiGraphVf2Mapping, Directed); +vf2_mapping_impl!(GraphVf2Mapping, Undirected); diff --git a/src/lib.rs b/src/lib.rs index 383c8f1cdf..62b92730ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ use rayon::prelude::*; use crate::generators::PyInit_generators; use crate::iterators::{ AllPairsPathLengthMapping, AllPairsPathMapping, EdgeList, NodeIndices, - NodeMap, NodesCountMapping, PathLengthMapping, PathMapping, Pos2DMapping, + NodesCountMapping, PathLengthMapping, PathMapping, Pos2DMapping, WeightedEdgeList, }; @@ -337,32 +337,16 @@ fn digraph_is_isomorphic( edge_matcher: Option, id_order: bool, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( + isomorphism::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Equal, true, - None, - )?; - Ok(res) + ) } /// Determine if 2 undirected graphs are isomorphic @@ -408,32 +392,16 @@ fn graph_is_isomorphic( edge_matcher: Option, id_order: bool, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( + isomorphism::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Equal, true, - None, - )?; - Ok(res) + ) } /// Determine if 2 directed graphs are subgraph - isomorphic @@ -487,32 +455,16 @@ fn digraph_is_subgraph_isomorphic( id_order: bool, induced: bool, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( + isomorphism::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Greater, induced, - None, - )?; - Ok(res) + ) } /// Determine if 2 undirected graphs are subgraph - isomorphic @@ -566,41 +518,35 @@ fn graph_is_subgraph_isomorphic( id_order: bool, induced: bool, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( + isomorphism::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Greater, induced, - None, - )?; - Ok(res) + ) } -/// Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects +/// Return an iterator over all vf2 mappings between two :class:`~retworkx.PyDiGraph` objects /// /// This funcion will run the vf2 algorithm used from /// :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` -/// but instead of returning a boolean it will return the mapping of node ids -/// found from ``first`` to ``second``. If the graphs are not isomorphic than -/// ``None`` will be returned. +/// but instead of returning a boolean it will return an iterator over all possible +/// mapping of node ids found from ``first`` to ``second``. If the graphs are not +/// isomorphic then the iterator will be empty. A simple example that retrieves +/// one mapping would be:: +/// +/// graph_a = retworkx.generators.directed_path_graph(3) +/// graph_b = retworkx.generators.direccted_path_graph(2) +/// vf2 = retworkx.digraph_vf2_mapping(graph_a, graph_b, subgraph=True) +/// try: +/// mapping = next(vf2) +/// except: +/// pass +/// /// /// :param PyDiGraph first: The first graph to find the mapping for /// :param PyDiGraph second: The second graph to find the mapping for @@ -621,9 +567,9 @@ fn graph_is_subgraph_isomorphic( /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. /// -/// :returns: A dicitonary of node indices from ``first`` to node indices in -/// ``second`` representing the mapping found. -/// :rtype: NodeMap +/// :returns: An iterator over dicitonaries of node indices from ``first`` to node indices +/// in ``second`` representing the mapping found. +/// :rtype: Iterable[NodeMap] #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] fn digraph_vf2_mapping( py: Python, @@ -634,56 +580,44 @@ fn digraph_vf2_mapping( id_order: bool, subgraph: bool, induced: bool, -) -> PyResult> { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); +) -> isomorphism::DiGraphVf2Mapping { let ordering = if subgraph { Ordering::Greater } else { Ordering::Equal }; - let mut mapping: HashMap = HashMap::with_capacity( - first.graph.node_count().min(second.graph.node_count()), - ); - let res = isomorphism::is_isomorphic( + + isomorphism::DiGraphVf2Mapping::new( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, ordering, induced, - Some(&mut mapping), - )?; - if res { - Ok(Some(NodeMap { node_map: mapping })) - } else { - Ok(None) - } + ) } -/// Return the vf2 mapping between two :class:`~retworkx.PyDiGraph` objects +/// Return an iterator over all vf2 mappings between two :class:`~retworkx.PyGraph` objects /// /// This funcion will run the vf2 algorithm used from /// :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` -/// but instead of returning a boolean it will return the mapping of node ids -/// found from ``first`` to ``second``. If the graphs are not isomorphic than -/// ``None`` will be returned. -/// -/// :param PyDiGraph first: The first graph to find the mapping for -/// :param PyDiGraph second: The second graph to find the mapping for +/// but instead of returning a boolean it will return an iterator over all possible +/// mapping of node ids found from ``first`` to ``second``. If the graphs are not +/// isomorphic then the iterator will be empty. A simple example that retrieves +/// one mapping would be:: +/// +/// graph_a = retworkx.generators.path_graph(3) +/// graph_b = retworkx.generators.path_graph(2) +/// vf2 = retworkx.graph_vf2_mapping(graph_a, graph_b, subgraph=True) +/// try: +/// mapping = next(vf2) +/// except: +/// pass +/// +/// :param PyGraph first: The first graph to find the mapping for +/// :param PyGraph second: The second graph to find the mapping for /// :param node_matcher: An optional python callable object that takes 2 /// positional arguments, one for each node data object in either graph. /// If the return of this function evaluates to True then the nodes @@ -701,9 +635,9 @@ fn digraph_vf2_mapping( /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. /// -/// :returns: A dicitonary of node indices from ``first`` to node indices in -/// ``second`` representing the mapping found. -/// :rtype: NodeMap +/// :returns: An iterator over dicitonaries of node indices from ``first`` to node indices +/// in ``second`` representing the mapping found. +/// :rtype: Iterable[NodeMap] #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] fn graph_vf2_mapping( py: Python, @@ -714,44 +648,23 @@ fn graph_vf2_mapping( id_order: bool, subgraph: bool, induced: bool, -) -> PyResult> { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); +) -> isomorphism::GraphVf2Mapping { let ordering = if subgraph { Ordering::Greater } else { Ordering::Equal }; - let mut mapping: HashMap = HashMap::with_capacity( - first.graph.node_count().min(second.graph.node_count()), - ); - let res = isomorphism::is_isomorphic( + + isomorphism::GraphVf2Mapping::new( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, ordering, induced, - Some(&mut mapping), - )?; - if res { - Ok(Some(NodeMap { node_map: mapping })) - } else { - Ok(None) - } + ) } /// Return the topological sort of node indexes from the provided graph diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index 0e71034cf7..620c6feb7a 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -227,7 +227,7 @@ def test_digraph_vf2_mapping_identical(self): graph = retworkx.generators.directed_grid_graph(2, 2) second_graph = retworkx.generators.directed_grid_graph(2, 2) mapping = retworkx.digraph_vf2_mapping(graph, second_graph) - self.assertEqual(mapping, {0: 0, 1: 1, 2: 2, 3: 3}) + self.assertEqual(next(mapping), {0: 0, 1: 1, 2: 2, 3: 3}) def test_digraph_vf2_mapping_identical_removals(self): graph = retworkx.generators.directed_path_graph(2) @@ -235,7 +235,7 @@ def test_digraph_vf2_mapping_identical_removals(self): second_graph.remove_nodes_from([1, 2]) second_graph.add_edge(0, 3, None) mapping = retworkx.digraph_vf2_mapping(graph, second_graph) - self.assertEqual({0: 0, 1: 3}, mapping) + self.assertEqual({0: 0, 1: 3}, next(mapping)) def test_digraph_vf2_mapping_identical_removals_first(self): second_graph = retworkx.generators.directed_path_graph(2) @@ -243,7 +243,7 @@ def test_digraph_vf2_mapping_identical_removals_first(self): graph.remove_nodes_from([1, 2]) graph.add_edge(0, 3, None) mapping = retworkx.digraph_vf2_mapping(graph, second_graph) - self.assertEqual({0: 0, 3: 1}, mapping) + self.assertEqual({0: 0, 3: 1}, next(mapping)) def test_subgraph_vf2_mapping(self): graph = retworkx.generators.directed_grid_graph(10, 10) @@ -251,7 +251,7 @@ def test_subgraph_vf2_mapping(self): mapping = retworkx.digraph_vf2_mapping( graph, second_graph, subgraph=True ) - self.assertEqual(mapping, {0: 0, 1: 1, 10: 2, 11: 3}) + self.assertEqual(next(mapping), {0: 0, 1: 1, 10: 2, 11: 3}) def test_digraph_vf2_mapping_identical_vf2pp(self): graph = retworkx.generators.directed_grid_graph(2, 2) @@ -263,7 +263,7 @@ def test_digraph_vf2_mapping_identical_vf2pp(self): {0: 0, 1: 1, 2: 2, 3: 3}, {0: 0, 1: 2, 2: 1, 3: 3}, ] - self.assertIn(mapping, valid_mappings) + self.assertIn(next(mapping), valid_mappings) def test_graph_vf2_mapping_identical_removals_vf2pp(self): graph = retworkx.generators.directed_path_graph(2) @@ -273,7 +273,7 @@ def test_graph_vf2_mapping_identical_removals_vf2pp(self): mapping = retworkx.digraph_vf2_mapping( graph, second_graph, id_order=False ) - self.assertEqual({0: 0, 1: 3}, mapping) + self.assertEqual({0: 0, 1: 3}, next(mapping)) def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): second_graph = retworkx.generators.directed_path_graph(2) @@ -283,7 +283,7 @@ def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): mapping = retworkx.digraph_vf2_mapping( graph, second_graph, id_order=False ) - self.assertEqual({0: 0, 3: 1}, mapping) + self.assertEqual({0: 0, 3: 1}, next(mapping)) def test_subgraph_vf2_mapping_vf2pp(self): graph = retworkx.generators.directed_grid_graph(3, 3) @@ -295,7 +295,7 @@ def test_subgraph_vf2_mapping_vf2pp(self): {8: 3, 5: 2, 7: 1, 4: 0}, {7: 2, 5: 1, 4: 0, 8: 3}, ] - self.assertIn(mapping, valid_mappings) + self.assertIn(next(mapping), valid_mappings) def test_vf2pp_remapping(self): temp = retworkx.generators.directed_grid_graph(3, 3) @@ -314,4 +314,4 @@ def test_vf2pp_remapping(self): {6: 1, 5: 0, 8: 2, 9: 3}, {6: 2, 5: 0, 9: 3, 8: 1}, ] - self.assertIn(mapping, expected_mappings) + self.assertIn(next(mapping), expected_mappings) diff --git a/tests/graph/test_isomorphic.py b/tests/graph/test_isomorphic.py index 8bc3c0fe04..c3e04c8ad1 100644 --- a/tests/graph/test_isomorphic.py +++ b/tests/graph/test_isomorphic.py @@ -233,7 +233,7 @@ def test_graph_vf2_mapping_identical(self): graph = retworkx.generators.grid_graph(2, 2) second_graph = retworkx.generators.grid_graph(2, 2) mapping = retworkx.graph_vf2_mapping(graph, second_graph) - self.assertEqual(mapping, {0: 0, 1: 1, 2: 2, 3: 3}) + self.assertEqual(next(mapping), {0: 0, 1: 1, 2: 2, 3: 3}) def test_graph_vf2_mapping_identical_removals(self): graph = retworkx.generators.path_graph(2) @@ -241,7 +241,7 @@ def test_graph_vf2_mapping_identical_removals(self): second_graph.remove_nodes_from([1, 2]) second_graph.add_edge(0, 3, None) mapping = retworkx.graph_vf2_mapping(graph, second_graph) - self.assertEqual({0: 0, 1: 3}, mapping) + self.assertEqual({0: 0, 1: 3}, next(mapping)) def test_graph_vf2_mapping_identical_removals_first(self): second_graph = retworkx.generators.path_graph(2) @@ -252,13 +252,13 @@ def test_graph_vf2_mapping_identical_removals_first(self): graph, second_graph, ) - self.assertEqual({0: 0, 3: 1}, mapping) + self.assertEqual({0: 0, 3: 1}, next(mapping)) def test_subgraph_vf2_mapping(self): graph = retworkx.generators.grid_graph(10, 10) second_graph = retworkx.generators.grid_graph(2, 2) mapping = retworkx.graph_vf2_mapping(graph, second_graph, subgraph=True) - self.assertEqual(mapping, {0: 0, 1: 1, 10: 2, 11: 3}) + self.assertEqual(next(mapping), {0: 0, 1: 1, 10: 2, 11: 3}) def test_graph_vf2_mapping_identical_vf2pp(self): graph = retworkx.generators.grid_graph(2, 2) @@ -270,7 +270,7 @@ def test_graph_vf2_mapping_identical_vf2pp(self): {0: 0, 1: 1, 2: 2, 3: 3}, {0: 0, 1: 2, 2: 1, 3: 3}, ] - self.assertIn(mapping, valid_mappings) + self.assertIn(next(mapping), valid_mappings) def test_graph_vf2_mapping_identical_removals_vf2pp(self): graph = retworkx.generators.path_graph(2) @@ -280,7 +280,7 @@ def test_graph_vf2_mapping_identical_removals_vf2pp(self): mapping = retworkx.graph_vf2_mapping( graph, second_graph, id_order=False ) - self.assertEqual({0: 0, 1: 3}, mapping) + self.assertEqual({0: 0, 1: 3}, next(mapping)) def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): second_graph = retworkx.generators.path_graph(2) @@ -290,7 +290,7 @@ def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): mapping = retworkx.graph_vf2_mapping( graph, second_graph, id_order=False ) - self.assertEqual({0: 0, 3: 1}, mapping) + self.assertEqual({0: 0, 3: 1}, next(mapping)) def test_subgraph_vf2_mapping_vf2pp(self): graph = retworkx.generators.grid_graph(3, 3) @@ -308,7 +308,7 @@ def test_subgraph_vf2_mapping_vf2pp(self): {3: 1, 0: 0, 4: 3, 1: 2}, {1: 1, 2: 0, 4: 3, 5: 2}, ] - self.assertIn(mapping, valid_mappings) + self.assertIn(next(mapping), valid_mappings) def test_vf2pp_remapping(self): temp = retworkx.generators.grid_graph(3, 3) @@ -333,4 +333,4 @@ def test_vf2pp_remapping(self): {5: 3, 8: 1, 4: 2, 7: 0}, {5: 3, 4: 1, 7: 0, 8: 2}, ] - self.assertIn(mapping, expected_mappings) + self.assertIn(next(mapping), expected_mappings) From 73a749f3b5cce323fd6df074bf40f978575b335f Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Fri, 2 Jul 2021 14:37:58 +0300 Subject: [PATCH 09/27] update release note --- releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml | 11 ++++++++--- retworkx/__init__.py | 2 +- src/lib.rs | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml b/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml index ea9e2fb3d8..23f256af6b 100644 --- a/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml +++ b/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml @@ -4,7 +4,8 @@ features: Added a new function, :func:`retworkx.vf2_mapping`, which will use the vf2 isomorphism algorithm (which is also used for :func:`retworkx.is_isomorphic` and :func:`retworkx.is_subgraph_isomorphic`) - to return an isomorphic mapping between two graphs. For example: + to return an iterator over all valid isomorphic mappings between two graphs. + For example: .. jupyter-execute:: @@ -12,5 +13,9 @@ features: graph = retworkx.generators.directed_grid_graph(10, 10) other_graph = retworkx.generators.directed_grid_graph(4, 4) - mapping = retworkx.vf2_mapping(graph, other_graph, subgraph=True) - print(mapping) + vf2 = retworkx.vf2_mapping(graph, other_graph, subgraph=True) + try: + mapping = next(vf2) + print(mapping) + except StopIteration: + pass diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 99e9fb4336..2bcf063ef6 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1333,7 +1333,7 @@ def vf2_mapping( vf2 = retworkx.vf2_mapping(graph_a, graph_b, subgraph=True) try: mapping = next(vf2) - except: + except StopIteration: pass :param first: The first graph to find the mapping for diff --git a/src/lib.rs b/src/lib.rs index 62b92730ae..f1041b27df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -544,7 +544,7 @@ fn graph_is_subgraph_isomorphic( /// vf2 = retworkx.digraph_vf2_mapping(graph_a, graph_b, subgraph=True) /// try: /// mapping = next(vf2) -/// except: +/// except StopIteration: /// pass /// /// @@ -613,7 +613,7 @@ fn digraph_vf2_mapping( /// vf2 = retworkx.graph_vf2_mapping(graph_a, graph_b, subgraph=True) /// try: /// mapping = next(vf2) -/// except: +/// except StopIteration: /// pass /// /// :param PyGraph first: The first graph to find the mapping for From f9378528759cd155b6c30445290aa4311bad7c03 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Fri, 2 Jul 2021 14:46:14 +0300 Subject: [PATCH 10/27] lint --- retworkx/__init__.py | 4 ++-- src/lib.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 2bcf063ef6..f0894c42a3 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1355,8 +1355,8 @@ def vf2_mapping( of a node-induced subgraph of first isomorphic to second graph. Default: ``True``. - :returns: An iterator over dicitonaries of node indices from ``first`` to node indices in - ``second`` representing the mapping found. + :returns: An iterator over dicitonaries of node indices from ``first`` to node + indices in ``second`` representing the mapping found. :rtype: Iterable[NodeMap] """ raise TypeError("Invalid Input Type %s for graph" % type(first)) diff --git a/src/lib.rs b/src/lib.rs index f1041b27df..0c59786bb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -567,8 +567,8 @@ fn graph_is_subgraph_isomorphic( /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. /// -/// :returns: An iterator over dicitonaries of node indices from ``first`` to node indices -/// in ``second`` representing the mapping found. +/// :returns: An iterator over dicitonaries of node indices from ``first`` to node +/// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] fn digraph_vf2_mapping( @@ -635,8 +635,8 @@ fn digraph_vf2_mapping( /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. /// -/// :returns: An iterator over dicitonaries of node indices from ``first`` to node indices -/// in ``second`` representing the mapping found. +/// :returns: An iterator over dicitonaries of node indices from ``first`` to node +/// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] fn graph_vf2_mapping( From 8510bd493597de7854a095a7f843b52e50d48dce Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Thu, 15 Jul 2021 19:19:04 +0300 Subject: [PATCH 11/27] fix Vf2ppSorter to use node id instead of edge insertion order as a tie breaker --- src/isomorphism.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index 346b56a2dd..a4672cbcf8 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -127,7 +127,13 @@ where .iter() .enumerate() .max_by_key(|&(_, &node)| { - (conn_in[node], dout[node], conn_out[node], din[node]) + ( + conn_in[node], + dout[node], + conn_out[node], + din[node], + -1 * node as isize, + ) }) .unwrap(); @@ -185,7 +191,9 @@ where let mut sorted_nodes: Vec = graph.node_indices().map(|node| node.index()).collect(); - sorted_nodes.par_sort_by_key(|&node| (dout[node], din[node])); + sorted_nodes.par_sort_by_key(|&node| { + (dout[node], din[node], -1 * node as isize) + }); sorted_nodes.reverse(); for node in sorted_nodes { From c085e433055146ab3305b71026e5dfd44520bae2 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Thu, 15 Jul 2021 19:29:06 +0300 Subject: [PATCH 12/27] fix clippy warning --- src/isomorphism.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index a4672cbcf8..d52175bbe0 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -132,7 +132,7 @@ where dout[node], conn_out[node], din[node], - -1 * node as isize, + -(node as isize), ) }) .unwrap(); @@ -191,9 +191,8 @@ where let mut sorted_nodes: Vec = graph.node_indices().map(|node| node.index()).collect(); - sorted_nodes.par_sort_by_key(|&node| { - (dout[node], din[node], -1 * node as isize) - }); + sorted_nodes + .par_sort_by_key(|&node| (dout[node], din[node], -(node as isize))); sorted_nodes.reverse(); for node in sorted_nodes { From 8f3c701eaaca1403e47dbfee476c2a75d6d7b623 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Thu, 15 Jul 2021 19:36:19 +0300 Subject: [PATCH 13/27] update text signature --- src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 6f13a72c70..0a1e71e881 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -579,6 +579,10 @@ fn graph_is_subgraph_isomorphic( /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] +#[pyo3( + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, subgraph=False, induced=True)" +)] fn digraph_vf2_mapping( py: Python, first: &digraph::PyDiGraph, @@ -647,6 +651,10 @@ fn digraph_vf2_mapping( /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] #[pyfunction(id_order = "true", subgraph = "false", induced = "true")] +#[pyo3( + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, subgraph=False, induced=True)" +)] fn graph_vf2_mapping( py: Python, first: &graph::PyGraph, From e39dcbe5eaa8ea66df7f591633ceb3b9685294ab Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Thu, 15 Jul 2021 20:24:36 +0300 Subject: [PATCH 14/27] update tests since now we can deterministically predict the output mapping --- tests/digraph/test_isomorphic.py | 18 +++--------------- tests/graph/test_isomorphic.py | 30 +++--------------------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index 620c6feb7a..7e022fa5ac 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -259,11 +259,7 @@ def test_digraph_vf2_mapping_identical_vf2pp(self): mapping = retworkx.digraph_vf2_mapping( graph, second_graph, id_order=False ) - valid_mappings = [ - {0: 0, 1: 1, 2: 2, 3: 3}, - {0: 0, 1: 2, 2: 1, 3: 3}, - ] - self.assertIn(next(mapping), valid_mappings) + self.assertEqual(next(mapping), {0: 0, 1: 1, 2: 2, 3: 3}) def test_graph_vf2_mapping_identical_removals_vf2pp(self): graph = retworkx.generators.directed_path_graph(2) @@ -291,11 +287,7 @@ def test_subgraph_vf2_mapping_vf2pp(self): mapping = retworkx.digraph_vf2_mapping( graph, second_graph, subgraph=True, id_order=False ) - valid_mappings = [ - {8: 3, 5: 2, 7: 1, 4: 0}, - {7: 2, 5: 1, 4: 0, 8: 3}, - ] - self.assertIn(next(mapping), valid_mappings) + self.assertEqual(next(mapping), {4: 0, 5: 1, 7: 2, 8: 3}) def test_vf2pp_remapping(self): temp = retworkx.generators.directed_grid_graph(3, 3) @@ -310,8 +302,4 @@ def test_vf2pp_remapping(self): mapping = retworkx.digraph_vf2_mapping( graph, second_graph, subgraph=True, id_order=False ) - expected_mappings = [ - {6: 1, 5: 0, 8: 2, 9: 3}, - {6: 2, 5: 0, 9: 3, 8: 1}, - ] - self.assertIn(next(mapping), expected_mappings) + self.assertEqual(next(mapping), {5: 0, 6: 1, 8: 2, 9: 3}) diff --git a/tests/graph/test_isomorphic.py b/tests/graph/test_isomorphic.py index c3e04c8ad1..1d133005d0 100644 --- a/tests/graph/test_isomorphic.py +++ b/tests/graph/test_isomorphic.py @@ -266,11 +266,7 @@ def test_graph_vf2_mapping_identical_vf2pp(self): mapping = retworkx.graph_vf2_mapping( graph, second_graph, id_order=False ) - valid_mappings = [ - {0: 0, 1: 1, 2: 2, 3: 3}, - {0: 0, 1: 2, 2: 1, 3: 3}, - ] - self.assertIn(next(mapping), valid_mappings) + self.assertEqual(next(mapping), {0: 0, 1: 1, 2: 2, 3: 3}) def test_graph_vf2_mapping_identical_removals_vf2pp(self): graph = retworkx.generators.path_graph(2) @@ -298,17 +294,7 @@ def test_subgraph_vf2_mapping_vf2pp(self): mapping = retworkx.graph_vf2_mapping( graph, second_graph, subgraph=True, id_order=False ) - valid_mappings = [ - {3: 2, 4: 3, 6: 0, 7: 1}, - {3: 1, 4: 3, 6: 0, 7: 2}, - {4: 3, 5: 1, 7: 2, 8: 0}, - {0: 0, 1: 1, 3: 2, 4: 3}, - {7: 1, 8: 0, 4: 3, 5: 2}, - {5: 1, 2: 0, 1: 2, 4: 3}, - {3: 1, 0: 0, 4: 3, 1: 2}, - {1: 1, 2: 0, 4: 3, 5: 2}, - ] - self.assertIn(next(mapping), valid_mappings) + self.assertEqual(next(mapping), {4: 0, 3: 2, 0: 3, 1: 1}) def test_vf2pp_remapping(self): temp = retworkx.generators.grid_graph(3, 3) @@ -323,14 +309,4 @@ def test_vf2pp_remapping(self): mapping = retworkx.graph_vf2_mapping( graph, second_graph, subgraph=True, id_order=False ) - expected_mappings = [ - {2: 2, 3: 0, 5: 3, 6: 1}, - {5: 3, 6: 1, 8: 2, 9: 0}, - {2: 1, 5: 3, 6: 2, 3: 0}, - {2: 2, 1: 0, 5: 3, 4: 1}, - {5: 3, 6: 2, 8: 1, 9: 0}, - {4: 2, 1: 0, 5: 3, 2: 1}, - {5: 3, 8: 1, 4: 2, 7: 0}, - {5: 3, 4: 1, 7: 0, 8: 2}, - ] - self.assertIn(next(mapping), expected_mappings) + self.assertEqual(next(mapping), {5: 0, 4: 2, 1: 3, 2: 1}) From 9b9bf9142b989e7ae6ea405f0e3bbe7ad0d8228f Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Fri, 23 Jul 2021 15:55:20 +0300 Subject: [PATCH 15/27] add call_limit kwarg in Vf2 algorithm --- retworkx/__init__.py | 46 +++++++++++++++++++++++++++++------ src/isomorphism.rs | 16 +++++++++++- src/lib.rs | 58 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 8ef4c7e24d..0ae52424b1 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -715,7 +715,12 @@ def _graph_dfs_edges(graph, source): @functools.singledispatch def is_isomorphic( - first, second, node_matcher=None, edge_matcher=None, id_order=True + first, + second, + node_matcher=None, + edge_matcher=None, + id_order=True, + call_limit=None, ): """Determine if 2 graphs are isomorphic @@ -750,6 +755,9 @@ def is_isomorphic( :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``. Default: ``None``. :returns: ``True`` if the 2 graphs are isomorphic, ``False`` if they are not. @@ -763,19 +771,29 @@ def is_isomorphic( @is_isomorphic.register(PyDiGraph) def _digraph_is_isomorphic( - first, second, node_matcher=None, edge_matcher=None, id_order=True + first, + second, + node_matcher=None, + edge_matcher=None, + id_order=True, + call_limit=None, ): return digraph_is_isomorphic( - first, second, node_matcher, edge_matcher, id_order + first, second, node_matcher, edge_matcher, id_order, call_limit ) @is_isomorphic.register(PyGraph) def _graph_is_isomorphic( - first, second, node_matcher=None, edge_matcher=None, id_order=True + first, + second, + node_matcher=None, + edge_matcher=None, + id_order=True, + call_limit=None, ): return graph_is_isomorphic( - first, second, node_matcher, edge_matcher, id_order + first, second, node_matcher, edge_matcher, id_order, call_limit ) @@ -836,6 +854,7 @@ def is_subgraph_isomorphic( edge_matcher=None, id_order=False, induced=True, + call_limit=None, ): """Determine if 2 graphs are subgraph isomorphic @@ -873,6 +892,9 @@ def is_subgraph_isomorphic( :param bool induced: If set to ``True`` this function will check the existence of a node-induced subgraph of first isomorphic to second graph. Default: ``True``. + :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``. Default: ``None``. :returns: ``True`` if there is a subgraph of `first` isomorphic to `second` , ``False`` if there is not. @@ -889,9 +911,10 @@ def _digraph_is_subgraph_isomorphic( edge_matcher=None, id_order=False, induced=True, + call_limit=None, ): return digraph_is_subgraph_isomorphic( - first, second, node_matcher, edge_matcher, id_order, induced + first, second, node_matcher, edge_matcher, id_order, induced, call_limit ) @@ -903,9 +926,10 @@ def _graph_is_subgraph_isomorphic( edge_matcher=None, id_order=False, induced=True, + call_limit=None, ): return graph_is_subgraph_isomorphic( - first, second, node_matcher, edge_matcher, id_order, induced + first, second, node_matcher, edge_matcher, id_order, induced, call_limit ) @@ -1403,6 +1427,7 @@ def vf2_mapping( id_order=True, subgraph=False, induced=True, + call_limit=None, ): """ Return an iterator over all vf2 mappings between two graphs. @@ -1440,6 +1465,9 @@ def vf2_mapping( :param bool induced: If set to ``True`` this function will check the existence of a node-induced subgraph of first isomorphic to second graph. Default: ``True``. + :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. Default: ``None``. :returns: An iterator over dicitonaries of node indices from ``first`` to node indices in ``second`` representing the mapping found. @@ -1457,6 +1485,7 @@ def _digraph_vf2_mapping( id_order=True, subgraph=False, induced=True, + call_limit=None, ): return digraph_vf2_mapping( first, @@ -1466,6 +1495,7 @@ def _digraph_vf2_mapping( id_order=id_order, subgraph=subgraph, induced=induced, + call_limit=call_limit, ) @@ -1478,6 +1508,7 @@ def _graph_vf2_mapping( id_order=True, subgraph=False, induced=True, + call_limit=None, ): return graph_vf2_mapping( first, @@ -1487,4 +1518,5 @@ def _graph_vf2_mapping( id_order=id_order, subgraph=subgraph, induced=induced, + call_limit=call_limit, ) diff --git a/src/isomorphism.rs b/src/isomorphism.rs index d52175bbe0..5b4e7652d2 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism.rs @@ -367,6 +367,7 @@ pub fn is_isomorphic( 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) @@ -376,6 +377,7 @@ pub fn is_isomorphic( 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); @@ -411,6 +413,8 @@ where node_map_g0: HashMap, node_map_g1: HashMap, stack: Vec>, + call_limit: Option, + _counter: usize, } impl Vf2Algorithm @@ -428,6 +432,7 @@ where id_order: bool, ordering: Ordering, induced: bool, + call_limit: Option, ) -> Self { let (g0, node_map_g0) = if id_order { DefaultIdSorter.reorder(py, g0) @@ -451,6 +456,8 @@ where node_map_g0, node_map_g1, stack: vec![Frame::Outer], + call_limit, + _counter: 0, } } @@ -866,6 +873,12 @@ where self.stack.push(f0); return Ok(Some(self.mapping())); } + self._counter += 1; + if let Some(limit) = self.call_limit { + if self._counter > limit { + return Ok(None); + } + } // Check cardinalities of Tin, Tout sets if self.st[0] .out_size @@ -930,10 +943,11 @@ macro_rules! vf2_mapping_impl { id_order: bool, ordering: Ordering, induced: bool, + call_limit: Option, ) -> Self { let vf2 = Vf2Algorithm::new( py, g0, g1, node_match, edge_match, id_order, ordering, - induced, + induced, call_limit, ); $name { vf2 } } diff --git a/src/lib.rs b/src/lib.rs index 0a1e71e881..344a4ef7dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -323,13 +323,17 @@ fn digraph_union( /// :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``. Default: ``None``. /// /// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are /// not. /// :rtype: bool -#[pyfunction(id_order = "true")] +#[pyfunction(id_order = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, call_limit=None)" )] fn digraph_is_isomorphic( py: Python, @@ -338,6 +342,7 @@ fn digraph_is_isomorphic( node_matcher: Option, edge_matcher: Option, id_order: bool, + call_limit: Option, ) -> PyResult { isomorphism::is_isomorphic( py, @@ -348,6 +353,7 @@ fn digraph_is_isomorphic( id_order, Ordering::Equal, true, + call_limit, ) } @@ -380,13 +386,17 @@ fn digraph_is_isomorphic( /// :param bool (default=True) id_order: If set to true, the algorithm matches the /// nodes in order specified by their ids. Otherwise, it uses a heuristic /// matching order based in [VF2]_ paper. +/// :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``. Default: ``None``. /// /// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are /// not. /// :rtype: bool -#[pyfunction(id_order = "true")] +#[pyfunction(id_order = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, call_limit=None)" )] fn graph_is_isomorphic( py: Python, @@ -395,6 +405,7 @@ fn graph_is_isomorphic( node_matcher: Option, edge_matcher: Option, id_order: bool, + call_limit: Option, ) -> PyResult { isomorphism::is_isomorphic( py, @@ -405,6 +416,7 @@ fn graph_is_isomorphic( id_order, Ordering::Equal, true, + call_limit, ) } @@ -444,13 +456,17 @@ fn graph_is_isomorphic( /// :param bool induced: If set to ``True`` this function will check the existence /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. +/// :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``. Default: ``None``. /// /// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, /// ``False`` if there is not. /// :rtype: bool -#[pyfunction(id_order = "false", induced = "true")] +#[pyfunction(id_order = "false", induced = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=False, induced=True, call_limit=None)" )] fn digraph_is_subgraph_isomorphic( py: Python, @@ -460,6 +476,7 @@ fn digraph_is_subgraph_isomorphic( edge_matcher: Option, id_order: bool, induced: bool, + call_limit: Option, ) -> PyResult { isomorphism::is_isomorphic( py, @@ -470,6 +487,7 @@ fn digraph_is_subgraph_isomorphic( id_order, Ordering::Greater, induced, + call_limit, ) } @@ -509,13 +527,17 @@ fn digraph_is_subgraph_isomorphic( /// :param bool induced: If set to ``True`` this function will check the existence /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. +/// :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``. Default: ``None``. /// /// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, /// ``False`` if there is not. /// :rtype: bool -#[pyfunction(id_order = "false", induced = "true")] +#[pyfunction(id_order = "false", induced = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=False, induced=True, call_limit=None)" )] fn graph_is_subgraph_isomorphic( py: Python, @@ -525,6 +547,7 @@ fn graph_is_subgraph_isomorphic( edge_matcher: Option, id_order: bool, induced: bool, + call_limit: Option, ) -> PyResult { isomorphism::is_isomorphic( py, @@ -535,6 +558,7 @@ fn graph_is_subgraph_isomorphic( id_order, Ordering::Greater, induced, + call_limit, ) } @@ -574,14 +598,17 @@ fn graph_is_subgraph_isomorphic( /// :param bool induced: If set to ``True`` this function will check the existence /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. +/// :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. Default: ``None``. /// /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] -#[pyfunction(id_order = "true", subgraph = "false", induced = "true")] +#[pyfunction(id_order = "true", subgraph = "false", induced = "true", call_limit = "None")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, - id_order=True, subgraph=False, induced=True)" + id_order=True, subgraph=False, induced=True, call_limit=None)" )] fn digraph_vf2_mapping( py: Python, @@ -592,6 +619,7 @@ fn digraph_vf2_mapping( id_order: bool, subgraph: bool, induced: bool, + call_limit: Option, ) -> isomorphism::DiGraphVf2Mapping { let ordering = if subgraph { Ordering::Greater @@ -608,6 +636,7 @@ fn digraph_vf2_mapping( id_order, ordering, induced, + call_limit, ) } @@ -646,14 +675,17 @@ fn digraph_vf2_mapping( /// :param bool induced: If set to ``True`` this function will check the existence /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. +/// :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. Default: ``None``. /// /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] -#[pyfunction(id_order = "true", subgraph = "false", induced = "true")] +#[pyfunction(id_order = "true", subgraph = "false", induced = "true", call_limit = "None")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, - id_order=True, subgraph=False, induced=True)" + id_order=True, subgraph=False, induced=True, call_limit=None)" )] fn graph_vf2_mapping( py: Python, @@ -664,6 +696,7 @@ fn graph_vf2_mapping( id_order: bool, subgraph: bool, induced: bool, + call_limit: Option, ) -> isomorphism::GraphVf2Mapping { let ordering = if subgraph { Ordering::Greater @@ -680,6 +713,7 @@ fn graph_vf2_mapping( id_order, ordering, induced, + call_limit, ) } From 42317bc5c46473f85d595fbc5f5c72c178c43f9a Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Fri, 23 Jul 2021 17:26:12 +0300 Subject: [PATCH 16/27] run cargo fmt --- src/lib.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c2ce46efbc..4468e5eae4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -645,7 +645,12 @@ fn graph_is_subgraph_isomorphic( /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] -#[pyfunction(id_order = "true", subgraph = "false", induced = "true", call_limit = "None")] +#[pyfunction( + id_order = "true", + subgraph = "false", + induced = "true", + call_limit = "None" +)] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=True, subgraph=False, induced=True, call_limit=None)" @@ -722,7 +727,12 @@ fn digraph_vf2_mapping( /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] -#[pyfunction(id_order = "true", subgraph = "false", induced = "true", call_limit = "None")] +#[pyfunction( + id_order = "true", + subgraph = "false", + induced = "true", + call_limit = "None" +)] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=True, subgraph=False, induced=True, call_limit=None)" From d62af072f201571c94f029f0c32627681ed3e8ff Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Fri, 23 Jul 2021 17:42:42 +0300 Subject: [PATCH 17/27] lint --- retworkx/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 0ae52424b1..3de59f6fd5 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -755,9 +755,9 @@ def is_isomorphic( :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``. Default: ``None``. + :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``. Default: ``None``. :returns: ``True`` if the 2 graphs are isomorphic, ``False`` if they are not. @@ -892,9 +892,9 @@ def is_subgraph_isomorphic( :param bool induced: If set to ``True`` this function will check the existence of a node-induced subgraph of first isomorphic to second graph. Default: ``True``. - :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``. Default: ``None``. + :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``. Default: ``None``. :returns: ``True`` if there is a subgraph of `first` isomorphic to `second` , ``False`` if there is not. @@ -1465,9 +1465,9 @@ def vf2_mapping( :param bool induced: If set to ``True`` this function will check the existence of a node-induced subgraph of first isomorphic to second graph. Default: ``True``. - :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. Default: ``None``. + :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. Default: ``None``. :returns: An iterator over dicitonaries of node indices from ``first`` to node indices in ``second`` representing the mapping found. From 41e7c5dfd915235b1227a093485513ad2a749291 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Thu, 12 Aug 2021 16:38:59 +0300 Subject: [PATCH 18/27] resolve all conflicts --- src/isomorphism/mod.rs | 295 ++++++++++++++++++++-------- src/shortest_path/floyd_warshall.rs | 10 + 2 files changed, 222 insertions(+), 83 deletions(-) diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 125ff2c623..0876ad98a0 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -50,13 +50,17 @@ use pyo3::Python; /// :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``. Default: ``None``. /// /// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are /// not. /// :rtype: bool -#[pyfunction(id_order = "true")] +#[pyfunction(id_order = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, call_limit=None)" )] fn digraph_is_isomorphic( py: Python, @@ -65,32 +69,19 @@ fn digraph_is_isomorphic( node_matcher: Option, edge_matcher: Option, id_order: bool, + call_limit: Option, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = vf2::is_isomorphic( + vf2::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Equal, true, - )?; - Ok(res) + call_limit, + ) } /// Determine if 2 undirected graphs are isomorphic @@ -122,13 +113,17 @@ fn digraph_is_isomorphic( /// :param bool (default=True) id_order: If set to true, the algorithm matches the /// nodes in order specified by their ids. Otherwise, it uses a heuristic /// matching order based in [VF2]_ paper. +/// :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``. Default: ``None``. /// /// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are /// not. /// :rtype: bool -#[pyfunction(id_order = "true")] +#[pyfunction(id_order = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, call_limit=None)" )] fn graph_is_isomorphic( py: Python, @@ -137,32 +132,19 @@ fn graph_is_isomorphic( node_matcher: Option, edge_matcher: Option, id_order: bool, + call_limit: Option, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = vf2::is_isomorphic( + vf2::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Equal, true, - )?; - Ok(res) + call_limit, + ) } /// Determine if 2 directed graphs are subgraph - isomorphic @@ -201,13 +183,17 @@ fn graph_is_isomorphic( /// :param bool induced: If set to ``True`` this function will check the existence /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. +/// :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``. Default: ``None``. /// /// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, /// ``False`` if there is not. /// :rtype: bool -#[pyfunction(id_order = "false", induced = "true")] +#[pyfunction(id_order = "false", induced = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=False, induced=True, call_limit=None)" )] fn digraph_is_subgraph_isomorphic( py: Python, @@ -217,32 +203,19 @@ fn digraph_is_subgraph_isomorphic( edge_matcher: Option, id_order: bool, induced: bool, + call_limit: Option, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = vf2::is_isomorphic( + vf2::is_isomorphic( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, Ordering::Greater, induced, - )?; - Ok(res) + call_limit, + ) } /// Determine if 2 undirected graphs are subgraph - isomorphic @@ -281,13 +254,17 @@ fn digraph_is_subgraph_isomorphic( /// :param bool induced: If set to ``True`` this function will check the existence /// of a node-induced subgraph of first isomorphic to second graph. /// Default: ``True``. +/// :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``. Default: ``None``. /// /// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, /// ``False`` if there is not. /// :rtype: bool -#[pyfunction(id_order = "false", induced = "true")] +#[pyfunction(id_order = "false", induced = "true", call_limit = "None")] #[pyo3( - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=False, induced=True, call_limit=None)" )] fn graph_is_subgraph_isomorphic( py: Python, @@ -297,30 +274,182 @@ fn graph_is_subgraph_isomorphic( edge_matcher: Option, id_order: bool, induced: bool, + call_limit: Option, ) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); + vf2::is_isomorphic( + py, + &first.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + Ordering::Greater, + induced, + call_limit, + ) +} - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); +/// Return an iterator over all vf2 mappings between two :class:`~retworkx.PyDiGraph` objects +/// +/// This funcion will run the vf2 algorithm used from +/// :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` +/// but instead of returning a boolean it will return an iterator over all possible +/// mapping of node ids found from ``first`` to ``second``. If the graphs are not +/// isomorphic then the iterator will be empty. A simple example that retrieves +/// one mapping would be:: +/// +/// graph_a = retworkx.generators.directed_path_graph(3) +/// graph_b = retworkx.generators.direccted_path_graph(2) +/// vf2 = retworkx.digraph_vf2_mapping(graph_a, graph_b, subgraph=True) +/// try: +/// mapping = next(vf2) +/// except StopIteration: +/// pass +/// +/// +/// :param PyDiGraph first: The first graph to find the mapping for +/// :param PyDiGraph second: The second graph to find the mapping for +/// :param node_matcher: An optional python callable object that takes 2 +/// positional arguments, one for each node data object in either graph. +/// If the return of this function evaluates to True then the nodes +/// passed to it are vieded as matching. +/// :param 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 bool subgraph: If set to ``True`` the function will return the +/// subgraph isomorphic found between the graphs. +/// :param bool induced: If set to ``True`` this function will check the existence +/// of a node-induced subgraph of first isomorphic to second graph. +/// Default: ``True``. +/// :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. Default: ``None``. +/// +/// :returns: An iterator over dicitonaries of node indices from ``first`` to node +/// indices in ``second`` representing the mapping found. +/// :rtype: Iterable[NodeMap] +#[pyfunction( + id_order = "true", + subgraph = "false", + induced = "true", + call_limit = "None" +)] +#[pyo3( + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, subgraph=False, induced=True, call_limit=None)" +)] +fn digraph_vf2_mapping( + py: Python, + first: &digraph::PyDiGraph, + second: &digraph::PyDiGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + subgraph: bool, + induced: bool, + call_limit: Option, +) -> vf2::DiGraphVf2Mapping { + let ordering = if subgraph { + Ordering::Greater + } else { + Ordering::Equal + }; - let res = vf2::is_isomorphic( + vf2::DiGraphVf2Mapping::new( py, &first.graph, &second.graph, - compare_nodes, - compare_edges, + node_matcher, + edge_matcher, id_order, - Ordering::Greater, + ordering, + induced, + call_limit, + ) +} + +/// Return an iterator over all vf2 mappings between two :class:`~retworkx.PyGraph` objects +/// +/// This funcion will run the vf2 algorithm used from +/// :func:`~retworkx.is_isomorphic` and :func:`~retworkx.is_subgraph_isomorphic` +/// but instead of returning a boolean it will return an iterator over all possible +/// mapping of node ids found from ``first`` to ``second``. If the graphs are not +/// isomorphic then the iterator will be empty. A simple example that retrieves +/// one mapping would be:: +/// +/// graph_a = retworkx.generators.path_graph(3) +/// graph_b = retworkx.generators.path_graph(2) +/// vf2 = retworkx.graph_vf2_mapping(graph_a, graph_b, subgraph=True) +/// try: +/// mapping = next(vf2) +/// except StopIteration: +/// pass +/// +/// :param PyGraph first: The first graph to find the mapping for +/// :param PyGraph second: The second graph to find the mapping for +/// :param node_matcher: An optional python callable object that takes 2 +/// positional arguments, one for each node data object in either graph. +/// If the return of this function evaluates to True then the nodes +/// passed to it are vieded as matching. +/// :param 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 bool subgraph: If set to ``True`` the function will return the +/// subgraph isomorphic found between the graphs. +/// :param bool induced: If set to ``True`` this function will check the existence +/// of a node-induced subgraph of first isomorphic to second graph. +/// Default: ``True``. +/// :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. Default: ``None``. +/// +/// :returns: An iterator over dicitonaries of node indices from ``first`` to node +/// indices in ``second`` representing the mapping found. +/// :rtype: Iterable[NodeMap] +#[pyfunction( + id_order = "true", + subgraph = "false", + induced = "true", + call_limit = "None" +)] +#[pyo3( + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, + id_order=True, subgraph=False, induced=True, call_limit=None)" +)] +fn graph_vf2_mapping( + py: Python, + first: &graph::PyGraph, + second: &graph::PyGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + subgraph: bool, + induced: bool, + call_limit: Option, +) -> vf2::GraphVf2Mapping { + let ordering = if subgraph { + Ordering::Greater + } else { + Ordering::Equal + }; + + vf2::GraphVf2Mapping::new( + py, + &first.graph, + &second.graph, + node_matcher, + edge_matcher, + id_order, + ordering, induced, - )?; - Ok(res) + call_limit, + ) } diff --git a/src/shortest_path/floyd_warshall.rs b/src/shortest_path/floyd_warshall.rs index dd02bdde82..3a28bbc293 100644 --- a/src/shortest_path/floyd_warshall.rs +++ b/src/shortest_path/floyd_warshall.rs @@ -28,6 +28,16 @@ use ndarray::prelude::*; use rayon::prelude::*; use crate::iterators::{AllPairsPathLengthMapping, PathLengthMapping}; +use crate::NodesRemoved; + +impl<'a, Ty> NodesRemoved for &'a StableGraph +where + Ty: EdgeType, +{ + fn nodes_removed(&self) -> bool { + self.node_bound() != self.node_count() + } +} pub fn floyd_warshall( py: Python, From 3ce7499e932c92f26cbaa77af13058b2b5a82b13 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Thu, 12 Aug 2021 17:43:17 +0300 Subject: [PATCH 19/27] update release note and add more tests --- .../notes/vf2-mapping-6fd49ab8b1b552c2.yaml | 5 +++ tests/digraph/test_isomorphic.py | 41 ++++--------------- tests/digraph/test_subgraph_isomorphic.py | 40 ++++++++++++++++++ tests/graph/test_isomorphic.py | 35 ++++------------ tests/graph/test_subgraph_isomorphic.py | 40 ++++++++++++++++++ 5 files changed, 101 insertions(+), 60 deletions(-) diff --git a/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml b/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml index 23f256af6b..f2055adb9d 100644 --- a/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml +++ b/releasenotes/notes/vf2-mapping-6fd49ab8b1b552c2.yaml @@ -19,3 +19,8 @@ features: print(mapping) except StopIteration: pass + - | + Added a new kwarg, ``call_limit`` to :func:`retworkx.is_isomorphic` and + :func:`retworkx.is_subgraph_isomorphic` which is used to set an upper + 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. \ No newline at end of file diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index 7e022fa5ac..484f5645a6 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -245,14 +245,6 @@ def test_digraph_vf2_mapping_identical_removals_first(self): mapping = retworkx.digraph_vf2_mapping(graph, second_graph) self.assertEqual({0: 0, 3: 1}, next(mapping)) - def test_subgraph_vf2_mapping(self): - graph = retworkx.generators.directed_grid_graph(10, 10) - second_graph = retworkx.generators.directed_grid_graph(2, 2) - mapping = retworkx.digraph_vf2_mapping( - graph, second_graph, subgraph=True - ) - self.assertEqual(next(mapping), {0: 0, 1: 1, 10: 2, 11: 3}) - def test_digraph_vf2_mapping_identical_vf2pp(self): graph = retworkx.generators.directed_grid_graph(2, 2) second_graph = retworkx.generators.directed_grid_graph(2, 2) @@ -261,7 +253,7 @@ def test_digraph_vf2_mapping_identical_vf2pp(self): ) self.assertEqual(next(mapping), {0: 0, 1: 1, 2: 2, 3: 3}) - def test_graph_vf2_mapping_identical_removals_vf2pp(self): + def test_digraph_vf2_mapping_identical_removals_vf2pp(self): graph = retworkx.generators.directed_path_graph(2) second_graph = retworkx.generators.directed_path_graph(4) second_graph.remove_nodes_from([1, 2]) @@ -271,7 +263,7 @@ def test_graph_vf2_mapping_identical_removals_vf2pp(self): ) self.assertEqual({0: 0, 1: 3}, next(mapping)) - def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): + def test_digraph_vf2_mapping_identical_removals_first_vf2pp(self): second_graph = retworkx.generators.directed_path_graph(2) graph = retworkx.generators.directed_path_graph(4) graph.remove_nodes_from([1, 2]) @@ -281,25 +273,10 @@ def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): ) self.assertEqual({0: 0, 3: 1}, next(mapping)) - def test_subgraph_vf2_mapping_vf2pp(self): - graph = retworkx.generators.directed_grid_graph(3, 3) - second_graph = retworkx.generators.directed_grid_graph(2, 2) - mapping = retworkx.digraph_vf2_mapping( - graph, second_graph, subgraph=True, id_order=False - ) - self.assertEqual(next(mapping), {4: 0, 5: 1, 7: 2, 8: 3}) - - def test_vf2pp_remapping(self): - temp = retworkx.generators.directed_grid_graph(3, 3) - - graph = retworkx.PyDiGraph() - dummy = graph.add_node(0) - - graph.compose(temp, dict()) - graph.remove_node(dummy) - - second_graph = retworkx.generators.directed_grid_graph(2, 2) - mapping = retworkx.digraph_vf2_mapping( - graph, second_graph, subgraph=True, id_order=False - ) - self.assertEqual(next(mapping), {5: 0, 6: 1, 8: 2, 9: 3}) + def test_digraph_vf2_number_of_valid_mappings(self): + graph = retworkx.generators.directed_mesh_graph(3) + mapping = retworkx.digraph_vf2_mapping(graph, graph, id_order=True) + total = 0 + for _ in mapping: + total += 1 + self.assertEqual(total, 6) diff --git a/tests/digraph/test_subgraph_isomorphic.py b/tests/digraph/test_subgraph_isomorphic.py index 48c833e2a2..c6df3c88f5 100644 --- a/tests/digraph/test_subgraph_isomorphic.py +++ b/tests/digraph/test_subgraph_isomorphic.py @@ -204,3 +204,43 @@ def test_non_induced_grid_subgraph_isomorphic(self): self.assertTrue( retworkx.is_subgraph_isomorphic(g_a, g_b, induced=False) ) + + def test_subgraph_vf2_mapping(self): + graph = retworkx.generators.directed_grid_graph(10, 10) + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True + ) + self.assertEqual(next(mapping), {0: 0, 1: 1, 10: 2, 11: 3}) + + def test_subgraph_vf2_all_mappings(self): + graph = retworkx.generators.directed_path_graph(3) + second_graph = retworkx.generators.directed_path_graph(2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=True + ) + self.assertEqual(next(mapping), {0: 0, 1: 1}) + self.assertEqual(next(mapping), {2: 1, 1: 0}) + + def test_subgraph_vf2_mapping_vf2pp(self): + graph = retworkx.generators.directed_grid_graph(3, 3) + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + self.assertEqual(next(mapping), {4: 0, 5: 1, 7: 2, 8: 3}) + + def test_vf2pp_remapping(self): + temp = retworkx.generators.directed_grid_graph(3, 3) + + graph = retworkx.PyDiGraph() + dummy = graph.add_node(0) + + graph.compose(temp, dict()) + graph.remove_node(dummy) + + second_graph = retworkx.generators.directed_grid_graph(2, 2) + mapping = retworkx.digraph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + self.assertEqual(next(mapping), {5: 0, 6: 1, 8: 2, 9: 3}) diff --git a/tests/graph/test_isomorphic.py b/tests/graph/test_isomorphic.py index 1d133005d0..9cd82acb96 100644 --- a/tests/graph/test_isomorphic.py +++ b/tests/graph/test_isomorphic.py @@ -254,12 +254,6 @@ def test_graph_vf2_mapping_identical_removals_first(self): ) self.assertEqual({0: 0, 3: 1}, next(mapping)) - def test_subgraph_vf2_mapping(self): - graph = retworkx.generators.grid_graph(10, 10) - second_graph = retworkx.generators.grid_graph(2, 2) - mapping = retworkx.graph_vf2_mapping(graph, second_graph, subgraph=True) - self.assertEqual(next(mapping), {0: 0, 1: 1, 10: 2, 11: 3}) - def test_graph_vf2_mapping_identical_vf2pp(self): graph = retworkx.generators.grid_graph(2, 2) second_graph = retworkx.generators.grid_graph(2, 2) @@ -288,25 +282,10 @@ def test_graph_vf2_mapping_identical_removals_first_vf2pp(self): ) self.assertEqual({0: 0, 3: 1}, next(mapping)) - def test_subgraph_vf2_mapping_vf2pp(self): - graph = retworkx.generators.grid_graph(3, 3) - second_graph = retworkx.generators.grid_graph(2, 2) - mapping = retworkx.graph_vf2_mapping( - graph, second_graph, subgraph=True, id_order=False - ) - self.assertEqual(next(mapping), {4: 0, 3: 2, 0: 3, 1: 1}) - - def test_vf2pp_remapping(self): - temp = retworkx.generators.grid_graph(3, 3) - - graph = retworkx.PyGraph() - dummy = graph.add_node(0) - - graph.compose(temp, dict()) - graph.remove_node(dummy) - - second_graph = retworkx.generators.grid_graph(2, 2) - mapping = retworkx.graph_vf2_mapping( - graph, second_graph, subgraph=True, id_order=False - ) - self.assertEqual(next(mapping), {5: 0, 4: 2, 1: 3, 2: 1}) + def test_graph_vf2_number_of_valid_mappings(self): + graph = retworkx.generators.mesh_graph(3) + mapping = retworkx.graph_vf2_mapping(graph, graph, id_order=True) + total = 0 + for _ in mapping: + total += 1 + self.assertEqual(total, 6) diff --git a/tests/graph/test_subgraph_isomorphic.py b/tests/graph/test_subgraph_isomorphic.py index 2a656e0681..7309082eaa 100644 --- a/tests/graph/test_subgraph_isomorphic.py +++ b/tests/graph/test_subgraph_isomorphic.py @@ -204,3 +204,43 @@ def test_non_induced_grid_subgraph_isomorphic(self): self.assertTrue( retworkx.is_subgraph_isomorphic(g_a, g_b, induced=False) ) + + def test_subgraph_vf2_mapping(self): + graph = retworkx.generators.grid_graph(10, 10) + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping(graph, second_graph, subgraph=True) + self.assertEqual(next(mapping), {0: 0, 1: 1, 10: 2, 11: 3}) + + def test_subgraph_vf2_all_mappings(self): + graph = retworkx.generators.path_graph(3) + second_graph = retworkx.generators.path_graph(2) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=True + ) + self.assertEqual(next(mapping), {0: 0, 1: 1}) + self.assertEqual(next(mapping), {0: 1, 1: 0}) + self.assertEqual(next(mapping), {2: 1, 1: 0}) + self.assertEqual(next(mapping), {1: 1, 2: 0}) + + def test_subgraph_vf2_mapping_vf2pp(self): + graph = retworkx.generators.grid_graph(3, 3) + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + self.assertEqual(next(mapping), {4: 0, 3: 2, 0: 3, 1: 1}) + + def test_vf2pp_remapping(self): + temp = retworkx.generators.grid_graph(3, 3) + + graph = retworkx.PyGraph() + dummy = graph.add_node(0) + + graph.compose(temp, dict()) + graph.remove_node(dummy) + + second_graph = retworkx.generators.grid_graph(2, 2) + mapping = retworkx.graph_vf2_mapping( + graph, second_graph, subgraph=True, id_order=False + ) + self.assertEqual(next(mapping), {5: 0, 4: 2, 1: 3, 2: 1}) From 83a3d01d098b2723ae527825a5b409807bdaa0be Mon Sep 17 00:00:00 2001 From: georgios-ts <45130028+georgios-ts@users.noreply.github.com> Date: Mon, 16 Aug 2021 17:28:15 +0300 Subject: [PATCH 20/27] call_limit does not need to be explicitly set to None Co-authored-by: Matthew Treinish --- retworkx/__init__.py | 4 ++-- src/isomorphism/mod.rs | 20 +++++++++----------- src/lib.rs | 1 - 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 3de59f6fd5..98a17f2072 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -757,7 +757,7 @@ def is_isomorphic( 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``. Default: ``None``. + the algorithm will stop and return ``False``. :returns: ``True`` if the 2 graphs are isomorphic, ``False`` if they are not. @@ -894,7 +894,7 @@ def is_subgraph_isomorphic( Default: ``True``. :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``. Default: ``None``. + the algorithm will stop and return ``False``. :returns: ``True`` if there is a subgraph of `first` isomorphic to `second` , ``False`` if there is not. diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 0876ad98a0..afe00d5620 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -52,12 +52,12 @@ use pyo3::Python; /// 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``. Default: ``None``. +/// will stop and return ``False``. /// /// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are /// not. /// :rtype: bool -#[pyfunction(id_order = "true", call_limit = "None")] +#[pyfunction(id_order = "true")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=True, call_limit=None)" @@ -115,12 +115,12 @@ fn digraph_is_isomorphic( /// matching order based in [VF2]_ paper. /// :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``. Default: ``None``. +/// will stop and return ``False``. /// /// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are /// not. /// :rtype: bool -#[pyfunction(id_order = "true", call_limit = "None")] +#[pyfunction(id_order = "true")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=True, call_limit=None)" @@ -185,12 +185,12 @@ fn graph_is_isomorphic( /// Default: ``True``. /// :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``. Default: ``None``. +/// will stop and return ``False``. /// /// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, /// ``False`` if there is not. /// :rtype: bool -#[pyfunction(id_order = "false", induced = "true", call_limit = "None")] +#[pyfunction(id_order = "false", induced = "true")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True, call_limit=None)" @@ -256,12 +256,12 @@ fn digraph_is_subgraph_isomorphic( /// Default: ``True``. /// :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``. Default: ``None``. +/// will stop and return ``False``. /// /// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, /// ``False`` if there is not. /// :rtype: bool -#[pyfunction(id_order = "false", induced = "true", call_limit = "None")] +#[pyfunction(id_order = "false", induced = "true")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True, call_limit=None)" @@ -327,7 +327,7 @@ fn graph_is_subgraph_isomorphic( /// Default: ``True``. /// :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. Default: ``None``. +/// will stop. /// /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. @@ -336,7 +336,6 @@ fn graph_is_subgraph_isomorphic( id_order = "true", subgraph = "false", induced = "true", - call_limit = "None" )] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, @@ -418,7 +417,6 @@ fn digraph_vf2_mapping( id_order = "true", subgraph = "false", induced = "true", - call_limit = "None" )] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, diff --git a/src/lib.rs b/src/lib.rs index 5532db83cc..3196057556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,6 @@ // under the License. #![allow(clippy::float_cmp)] -#![allow(clippy::too_many_arguments)] mod coloring; mod connectivity; From 4c5a9f1f8b23c611235ee1436a451788108a01a2 Mon Sep 17 00:00:00 2001 From: georgios-ts <45130028+georgios-ts@users.noreply.github.com> Date: Mon, 16 Aug 2021 17:34:19 +0300 Subject: [PATCH 21/27] simplify iteration in gc protocol Co-authored-by: Matthew Treinish --- src/isomorphism/vf2.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/isomorphism/vf2.rs b/src/isomorphism/vf2.rs index 5b4e7652d2..06f5ce7f49 100644 --- a/src/isomorphism/vf2.rs +++ b/src/isomorphism/vf2.rs @@ -977,16 +977,12 @@ macro_rules! vf2_mapping_impl { ) -> Result<(), PyTraverseError> { for j in 0..2 { for node in - self.vf2.st[j].graph.node_indices().map(|node| { - self.vf2.st[j].graph.node_weight(node).unwrap() - }) + self.vf2.st[j].graph.node_weights() { visit.call(node)?; } for edge in - self.vf2.st[j].graph.edge_indices().map(|edge| { - self.vf2.st[j].graph.edge_weight(edge).unwrap() - }) + self.vf2.st[j].graph.edge_weights() { visit.call(edge)?; } From 539f49185b06abbbdbc38ce45be6c77ec459e43d Mon Sep 17 00:00:00 2001 From: georgios-ts <45130028+georgios-ts@users.noreply.github.com> Date: Mon, 16 Aug 2021 17:36:37 +0300 Subject: [PATCH 22/27] update vf2 doc Co-authored-by: Matthew Treinish --- src/isomorphism/vf2.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/isomorphism/vf2.rs b/src/isomorphism/vf2.rs index 06f5ce7f49..c7c9e3e525 100644 --- a/src/isomorphism/vf2.rs +++ b/src/isomorphism/vf2.rs @@ -11,8 +11,9 @@ // under the License. #![allow(clippy::too_many_arguments)] -// This module is a forked version of petgraph's isomorphism module @ 0.5.0. -// It has then been modified to function with PyDiGraph inputs instead of Graph. +// 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 fixedbitset::FixedBitSet; use std::cmp::Ordering; From ecce089ce367b8136b68224abe40f932b28eb605 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Mon, 16 Aug 2021 18:52:39 +0300 Subject: [PATCH 23/27] cargo fmt + remove unnecessary mut ref in semantic matcher --- src/isomorphism/mod.rs | 12 ++---------- src/isomorphism/vf2.rs | 14 +++++--------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index afe00d5620..a7d205b626 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -332,11 +332,7 @@ fn graph_is_subgraph_isomorphic( /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] -#[pyfunction( - id_order = "true", - subgraph = "false", - induced = "true", -)] +#[pyfunction(id_order = "true", subgraph = "false", induced = "true")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=True, subgraph=False, induced=True, call_limit=None)" @@ -413,11 +409,7 @@ fn digraph_vf2_mapping( /// :returns: An iterator over dicitonaries of node indices from ``first`` to node /// indices in ``second`` representing the mapping found. /// :rtype: Iterable[NodeMap] -#[pyfunction( - id_order = "true", - subgraph = "false", - induced = "true", -)] +#[pyfunction(id_order = "true", subgraph = "false", induced = "true")] #[pyo3( text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=True, subgraph=False, induced=True, call_limit=None)" diff --git a/src/isomorphism/vf2.rs b/src/isomorphism/vf2.rs index c7c9e3e525..9e8bfab1ed 100644 --- a/src/isomorphism/vf2.rs +++ b/src/isomorphism/vf2.rs @@ -338,7 +338,7 @@ where trait SemanticMatcher { fn enabled(&self) -> bool; - fn eq(&mut self, _: Python, _: &T, _: &T) -> PyResult; + fn eq(&self, _: Python, _: &T, _: &T) -> PyResult; } impl SemanticMatcher for Option { @@ -347,8 +347,8 @@ impl SemanticMatcher for Option { self.is_some() } #[inline] - fn eq(&mut self, py: Python, a: &PyObject, b: &PyObject) -> PyResult { - let res = self.as_mut().unwrap().call1(py, (a, b))?; + fn eq(&self, py: Python, a: &PyObject, b: &PyObject) -> PyResult { + let res = self.as_ref().unwrap().call1(py, (a, b))?; res.is_true(py) } } @@ -977,14 +977,10 @@ macro_rules! vf2_mapping_impl { visit: PyVisit, ) -> Result<(), PyTraverseError> { for j in 0..2 { - for node in - self.vf2.st[j].graph.node_weights() - { + for node in self.vf2.st[j].graph.node_weights() { visit.call(node)?; } - for edge in - self.vf2.st[j].graph.edge_weights() - { + for edge in self.vf2.st[j].graph.edge_weights() { visit.call(edge)?; } } From 847a192a71936926211f4e6967068c176dc85274 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Mon, 16 Aug 2021 20:31:28 +0300 Subject: [PATCH 24/27] more tests --- tests/digraph/test_isomorphic.py | 56 ++++++++++++++++++++++++++++++++ tests/graph/test_isomorphic.py | 10 ++++++ 2 files changed, 66 insertions(+) diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index 484f5645a6..bcdcb9cc91 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -223,6 +223,62 @@ def test_isomorphic_compare_nodes_with_removals_deepcopy(self): ) ) + def test_digraph_isomorphic_self_loop(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from([0]) + graph.add_edges_from([(0, 0, "a")]) + self.assertTrue(retworkx.is_isomorphic(graph, graph)) + + def test_digraph_non_isomorphic_edge_mismatch_self_loop(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from([0]) + graph.add_edges_from([(0, 0, "a")]) + second_graph = retworkx.PyDiGraph() + second_graph.add_nodes_from([0]) + second_graph.add_edges_from([(0, 0, "b")]) + self.assertFalse( + retworkx.is_isomorphic( + graph, second_graph, edge_matcher=lambda x, y: x == y + ) + ) + + def test_digraph_non_isomorphic_rule_out_incoming(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from([0, 1, 2, 3]) + graph.add_edges_from_no_data([(0, 1), (0, 2), (2, 1)]) + second_graph = retworkx.PyDiGraph() + second_graph.add_nodes_from([0, 1, 2, 3]) + second_graph.add_edges_from_no_data([(0, 1), (0, 2), (3, 1)]) + self.assertFalse( + retworkx.is_isomorphic(graph, second_graph, id_order=True) + ) + + def test_digraph_non_isomorphic_rule_ins_outgoing(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from([0, 1, 2, 3]) + graph.add_edges_from_no_data([(1, 0), (2, 0), (1, 2)]) + second_graph = retworkx.PyDiGraph() + second_graph.add_nodes_from([0, 1, 2, 3]) + second_graph.add_edges_from_no_data([(1, 0), (2, 0), (1, 3)]) + self.assertFalse( + retworkx.is_isomorphic(graph, second_graph, id_order=True) + ) + + def test_digraph_non_isomorphic_rule_ins_incoming(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from([0, 1, 2, 3]) + graph.add_edges_from_no_data([(1, 0), (2, 0), (3, 1)]) + second_graph = retworkx.PyDiGraph() + second_graph.add_nodes_from([0, 1, 2, 3]) + second_graph.add_edges_from_no_data([(1, 0), (2, 0), (3, 1)]) + self.assertFalse( + retworkx.is_isomorphic(graph, second_graph, id_order=True) + ) + + def test_digraph_isomorphic_insufficient_call_limit(self): + graph = retworkx.generators.directed_path_graph(5) + self.assertFalse(retworkx.is_isomorphic(graph, graph, call_limit=2)) + def test_digraph_vf2_mapping_identical(self): graph = retworkx.generators.directed_grid_graph(2, 2) second_graph = retworkx.generators.directed_grid_graph(2, 2) diff --git a/tests/graph/test_isomorphic.py b/tests/graph/test_isomorphic.py index 9cd82acb96..425bdac004 100644 --- a/tests/graph/test_isomorphic.py +++ b/tests/graph/test_isomorphic.py @@ -229,6 +229,16 @@ def test_same_degrees_non_isomorphic(self): retworkx.is_isomorphic(g_a, g_b, id_order=id_order) ) + def test_graph_isomorphic_self_loop(self): + graph = retworkx.PyGraph() + graph.add_nodes_from([0, 1]) + graph.add_edges_from_no_data([(0, 0), (0, 1)]) + self.assertTrue(retworkx.is_isomorphic(graph, graph)) + + def test_graph_isomorphic_insufficient_call_limit(self): + graph = retworkx.generators.path_graph(5) + self.assertFalse(retworkx.is_isomorphic(graph, graph, call_limit=2)) + def test_graph_vf2_mapping_identical(self): graph = retworkx.generators.grid_graph(2, 2) second_graph = retworkx.generators.grid_graph(2, 2) From 32de477b7263fa368d9ecd5436899357b418a8a6 Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Mon, 16 Aug 2021 20:37:57 +0300 Subject: [PATCH 25/27] fix clippy error --- src/isomorphism/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index a7d205b626..1e00fe5a0f 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -11,6 +11,7 @@ // under the License. #![allow(clippy::float_cmp)] +#![allow(clippy::too_many_arguments)] mod vf2; From 085ac8e787db5f2adef7ce37ebcfe6b2f08da94d Mon Sep 17 00:00:00 2001 From: georgios-ts Date: Mon, 16 Aug 2021 20:50:58 +0300 Subject: [PATCH 26/27] fix failing test case --- tests/digraph/test_isomorphic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/digraph/test_isomorphic.py b/tests/digraph/test_isomorphic.py index bcdcb9cc91..171f2c9b30 100644 --- a/tests/digraph/test_isomorphic.py +++ b/tests/digraph/test_isomorphic.py @@ -267,7 +267,7 @@ def test_digraph_non_isomorphic_rule_ins_outgoing(self): def test_digraph_non_isomorphic_rule_ins_incoming(self): graph = retworkx.PyDiGraph() graph.add_nodes_from([0, 1, 2, 3]) - graph.add_edges_from_no_data([(1, 0), (2, 0), (3, 1)]) + graph.add_edges_from_no_data([(1, 0), (2, 0), (2, 1)]) second_graph = retworkx.PyDiGraph() second_graph.add_nodes_from([0, 1, 2, 3]) second_graph.add_edges_from_no_data([(1, 0), (2, 0), (3, 1)]) From 1573e7b44fa79035f95984cc772ee7122815f1d4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 16 Aug 2021 16:02:42 -0400 Subject: [PATCH 27/27] Fix docstring typo --- retworkx/__init__.py | 4 ++-- src/isomorphism/mod.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 98a17f2072..4a19de0bde 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -1452,10 +1452,10 @@ def vf2_mapping( :param node_matcher: An optional python callable object that takes 2 positional arguments, one for each node data object in either graph. If the return of this function evaluates to True then the nodes - passed to it are vieded as matching. + passed to it are viewed as matching. :param 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 + function evaluates to True then the edges passed to it are viewed 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 diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 1e00fe5a0f..8676b147fc 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -313,10 +313,10 @@ fn graph_is_subgraph_isomorphic( /// :param node_matcher: An optional python callable object that takes 2 /// positional arguments, one for each node data object in either graph. /// If the return of this function evaluates to True then the nodes -/// passed to it are vieded as matching. +/// passed to it are viewed as matching. /// :param 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 +/// function evaluates to True then the edges passed to it are viewed /// 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 @@ -390,10 +390,10 @@ fn digraph_vf2_mapping( /// :param node_matcher: An optional python callable object that takes 2 /// positional arguments, one for each node data object in either graph. /// If the return of this function evaluates to True then the nodes -/// passed to it are vieded as matching. +/// passed to it are viewed as matching. /// :param 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 +/// function evaluates to True then the edges passed to it are viewed /// 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