Skip to content

Commit

Permalink
Add bfs_predecessors (#819)
Browse files Browse the repository at this point in the history
This adds a bfs_predecessors method to complement bfs_successors.

* first test

* passing first test

* added breadth first test

* rustfmt

* remove debug tox config

* Update src/traversal/mod.rs

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* update bfs_successors to use Matthew's improvement to bfs_predecessors.

* add release notes

* Update releasenotes/notes/add_bfs_predecessors-751b468d1b3c01de.yaml

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* add to api docs

---------

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
  • Loading branch information
ewinston and mtreinish committed Apr 6, 2023
1 parent e025356 commit 1821aec
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 8 deletions.
2 changes: 2 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Traversal
rustworkx.dfs_edges
rustworkx.dfs_search
rustworkx.bfs_successors
rustworkx.bfs_predecessors
rustworkx.bfs_search
rustworkx.dijkstra_search
rustworkx.topological_sort
Expand Down Expand Up @@ -411,6 +412,7 @@ Custom Return Types
:toctree: apiref

rustworkx.BFSSuccessors
rustworkx.BFSPredecessors
rustworkx.NodeIndices
rustworkx.EdgeIndices
rustworkx.EdgeList
Expand Down
7 changes: 7 additions & 0 deletions releasenotes/notes/add_bfs_predecessors-751b468d1b3c01de.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
features:
- |
Added a new function, :func:`~.bfs_predecessors`, which is used
to return a list of predecessors in a reversed bread-first traversal
from a specified node. This is analogous to the existing
:func:`~.bfs_successors` method.
52 changes: 52 additions & 0 deletions src/iterators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,58 @@ impl PyGCProtocol for BFSSuccessors {
}
}

custom_vec_iter_impl!(
BFSPredecessors,
bfs_predecessors,
(PyObject, Vec<PyObject>),
"A custom class for the return from :func:`rustworkx.bfs_predecessors`
The class can is a read-only sequence of tuples of the form::
[(node, [predecessor_a, predecessor_b])]
where ``node``, ``predecessor_a``, and ``predecessor_b`` are the data payloads
for the nodes in the graph.
This class is a container class for the results of the
:func:`rustworkx.bfs_predecessors` function. It implements the Python
sequence protocol. So you can treat the return as read-only
sequence/list that is integer indexed. If you want to use it as an
iterator you can by wrapping it in an ``iter()`` that will yield the
results in order.
For example::
import rustworkx as rx
graph = rx.generators.directed_path_graph(5)
bfs_succ = rx.bfs_predecessors(0)
# Index based access
third_element = bfs_succ[2]
# Use as iterator
bfs_iter = iter(bfs_succ)
first_element = next(bfs_iter)
second_element = next(bfs_iter)
"
);

impl PyGCProtocol for BFSPredecessors {
fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
for node in &self.bfs_predecessors {
visit.call(&node.0)?;
for succ in &node.1 {
visit.call(succ)?;
}
}
Ok(())
}

fn __clear__(&mut self) {
self.bfs_predecessors = Vec::new();
}
}

