From a10e45390983475839eee5b8f06db7df25f155cc Mon Sep 17 00:00:00 2001 From: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> Date: Thu, 2 Jun 2022 05:52:03 -0700 Subject: [PATCH] Add All-Pairs Bellman-Ford (#611) * 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 --- docs/source/api.rst | 6 + ...l-pairs-bellman-ford-3e7a3d0ded31427f.yaml | 9 + retworkx/__init__.py | 92 ++++++++ src/lib.rs | 10 + src/shortest_path/all_pairs_bellman_ford.rs | 220 ++++++++++++++++++ src/shortest_path/mod.rs | 153 ++++++++++++ tests/digraph/test_bellman_ford.py | 122 ++++++++++ tests/graph/test_bellman_ford.py | 117 ++++++++++ 8 files changed, 729 insertions(+) create mode 100644 releasenotes/notes/all-pairs-bellman-ford-3e7a3d0ded31427f.yaml create mode 100644 src/shortest_path/all_pairs_bellman_ford.rs diff --git a/docs/source/api.rst b/docs/source/api.rst index 310f1b174..ab105d197 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -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 @@ -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 @@ -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 diff --git a/releasenotes/notes/all-pairs-bellman-ford-3e7a3d0ded31427f.yaml b/releasenotes/notes/all-pairs-bellman-ford-3e7a3d0ded31427f.yaml new file mode 100644 index 000000000..e683712ee --- /dev/null +++ b/releasenotes/notes/all-pairs-bellman-ford-3e7a3d0ded31427f.yaml @@ -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` \ No newline at end of file diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 0f6ed7979..209fafbea 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -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) diff --git a/src/lib.rs b/src/lib.rs index 023b9d059..94586bc45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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))?; diff --git a/src/shortest_path/all_pairs_bellman_ford.rs b/src/shortest_path/all_pairs_bellman_ford.rs new file mode 100644 index 000000000..19a53fa4b --- /dev/null +++ b/src/shortest_path/all_pairs_bellman_ford.rs @@ -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( + py: Python, + graph: &StablePyGraph, + edge_cost_fn: PyObject, +) -> PyResult { + 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> = + edge_weights_from_callable(py, graph, &Some(edge_cost_fn), 1.0)?; + let edge_cost = |e: EdgeIndex| -> PyResult { + 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 = graph.node_indices().collect(); + let out_map: DictMap = node_indices + .into_par_iter() + .map(|x| { + if *negative_cycle.read().unwrap() { + return ( + x.index(), + PathLengthMapping { + path_lengths: DictMap::new(), + }, + ); + } + + let path_lengths: Option>> = + 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( + py: Python, + graph: &StablePyGraph, + edge_cost_fn: PyObject, +) -> PyResult { + 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> = + edge_weights_from_callable(py, graph, &Some(edge_cost_fn), 1.0)?; + let edge_cost = |e: EdgeIndex| -> PyResult { + match edge_weights[e.index()] { + Some(weight) => Ok(weight), + None => Err(PyIndexError::new_err("No edge found for index")), + } + }; + + let node_indices: Vec = 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> = + DictMap::with_capacity(graph.node_count()); + let path_lengths: Option>> = + 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) +} diff --git a/src/shortest_path/mod.rs b/src/shortest_path/mod.rs index 859977580..91ce12bab 100644 --- a/src/shortest_path/mod.rs +++ b/src/shortest_path/mod.rs @@ -10,6 +10,7 @@ // License for the specific language governing permissions and limitations // under the License. +mod all_pairs_bellman_ford; pub mod all_pairs_dijkstra; mod average_length; mod distance_matrix; @@ -1588,3 +1589,155 @@ pub fn find_negative_cycle( nodes: cycle.into_iter().map(|x| x.index()).collect(), }) } + +/// For each node in the graph, calculates the lengths of the shortest paths +/// to all others in a :class:`~retworkx.PyDiGraph` object +/// +/// This function will calculate 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 :class:`~retworkx.PyDiGraph` to use +/// :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 source +/// node indices and the values are dicts 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. +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn digraph_all_pairs_bellman_ford_path_lengths( + py: Python, + graph: &digraph::PyDiGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_bellman_ford::all_pairs_bellman_ford_path_lengths(py, &graph.graph, edge_cost_fn) +} + +/// For each node in the graph, finds the shortest paths to all others in a +/// :class:`~retworkx.PyDiGraph` object +/// +/// This function will generate the shortest paths from all nodes in the graph +/// 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 :class:`~retworkx.PyDiGraph` object to use +/// :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 dicts of the target node and the list of the +/// node indices making up the shortest path to that node. 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. +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn digraph_all_pairs_bellman_ford_shortest_paths( + py: Python, + graph: &digraph::PyDiGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_bellman_ford::all_pairs_bellman_ford_shortest_paths(py, &graph.graph, edge_cost_fn) +} + +/// For each node in the graph, calculates the lengths of the shortest paths +/// to all others in a :class:`~retworkx.PyGraph` object +/// +/// This function will generate the shortest path from a source node using +/// the Bellman-Ford algorithm. +/// +/// :param graph: The input :class:`~retworkx.PyGraph` to use +/// :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 source +/// node indices and the values are dicts 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. +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn graph_all_pairs_bellman_ford_path_lengths( + py: Python, + graph: &graph::PyGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_bellman_ford::all_pairs_bellman_ford_path_lengths(py, &graph.graph, edge_cost_fn) +} + +/// For each node in the graph, finds the shortest paths to all others in a +/// :class:`~retworkx.PyGraph` object +/// +/// This function will generate the shortest path from a source node using +/// the Bellman-Ford algorithm. +/// +/// :param graph: The input :class:`~retworkx.PyGraph` object to use +/// :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 destination node +/// indices and the values are dicts of the target node and the list of the +/// node indices making up the shortest path to that node. 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. +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn graph_all_pairs_bellman_ford_shortest_paths( + py: Python, + graph: &graph::PyGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_bellman_ford::all_pairs_bellman_ford_shortest_paths(py, &graph.graph, edge_cost_fn) +} diff --git a/tests/digraph/test_bellman_ford.py b/tests/digraph/test_bellman_ford.py index 99a722ef8..3d5a9ef24 100644 --- a/tests/digraph/test_bellman_ford.py +++ b/tests/digraph/test_bellman_ford.py @@ -314,3 +314,125 @@ def test_find_negative_cycle_no_cycle(self): with self.assertRaises(ValueError): retworkx.find_negative_cycle(graph, edge_cost_fn=float) + + def test_bellman_ford_all_pair_path_lengths(self): + lengths = retworkx.digraph_all_pairs_bellman_ford_path_lengths(self.graph, float) + expected = { + 0: {1: 7.0, 2: 16.0, 3: 14.0, 4: 23.0, 5: 22.0}, + 1: {0: 19.0, 2: 10.0, 3: 33.0, 4: 42.0, 5: 15.0}, + 2: {0: 9.0, 1: 16.0, 3: 23.0, 4: 32.0, 5: 11.0}, + 3: {0: 11.0, 1: 18.0, 2: 2.0, 4: 9.0, 5: 13.0}, + 4: {5: 6.0}, + 5: {}, + } + self.assertEqual(expected, lengths) + + def test_bellman_ford_all_pair_paths(self): + paths = retworkx.digraph_all_pairs_bellman_ford_shortest_paths(self.graph, float) + expected = { + 0: {1: [0, 1], 2: [0, 3, 2], 3: [0, 3], 4: [0, 3, 4], 5: [0, 1, 5]}, + 1: { + 0: [1, 2, 0], + 2: [1, 2], + 3: [1, 2, 0, 3], + 4: [1, 2, 0, 3, 4], + 5: [1, 5], + }, + 2: { + 0: [2, 0], + 1: [2, 0, 1], + 3: [2, 0, 3], + 4: [2, 0, 3, 4], + 5: [2, 5], + }, + 3: { + 0: [3, 2, 0], + 1: [3, 2, 0, 1], + 2: [3, 2], + 4: [3, 4], + 5: [3, 2, 5], + }, + 4: {5: [4, 5]}, + 5: {}, + } + self.assertEqual(expected, paths) + + def test_bellman_ford_all_pair_path_lengths_with_node_removal(self): + self.graph.remove_node(3) + lengths = retworkx.digraph_all_pairs_bellman_ford_path_lengths(self.graph, float) + expected = { + 0: {1: 7.0, 2: 17.0, 5: 22.0}, + 1: {0: 19.0, 2: 10.0, 5: 15.0}, + 2: {0: 9.0, 1: 16.0, 5: 11.0}, + 4: {5: 6.0}, + 5: {}, + } + self.assertEqual(expected, lengths) + + def test_bellman_ford_all_pair_paths_with_node_removal(self): + self.graph.remove_node(3) + lengths = retworkx.digraph_all_pairs_bellman_ford_shortest_paths(self.graph, float) + expected = { + 0: {1: [0, 1], 2: [0, 1, 2], 5: [0, 1, 5]}, + 1: {0: [1, 2, 0], 2: [1, 2], 5: [1, 5]}, + 2: {0: [2, 0], 1: [2, 0, 1], 5: [2, 5]}, + 4: {5: [4, 5]}, + 5: {}, + } + self.assertEqual(expected, lengths) + + def test_bellman_ford_all_pair_path_lengths_empty_graph(self): + graph = retworkx.PyDiGraph() + self.assertEqual({}, retworkx.digraph_all_pairs_bellman_ford_path_lengths(graph, float)) + + def test_bellman_ford_all_pair_shortest_paths_empty_graph(self): + graph = retworkx.PyDiGraph() + self.assertEqual({}, retworkx.digraph_all_pairs_bellman_ford_shortest_paths(graph, float)) + + def test_bellman_ford_all_pair_path_lengths_graph_no_edges(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from(list(range(1000))) + expected = {x: {} for x in range(1000)} + self.assertEqual( + expected, + retworkx.digraph_all_pairs_bellman_ford_path_lengths(graph, float), + ) + + def test_bellman_ford_all_pair_shortest_paths_no_edges(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from(list(range(1000))) + expected = {x: {} for x in range(1000)} + self.assertEqual( + expected, + retworkx.digraph_all_pairs_bellman_ford_shortest_paths(graph, float), + ) + + def test_raises_negative_cycle_all_pairs_bellman_ford_paths(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from(list(range(4))) + graph.add_edges_from( + [ + (0, 1, 1), + (1, 2, -1), + (2, 3, -1), + (3, 0, -1), + ] + ) + + with self.assertRaises(retworkx.NegativeCycle): + retworkx.all_pairs_bellman_ford_shortest_paths(graph, float) + + def test_raises_negative_cycle_all_pairs_bellman_ford_path_lenghts(self): + graph = retworkx.PyDiGraph() + graph.add_nodes_from(list(range(4))) + graph.add_edges_from( + [ + (0, 1, 1), + (1, 2, -1), + (2, 3, -1), + (3, 0, -1), + ] + ) + + with self.assertRaises(retworkx.NegativeCycle): + retworkx.all_pairs_bellman_ford_path_lengths(graph, float) diff --git a/tests/graph/test_bellman_ford.py b/tests/graph/test_bellman_ford.py index e89f8109c..6511e3ea3 100644 --- a/tests/graph/test_bellman_ford.py +++ b/tests/graph/test_bellman_ford.py @@ -189,3 +189,120 @@ def test_raises_negative_cycle_bellman_ford_path_lenghts(self): with self.assertRaises(retworkx.NegativeCycle): retworkx.bellman_ford_shortest_path_lengths(graph, 0, edge_cost_fn=float) + + def test_bellman_all_pair_path_lengths(self): + lengths = retworkx.all_pairs_bellman_ford_path_lengths(self.graph, float) + expected = { + 0: {1: 7.0, 2: 9.0, 3: 11.0, 4: 20.0, 5: 20.0}, + 1: {0: 7.0, 2: 10.0, 3: 12.0, 4: 21.0, 5: 15.0}, + 2: {0: 9.0, 1: 10.0, 3: 2.0, 4: 11.0, 5: 11.0}, + 3: {0: 11.0, 1: 12.0, 2: 2.0, 4: 9.0, 5: 13.0}, + 4: {0: 20.0, 1: 21.0, 2: 11.0, 3: 9.0, 5: 6.0}, + 5: {0: 20.0, 1: 15.0, 2: 11.0, 3: 13.0, 4: 6.0}, + } + self.assertEqual(expected, lengths) + + def test_bellman_ford_all_pair_paths(self): + paths = retworkx.graph_all_pairs_bellman_ford_shortest_paths(self.graph, float) + expected = { + 0: { + 1: [0, 1], + 2: [0, 2], + 3: [0, 2, 3], + 4: [0, 2, 3, 4], + 5: [0, 2, 5], + }, + 1: {0: [1, 0], 2: [1, 2], 3: [1, 2, 3], 4: [1, 5, 4], 5: [1, 5]}, + 2: {0: [2, 0], 1: [2, 1], 3: [2, 3], 4: [2, 3, 4], 5: [2, 5]}, + 3: {0: [3, 2, 0], 1: [3, 2, 1], 2: [3, 2], 4: [3, 4], 5: [3, 2, 5]}, + 4: { + 0: [4, 3, 2, 0], + 1: [4, 5, 1], + 2: [4, 3, 2], + 3: [4, 3], + 5: [4, 5], + }, + 5: {0: [5, 2, 0], 1: [5, 1], 2: [5, 2], 3: [5, 2, 3], 4: [5, 4]}, + } + + self.assertEqual(expected, paths) + + def test_bellman_ford_all_pair_path_lengths_with_node_removal(self): + self.graph.remove_node(3) + lengths = retworkx.graph_all_pairs_bellman_ford_path_lengths(self.graph, float) + expected = { + 0: {1: 7.0, 2: 9.0, 4: 26.0, 5: 20.0}, + 1: {0: 7.0, 2: 10.0, 4: 21.0, 5: 15.0}, + 2: {0: 9.0, 1: 10.0, 4: 17.0, 5: 11.0}, + 4: {0: 26.0, 1: 21.0, 2: 17.0, 5: 6.0}, + 5: {0: 20.0, 1: 15.0, 2: 11.0, 4: 6.0}, + } + self.assertEqual(expected, lengths) + + def test_bellman_ford_all_pair_paths_with_node_removal(self): + self.graph.remove_node(3) + paths = retworkx.graph_all_pairs_bellman_ford_shortest_paths(self.graph, float) + expected = { + 0: {1: [0, 1], 2: [0, 2], 4: [0, 2, 5, 4], 5: [0, 2, 5]}, + 1: {0: [1, 0], 2: [1, 2], 4: [1, 5, 4], 5: [1, 5]}, + 2: {0: [2, 0], 1: [2, 1], 4: [2, 5, 4], 5: [2, 5]}, + 4: {0: [4, 5, 2, 0], 1: [4, 5, 1], 2: [4, 5, 2], 5: [4, 5]}, + 5: {0: [5, 2, 0], 1: [5, 1], 2: [5, 2], 4: [5, 4]}, + } + self.assertEqual(expected, paths) + + def test_bellman_ford_all_pair_path_lengths_empty_graph(self): + graph = retworkx.PyGraph() + self.assertEqual({}, retworkx.graph_all_pairs_bellman_ford_path_lengths(graph, float)) + + def test_bellman_ford_all_pair_shortest_paths_empty_graph(self): + graph = retworkx.PyGraph() + self.assertEqual({}, retworkx.graph_all_pairs_bellman_ford_shortest_paths(graph, float)) + + def test_bellman_ford_all_pair_path_lengths_graph_no_edges(self): + graph = retworkx.PyGraph() + graph.add_nodes_from(list(range(1000))) + expected = {x: {} for x in range(1000)} + self.assertEqual( + expected, + retworkx.graph_all_pairs_bellman_ford_path_lengths(graph, float), + ) + + def test_bellman_ford_all_pair_shortest_paths_no_edges(self): + graph = retworkx.PyGraph() + graph.add_nodes_from(list(range(1000))) + expected = {x: {} for x in range(1000)} + self.assertEqual( + expected, + retworkx.graph_all_pairs_bellman_ford_shortest_paths(graph, float), + ) + + def test_raises_negative_cycle_all_pairs_bellman_ford_paths(self): + graph = retworkx.PyGraph() + graph.add_nodes_from(list(range(4))) + graph.add_edges_from( + [ + (0, 1, 1), + (1, 2, -1), + (2, 3, -1), + (3, 0, -1), + ] + ) + + with self.assertRaises(retworkx.NegativeCycle): + retworkx.all_pairs_bellman_ford_shortest_paths(graph, float) + + def test_raises_negative_cycle_all_pairs_bellman_ford_path_lenghts(self): + graph = retworkx.PyGraph() + graph.add_nodes_from(list(range(4))) + graph.add_edges_from( + [ + (0, 1, 1), + (1, 2, -1), + (2, 3, -1), + (3, 0, -1), + ] + ) + + with self.assertRaises(retworkx.NegativeCycle): + retworkx.all_pairs_bellman_ford_path_lengths(graph, float)