Skip to content

Commit

Permalink
Add All-Pairs Bellman-Ford (Qiskit#611)
Browse files Browse the repository at this point in the history
* Add all_pairs_bellman_ford to Rust code

* Add Python dispatches and docs

* Add tests

* Tweak release note

* Early return if negative cycle already exists
  • Loading branch information
IvanIsCoding authored Jun 2, 2022
1 parent 0931d26 commit a10e453
Show file tree
Hide file tree
Showing 8 changed files with 729 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Shortest Paths
retworkx.all_pairs_dijkstra_path_lengths
retworkx.bellman_ford_shortest_paths
retworkx.bellman_ford_shortest_path_lengths
retworkx.all_pairs_bellman_ford_shortest_paths
retworkx.all_pairs_bellman_ford_path_lengths
retworkx.negative_edge_cycle
retworkx.find_negative_cycle
retworkx.distance_matrix
Expand Down Expand Up @@ -289,6 +291,8 @@ the functions from the explicitly typed based on the data type.
retworkx.digraph_all_pairs_dijkstra_path_lengths
retworkx.digraph_bellman_ford_shortest_path_lengths
retworkx.digraph_bellman_ford_shortest_path_lengths
retworkx.digraph_all_pairs_bellman_ford_shortest_paths
retworkx.digraph_all_pairs_bellman_ford_path_lengths
retworkx.digraph_k_shortest_path_lengths
retworkx.digraph_dfs_edges
retworkx.digraph_dfs_search
Expand Down Expand Up @@ -341,6 +345,8 @@ typed API based on the data type.
retworkx.graph_all_pairs_dijkstra_path_lengths
retworkx.graph_bellman_ford_shortest_path_lengths
retworkx.graph_bellman_ford_shortest_path_lengths
retworkx.graph_all_pairs_bellman_ford_shortest_paths
retworkx.graph_all_pairs_bellman_ford_path_lengths
retworkx.graph_dfs_edges
retworkx.graph_dfs_search
retworkx.graph_transitivity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
features:
- |
Added new functions to compute the all-pairs shortest path
in graphs with negative edge weights using the Bellman-Ford
algorithm with the SPFA heuristic:
* :func:`retworkx.all_pairs_bellman_ford_path_lengths`
* :func:`retworkx.all_pairs_bellman_ford_shortest_paths`
92 changes: 92 additions & 0 deletions retworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2184,3 +2184,95 @@ def _digraph_bellman_ford_shortest_path_lengths(graph, node, edge_cost_fn, goal=
@bellman_ford_shortest_path_lengths.register(PyGraph)
def _graph_bellman_ford_shortest_path_lengths(graph, node, edge_cost_fn, goal=None):
return graph_bellman_ford_shortest_path_lengths(graph, node, edge_cost_fn, goal=goal)


@functools.singledispatch
def all_pairs_bellman_ford_path_lengths(graph, edge_cost_fn):
"""For each node in the graph, calculates the lengths of the shortest paths to all others.
This function will generate the shortest path lengths from all nodes in the
graph using the Bellman-Ford algorithm. This function is multithreaded and will
launch a thread pool with threads equal to the number of CPUs by
default. You can tune the number of threads with the ``RAYON_NUM_THREADS``
environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would
limit the thread pool to 4 threads.
:param graph: The input graph to use. Can either be a
:class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`
:param edge_cost_fn: A callable object that acts as a weight function for
an edge. It will accept a single positional argument, the edge's weight
object and will return a float which will be used to represent the
weight/cost of the edge
:return: A read-only dictionary of path lengths. The keys are the source
node indices and the values are a dict of the target node and the
length of the shortest path to that node. For example::
{
0: {1: 2.0, 2: 2.0},
1: {2: 1.0},
2: {0: 1.0},
}
:rtype: AllPairsPathLengthMapping
:raises: :class:`~retworkx.NegativeCycle`: when there is a negative cycle and the shortest
path is not defined
"""
raise TypeError("Invalid Input Type %s for graph" % type(graph))


@all_pairs_bellman_ford_path_lengths.register(PyDiGraph)
def _digraph_all_pairs_bellman_ford_path_lengths(graph, edge_cost_fn):
return digraph_all_pairs_bellman_ford_path_lengths(graph, edge_cost_fn)


@all_pairs_bellman_ford_path_lengths.register(PyGraph)
def _graph_all_pairs_bellman_ford_path_lengths(graph, edge_cost_fn):
return graph_all_pairs_bellman_ford_path_lengths(graph, edge_cost_fn)


@functools.singledispatch
def all_pairs_bellman_ford_shortest_paths(graph, edge_cost_fn):
"""For each node in the graph, finds the shortest paths to all others.
This function will generate the shortest path from all nodes in the graph
using the Bellman-Ford algorithm. This function is multithreaded and will run
launch a thread pool with threads equal to the number of CPUs by default.
You can tune the number of threads with the ``RAYON_NUM_THREADS``
environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would
limit the thread pool to 4 threads.
:param graph: The input graph to use. Can either be a
:class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`
:param edge_cost_fn: A callable object that acts as a weight function for
an edge. It will accept a single positional argument, the edge's weight
object and will return a float which will be used to represent the
weight/cost of the edge
:return: A read-only dictionary of paths. The keys are source node
indices and the values are a dict of target node indices and a list
of node indices making the path. For example::
{
0: {1: [0, 1], 2: [0, 1, 2]},
1: {2: [1, 2]},
2: {0: [2, 0]},
}
:rtype: AllPairsPathMapping
:raises: :class:`~retworkx.NegativeCycle`: when there is a negative cycle and the shortest
path is not defined
"""
raise TypeError("Invalid Input Type %s for graph" % type(graph))


@all_pairs_bellman_ford_shortest_paths.register(PyDiGraph)
def _digraph_all_pairs_bellman_ford_shortest_path(graph, edge_cost_fn):
return digraph_all_pairs_bellman_ford_shortest_paths(graph, edge_cost_fn)


@all_pairs_bellman_ford_shortest_paths.register(PyGraph)
def _graph_all_pairs_bellman_ford_shortest_path(graph, edge_cost_fn):
return graph_all_pairs_bellman_ford_shortest_paths(graph, edge_cost_fn)
10 changes: 10 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(digraph_all_pairs_dijkstra_shortest_paths))?;
m.add_wrapped(wrap_pyfunction!(graph_all_pairs_dijkstra_path_lengths))?;
m.add_wrapped(wrap_pyfunction!(graph_all_pairs_dijkstra_shortest_paths))?;
m.add_wrapped(wrap_pyfunction!(
digraph_all_pairs_bellman_ford_path_lengths
))?;
m.add_wrapped(wrap_pyfunction!(
digraph_all_pairs_bellman_ford_shortest_paths
))?;
m.add_wrapped(wrap_pyfunction!(graph_all_pairs_bellman_ford_path_lengths))?;
m.add_wrapped(wrap_pyfunction!(
graph_all_pairs_bellman_ford_shortest_paths
))?;
m.add_wrapped(wrap_pyfunction!(graph_betweenness_centrality))?;
m.add_wrapped(wrap_pyfunction!(digraph_betweenness_centrality))?;
m.add_wrapped(wrap_pyfunction!(graph_astar_shortest_path))?;
Expand Down
220 changes: 220 additions & 0 deletions src/shortest_path/all_pairs_bellman_ford.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

use retworkx_core::dictmap::*;
use retworkx_core::shortest_path::bellman_ford;

use std::sync::RwLock;

use pyo3::exceptions::PyIndexError;
use pyo3::prelude::*;
use pyo3::Python;

use petgraph::graph::NodeIndex;
use petgraph::prelude::*;
use petgraph::EdgeType;

use rayon::prelude::*;

use crate::iterators::{
AllPairsPathLengthMapping, AllPairsPathMapping, PathLengthMapping, PathMapping,
};
use crate::{edge_weights_from_callable, NegativeCycle, StablePyGraph};

pub fn all_pairs_bellman_ford_path_lengths<Ty: EdgeType + Sync>(
py: Python,
graph: &StablePyGraph<Ty>,
edge_cost_fn: PyObject,
) -> PyResult<AllPairsPathLengthMapping> {
if graph.node_count() == 0 {
return Ok(AllPairsPathLengthMapping {
path_lengths: DictMap::new(),
});
} else if graph.edge_count() == 0 {
return Ok(AllPairsPathLengthMapping {
path_lengths: graph
.node_indices()
.map(|i| {
(
i.index(),
PathLengthMapping {
path_lengths: DictMap::new(),
},
)
})
.collect(),
});
}
let edge_weights: Vec<Option<f64>> =
edge_weights_from_callable(py, graph, &Some(edge_cost_fn), 1.0)?;
let edge_cost = |e: EdgeIndex| -> PyResult<f64> {
match edge_weights[e.index()] {
Some(weight) => Ok(weight),
None => Err(PyIndexError::new_err("No edge found for index")),
}
};

let negative_cycle = RwLock::new(false);

let node_indices: Vec<NodeIndex> = graph.node_indices().collect();
let out_map: DictMap<usize, PathLengthMapping> = node_indices
.into_par_iter()
.map(|x| {
if *negative_cycle.read().unwrap() {
return (
x.index(),
PathLengthMapping {
path_lengths: DictMap::new(),
},
);
}

let path_lengths: Option<Vec<Option<f64>>> =
bellman_ford(graph, x, |e| edge_cost(e.id()), None).unwrap();

if path_lengths.is_none() {
let mut cycle = negative_cycle.write().unwrap();
*cycle = true;
return (
x.index(),
PathLengthMapping {
path_lengths: DictMap::new(),
},
);
}

let out_map = PathLengthMapping {
path_lengths: path_lengths
.unwrap()
.into_iter()
.enumerate()
.filter_map(|(index, opt_cost)| {
if index != x.index() {
opt_cost.map(|cost| (index, cost))
} else {
None
}
})
.collect(),
};
(x.index(), out_map)
})
.collect();

if *negative_cycle.read().unwrap() {
return Err(NegativeCycle::new_err(
"The shortest-path is not defined because there is a negative cycle",
));
}

Ok(AllPairsPathLengthMapping {
path_lengths: out_map,
})
}

pub fn all_pairs_bellman_ford_shortest_paths<Ty: EdgeType + Sync>(
py: Python,
graph: &StablePyGraph<Ty>,
edge_cost_fn: PyObject,
) -> PyResult<AllPairsPathMapping> {
if graph.node_count() == 0 {
return Ok(AllPairsPathMapping {
paths: DictMap::new(),
});
} else if graph.edge_count() == 0 {
return Ok(AllPairsPathMapping {
paths: graph
.node_indices()
.map(|i| {
(
i.index(),
PathMapping {
paths: DictMap::new(),
},
)
})
.collect(),
});
}
let edge_weights: Vec<Option<f64>> =
edge_weights_from_callable(py, graph, &Some(edge_cost_fn), 1.0)?;
let edge_cost = |e: EdgeIndex| -> PyResult<f64> {
match edge_weights[e.index()] {
Some(weight) => Ok(weight),
None => Err(PyIndexError::new_err("No edge found for index")),
}
};

let node_indices: Vec<NodeIndex> = graph.node_indices().collect();

let negative_cycle = RwLock::new(false);

let out_map = AllPairsPathMapping {
paths: node_indices
.into_par_iter()
.map(|x| {
if *negative_cycle.read().unwrap() {
return (
x.index(),
PathMapping {
paths: DictMap::new(),
},
);
}

let mut paths: DictMap<NodeIndex, Vec<NodeIndex>> =
DictMap::with_capacity(graph.node_count());
let path_lengths: Option<Vec<Option<f64>>> =
bellman_ford(graph, x, |e| edge_cost(e.id()), Some(&mut paths)).unwrap();

if path_lengths.is_none() {
let mut cycle = negative_cycle.write().unwrap();
*cycle = true;
return (
x.index(),
PathMapping {
paths: DictMap::new(),
},
);
}

let index = x.index();

let out_paths = PathMapping {
paths: paths
.iter()
.filter_map(|path_mapping| {
let path_index = path_mapping.0.index();
if index != path_index {
Some((
path_index,
path_mapping.1.iter().map(|x| x.index()).collect(),
))
} else {
None
}
})
.collect(),
};
(index, out_paths)
})
.collect(),
};

if *negative_cycle.read().unwrap() {
return Err(NegativeCycle::new_err(
"The shortest-path is not defined because there is a negative cycle",
));
}

Ok(out_map)
}
Loading

0 comments on commit a10e453

Please sign in to comment.