Skip to content

Commit

Permalink
Has path (#952)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
eliotheinrich and IvanIsCoding authored Aug 4, 2023
1 parent 9c4513d commit c77c4b4
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 0 deletions.
21 changes: 21 additions & 0 deletions releasenotes/notes/has_path-1addf94c4d29d455.yaml
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 33 additions & 0 deletions rustworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))?;
Expand Down
56 changes: 56 additions & 0 deletions src/shortest_path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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
Expand Down Expand Up @@ -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<bool> {
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
///
Expand Down
15 changes: 15 additions & 0 deletions tests/rustworkx_tests/digraph/test_dijkstra.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
15 changes: 15 additions & 0 deletions tests/rustworkx_tests/graph/test_dijkstra.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down

0 comments on commit c77c4b4

Please sign in to comment.