custom_vec_iter_impl!(
NodeIndices,
nodes,
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
)?;
m.add("FailedToConverge", py.get_type::<FailedToConverge>())?;
m.add_wrapped(wrap_pyfunction!(bfs_successors))?;
m.add_wrapped(wrap_pyfunction!(bfs_predecessors))?;
m.add_wrapped(wrap_pyfunction!(graph_bfs_search))?;
m.add_wrapped(wrap_pyfunction!(digraph_bfs_search))?;
m.add_wrapped(wrap_pyfunction!(graph_dijkstra_search))?;
Expand Down Expand Up @@ -485,6 +486,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<graph::PyGraph>()?;
m.add_class::<toposort::TopologicalSorter>()?;
m.add_class::<iterators::BFSSuccessors>()?;
m.add_class::<iterators::BFSPredecessors>()?;
m.add_class::<iterators::Chains>()?;
m.add_class::<iterators::NodeIndices>()?;
m.add_class::<iterators::EdgeIndices>()?;
Expand Down
56 changes: 48 additions & 8 deletions src/traversal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,15 @@ pub fn bfs_successors(
let mut bfs = Bfs::new(&graph.graph, index);
let mut out_list: Vec<(PyObject, Vec<PyObject>)> = Vec::with_capacity(graph.node_count());
while let Some(nx) = bfs.next(&graph.graph) {
let children = graph
let successors: Vec<PyObject> = graph
.graph
.neighbors_directed(nx, petgraph::Direction::Outgoing);
let mut succesors: Vec<PyObject> = Vec::new();
for succ in children {
succesors.push(graph.graph.node_weight(succ).unwrap().clone_ref(py));
}
if !succesors.is_empty() {
.neighbors_directed(nx, petgraph::Direction::Outgoing)
.map(|pred| graph.graph.node_weight(pred).unwrap().clone_ref(py))
.collect();
if !successors.is_empty() {
out_list.push((
graph.graph.node_weight(nx).unwrap().clone_ref(py),
succesors,
successors,
));
}
}
Expand All @@ -166,6 +164,48 @@ pub fn bfs_successors(
}
}

/// Return predecessors in a breadth-first-search from a source node.
///
/// The return format is ``[(Parent Node, [Children Nodes])]`` in a bfs order
/// from the source node provided.
///
/// :param PyDiGraph graph: The DAG to get the bfs_predecessors from
/// :param int node: The index of the dag node to get the bfs predecessors for
///
/// :returns: A list of nodes's data and their children in bfs order. The
/// BFSPredecessors class that is returned is a custom container class that
/// implements the sequence protocol. This can be used as a python list
/// with index based access.
/// :rtype: BFSPredecessors
#[pyfunction]
#[pyo3(text_signature = "(graph, node, /)")]
pub fn bfs_predecessors(
py: Python,
graph: &digraph::PyDiGraph,
node: usize,
) -> iterators::BFSPredecessors {
let index = NodeIndex::new(node);
let reverse_graph = Reversed(&graph.graph);
let mut bfs = Bfs::new(reverse_graph, index);
let mut out_list: Vec<(PyObject, Vec<PyObject>)> = Vec::with_capacity(graph.node_count());
while let Some(nx) = bfs.next(reverse_graph) {
let predecessors: Vec<PyObject> = graph
.graph
.neighbors_directed(nx, petgraph::Direction::Incoming)
.map(|pred| graph.graph.node_weight(pred).unwrap().clone_ref(py))
.collect();
if !predecessors.is_empty() {
out_list.push((
graph.graph.node_weight(nx).unwrap().clone_ref(py),
predecessors,
));
}
}
iterators::BFSPredecessors {
bfs_predecessors: out_list,
}
}

/// Return the ancestors of a node in a graph.
///
/// This differs from :meth:`PyDiGraph.predecessors` method in that
Expand Down
135 changes: 135 additions & 0 deletions tests/rustworkx_tests/digraph/test_pred_succ.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,138 @@ def test_bfs_successors_sequence_stop_iterator(self):
next(res)
with self.assertRaises(StopIteration):
next(res)


class TestBfsPredecessors(unittest.TestCase):
def test_single_predecessor(self):
dag = rustworkx.PyDAG()
node_a = dag.add_node("a")
node_b = dag.add_child(node_a, "b", {"a": 1})
node_c = dag.add_child(node_b, "c", {"a": 2})
dag.add_child(node_c, "d", {"a": 1})
res = rustworkx.bfs_predecessors(dag, node_c)
res = rustworkx.bfs_predecessors(dag, node_c)
self.assertEqual([("c", ["b"]), ("b", ["a"])], res)

def test_many_parents(self):
dag = rustworkx.PyDAG()
parent_nodes = [dag.add_node({"parent": i}) for i in range(10)]
child = dag.add_node("child")
for i, parent in enumerate(parent_nodes):
dag.add_edge(parent, child, {"edge": i})
for i in range(10):
dag.add_child(child, {"grand_child": i}, {"gc_edge": i})
res = rustworkx.bfs_predecessors(dag, child)
self.assertEqual(
[
(
"child",
[
{"parent": 9},
{"parent": 8},
{"parent": 7},
{"parent": 6},
{"parent": 5},
{"parent": 4},
{"parent": 3},
{"parent": 2},
{"parent": 1},
{"parent": 0},
],
)
],
res,
)

def test_breadth_first(self):
dag = rustworkx.PyDAG()
layers = []
parent_cnt = 8
layers.append([dag.add_node({"layer1": i}) for i in range(parent_cnt)])
child_cnt = parent_cnt / 2
layers.append(
[
dag.add_child(parent1, {"layer2": i}, {})
for i, parent1 in enumerate(layers[-1][0::2])
]
)
for parent2, child in zip(layers[-2][1::2], layers[-1]):
dag.add_edge(parent2, child, {})

parent_cnt = child_cnt
child_cnt = parent_cnt / 2
layers.append(
[
dag.add_child(parent1, {"layer3": i}, {})
for i, parent1 in enumerate(layers[-1][0::2])
]
)
for parent2, child in zip(layers[-2][1::2], layers[-1]):
dag.add_edge(parent2, child, {})

parent_cnt = child_cnt
child_cnt = parent_cnt / 2
layers.append(
[
dag.add_child(parent1, {"layer4": i}, {})
for i, parent1 in enumerate(layers[-1][0::2])
]
)
for parent2, child in zip(layers[-2][1::2], layers[-1]):
dag.add_edge(parent2, child, {})

res = rustworkx.bfs_predecessors(dag, child)
self.assertEqual(
res,
[
(
{"layer4": 0},
[
{"layer3": 1},
{"layer3": 0},
],
),
(
{"layer3": 1},
[
{"layer2": 3},
{"layer2": 2},
],
),
(
{"layer3": 0},
[
{"layer2": 1},
{"layer2": 0},
],
),
(
{"layer2": 3},
[
{"layer1": 7},
{"layer1": 6},
],
),
(
{"layer2": 2},
[
{"layer1": 5},
{"layer1": 4},
],
),
(
{"layer2": 1},
[
{"layer1": 3},
{"layer1": 2},
],
),
(
{"layer2": 0},
[
{"layer1": 1},
{"layer1": 0},
],
),
],
)

0 comments on commit 1821aec

Please sign in to comment.