From fe4b681ed113977948f2e6d4cf4a68e563e55156 Mon Sep 17 00:00:00 2001 From: eliotheinrich <38039898+eliotheinrich@users.noreply.github.com> Date: Fri, 4 Aug 2023 01:02:16 -0400 Subject: [PATCH] Has path (#952) * Added has_path for PyGraph and PyDiGraph * Fixed linting errors and added release notes * Fixed doc type * Fixed cargo fmt problem * Fixed clippy problem * Typo in release notes * Apply suggestions from code review * Remove whitespace --------- Co-authored-by: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> --- .../notes/has_path-1addf94c4d29d455.yaml | 21 +++++++ rustworkx/__init__.py | 33 +++++++++++ src/lib.rs | 2 + src/shortest_path/mod.rs | 56 +++++++++++++++++++ .../rustworkx_tests/digraph/test_dijkstra.py | 15 +++++ tests/rustworkx_tests/graph/test_dijkstra.py | 15 +++++ 6 files changed, 142 insertions(+) create mode 100644 releasenotes/notes/has_path-1addf94c4d29d455.yaml diff --git a/releasenotes/notes/has_path-1addf94c4d29d455.yaml b/releasenotes/notes/has_path-1addf94c4d29d455.yaml new file mode 100644 index 0000000000..2e036c6693 --- /dev/null +++ b/releasenotes/notes/has_path-1addf94c4d29d455.yaml @@ -0,0 +1,21 @@ +--- +features: + - | + Added :func:`~rustworkx.has_path` which accepts as arguments a :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph` and checks if there is a path from source to destination + + .. jupyter-execute:: + + from rustworkx import PyDiGraph, has_path + + graph = PyDiGraph() + a = graph.add_node("A") + b = graph.add_node("B") + c = graph.add_node("C") + edge_list = [(a, b, 1), (b, c, 1)] + graph.add_edges_from(edge_list) + + path_exists = has_path(graph, a, c) + assert(path_exists == True) + + path_exists = has_path(graph, c, a) + assert(path_exists == False) diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index fba037d844..a42e16cc8d 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -587,6 +587,39 @@ def _graph_dijkstra_shortest_path(graph, source, target=None, weight_fn=None, de ) +@functools.singledispatch +def has_path( + graph, + source, + target, + as_undirected=False, +): + """Checks if a path exists between a source and target node + + :param graph: The input graph to use. Can either be a + :class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph` + :param int source: The node index to find paths from + :param int target: The index of the target node + :param bool as_undirected: If set to true the graph will be treated as + undirected for finding existence of a path. This only works with a + :class:`~rustworkx.PyDiGraph` input for ``graph`` + + :return: True if a path exists, False if not + :rtype: bool + """ + raise TypeError("Invalid Input Type %s for graph" % type(graph)) + + +@has_path.register(PyDiGraph) +def _digraph_has_path(graph, source, target, as_undirected=False): + return digraph_has_path(graph, source, target=target, as_undirected=as_undirected) + + +@has_path.register(PyGraph) +def _graph_has_path(graph, source, target): + return graph_has_path(graph, source, target=target) + + @functools.singledispatch def all_pairs_dijkstra_shortest_paths(graph, edge_cost_fn): """For each node in the graph, finds the shortest paths to all others. diff --git a/src/lib.rs b/src/lib.rs index a58f182898..430b043151 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -400,6 +400,8 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(digraph_all_simple_paths))?; m.add_wrapped(wrap_pyfunction!(graph_dijkstra_shortest_paths))?; m.add_wrapped(wrap_pyfunction!(digraph_dijkstra_shortest_paths))?; + m.add_wrapped(wrap_pyfunction!(graph_has_path))?; + m.add_wrapped(wrap_pyfunction!(digraph_has_path))?; m.add_wrapped(wrap_pyfunction!(graph_dijkstra_shortest_path_lengths))?; m.add_wrapped(wrap_pyfunction!(digraph_dijkstra_shortest_path_lengths))?; m.add_wrapped(wrap_pyfunction!(graph_bellman_ford_shortest_paths))?; diff --git a/src/shortest_path/mod.rs b/src/shortest_path/mod.rs index 229741da5b..0ced803391 100644 --- a/src/shortest_path/mod.rs +++ b/src/shortest_path/mod.rs @@ -109,6 +109,32 @@ pub fn graph_dijkstra_shortest_paths( }) } +/// Check if a graph has a path between source and target nodes +/// +/// :param PyGraph graph: +/// :param int source: The node index to find paths from +/// :param int target: The index of the target node +/// +/// :return: True if a path exists, False if not. +/// :rtype: bool +/// :raises ValueError: when an edge weight with NaN or negative value +/// is provided. +#[pyfunction] +#[pyo3( + signature=(graph, source, target), + text_signature = "(graph, source, target)" +)] +pub fn graph_has_path( + py: Python, + graph: &graph::PyGraph, + source: usize, + target: usize, +) -> PyResult { + let path_mapping = graph_dijkstra_shortest_paths(py, graph, source, Some(target), None, 1.0)?; + + Ok(!path_mapping.paths.is_empty()) +} + /// Find the shortest path from a node /// /// This function will generate the shortest path from a source node using @@ -184,6 +210,36 @@ pub fn digraph_dijkstra_shortest_paths( }) } +/// Check if a digraph has a path between source and target nodes +/// +/// :param PyDiGraph graph: +/// :param int source: The node index to find paths from +/// :param int target: The index of the target node +/// :param bool as_undirected: If set to true the graph will be treated as +/// undirected for finding a path +/// +/// :return: True if a path exists, False if not. +/// :rtype: bool +/// :raises ValueError: when an edge weight with NaN or negative value +/// is provided. +#[pyfunction] +#[pyo3( + signature=(graph, source, target, as_undirected=false), + text_signature = "(graph, source, target, /, as_undirected=false)" +)] +pub fn digraph_has_path( + py: Python, + graph: &digraph::PyDiGraph, + source: usize, + target: usize, + as_undirected: bool, +) -> PyResult { + let path_mapping = + digraph_dijkstra_shortest_paths(py, graph, source, Some(target), None, 1.0, as_undirected)?; + + Ok(!path_mapping.paths.is_empty()) +} + /// Compute the lengths of the shortest paths for a PyGraph object using /// Dijkstra's algorithm /// diff --git a/tests/rustworkx_tests/digraph/test_dijkstra.py b/tests/rustworkx_tests/digraph/test_dijkstra.py index 81180faab4..7ec1465898 100644 --- a/tests/rustworkx_tests/digraph/test_dijkstra.py +++ b/tests/rustworkx_tests/digraph/test_dijkstra.py @@ -70,6 +70,21 @@ def test_dijkstra_path(self): } self.assertEqual(expected, paths) + def test_dijkstra_has_path(self): + g = rustworkx.PyDiGraph() + a = g.add_node("A") + b = g.add_node("B") + c = g.add_node("C") + + edge_list = [ + (a, b, 7), + (c, b, 9), + (c, b, 10), + ] + g.add_edges_from(edge_list) + + self.assertFalse(rustworkx.digraph_has_path(g, a, c)) + def test_dijkstra_path_with_weight_fn(self): paths = rustworkx.digraph_dijkstra_shortest_paths(self.graph, self.a, weight_fn=lambda x: x) expected = { diff --git a/tests/rustworkx_tests/graph/test_dijkstra.py b/tests/rustworkx_tests/graph/test_dijkstra.py index e8625d32e1..3405036fe7 100644 --- a/tests/rustworkx_tests/graph/test_dijkstra.py +++ b/tests/rustworkx_tests/graph/test_dijkstra.py @@ -50,6 +50,21 @@ def test_dijkstra_path(self): expected = {4: [self.a, self.c, self.d, self.e]} self.assertEqual(expected, path) + def test_dijkstra_has_path(self): + g = rustworkx.PyGraph() + a = g.add_node("A") + b = g.add_node("B") + c = g.add_node("C") + + edge_list = [ + (a, b, 7), + (c, b, 9), + (c, b, 10), + ] + g.add_edges_from(edge_list) + + self.assertTrue(rustworkx.graph_has_path(g, a, c)) + def test_dijkstra_with_no_goal_set(self): path = rustworkx.graph_dijkstra_shortest_path_lengths(self.graph, self.a, lambda x: 1) expected = {1: 1.0, 2: 1.0, 3: 1.0, 4: 2.0, 5: 2.0}