Skip to content

Commit

Permalink
Add shortest_path and shortest_path_length
Browse files Browse the repository at this point in the history
  • Loading branch information
eriknw committed Mar 11, 2024
1 parent 46be698 commit 0a85e73
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 32 deletions.
4 changes: 3 additions & 1 deletion python/nx-cugraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ Below is the list of algorithms that are currently supported in nx-cugraph.
└─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.reciprocity.reciprocity.html#networkx.algorithms.reciprocity.reciprocity">reciprocity</a>
<a href="https://networkx.org/documentation/stable/reference/algorithms/shortest_paths.html">shortest_paths</a>
├─ <a href="https://networkx.org/documentation/stable/reference/algorithms/shortest_paths.html#module-networkx.algorithms.shortest_paths.generic">generic</a>
│ └─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.has_path.html#networkx.algorithms.shortest_paths.generic.has_path">has_path</a>
│ ├─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.has_path.html#networkx.algorithms.shortest_paths.generic.has_path">has_path</a>
│ ├─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.shortest_path.html#networkx.algorithms.shortest_paths.generic.shortest_path">shortest_path</a>
│ └─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.shortest_path_length.html#networkx.algorithms.shortest_paths.generic.shortest_path_length">shortest_path_length</a>
├─ <a href="https://networkx.org/documentation/stable/reference/algorithms/shortest_paths.html#module-networkx.algorithms.shortest_paths.unweighted">unweighted</a>
│ ├─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path.html#networkx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path">all_pairs_shortest_path</a>
│ ├─ <a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path_length.html#networkx.algorithms.shortest_paths.unweighted.all_pairs_shortest_path_length">all_pairs_shortest_path_length</a>
Expand Down
10 changes: 10 additions & 0 deletions python/nx-cugraph/_nx_cugraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"reciprocity",
"reverse",
"sedgewick_maze_graph",
"shortest_path",
"shortest_path_length",
"single_source_bellman_ford",
"single_source_bellman_ford_path",
"single_source_bellman_ford_path_length",
Expand Down Expand Up @@ -163,6 +165,8 @@
"katz_centrality": "`nstart` isn't used (but is checked), and `normalized=False` is not supported.",
"louvain_communities": "`seed` parameter is currently ignored, and self-loops are not yet supported.",
"pagerank": "`dangling` parameter is not supported, but it is checked for validity.",
"shortest_path": "Negative weights are not yet supported, and method is ununsed.",
"shortest_path_length": "Negative weights are not yet supported, and method is ununsed.",
"single_source_bellman_ford": "Negative cycles are not yet supported! ``NotImplementedError`` will be raised if there are negative edge weights. We plan to support negative edge weights soon. Also, callable ``weight`` argument is not supported.",
"single_source_bellman_ford_path": "Negative cycles are not yet supported! ``NotImplementedError`` will be raised if there are negative edge weights. We plan to support negative edge weights soon. Also, callable ``weight`` argument is not supported.",
"single_source_bellman_ford_path_length": "Negative cycles are not yet supported! ``NotImplementedError`` will be raised if there are negative edge weights. We plan to support negative edge weights soon. Also, callable ``weight`` argument is not supported.",
Expand Down Expand Up @@ -200,6 +204,12 @@
"pagerank": {
"dtype : dtype or None, optional": "The data type (np.float32, np.float64, or None) to use for the edge weights in the algorithm. If None, then dtype is determined by the edge values.",
},
"shortest_path": {
"dtype : dtype or None, optional": "The data type (np.float32, np.float64, or None) to use for the edge weights in the algorithm. If None, then dtype is determined by the edge values.",
},
"shortest_path_length": {
"dtype : dtype or None, optional": "The data type (np.float32, np.float64, or None) to use for the edge weights in the algorithm. If None, then dtype is determined by the edge values.",
},
"single_source_bellman_ford": {
"dtype : dtype or None, optional": "The data type (np.float32, np.float64, or None) to use for the edge weights in the algorithm. If None, then dtype is determined by the edge values.",
},
Expand Down
4 changes: 2 additions & 2 deletions python/nx-cugraph/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ repos:
- id: black
# - id: black-jupyter
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.1
rev: v0.3.2
hooks:
- id: ruff
args: [--fix-only, --show-fixes] # --unsafe-fixes]
Expand All @@ -77,7 +77,7 @@ repos:
additional_dependencies: [tomli]
files: ^(nx_cugraph|docs)/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.1
rev: v0.3.2
hooks:
- id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
137 changes: 136 additions & 1 deletion python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import networkx as nx
import numpy as np

import nx_cugraph as nxcg
from nx_cugraph.utils import networkx_algorithm
from nx_cugraph.convert import _to_graph
from nx_cugraph.utils import _dtype_param, _get_float_dtype, networkx_algorithm

from .unweighted import _bfs
from .weighted import _sssp

__all__ = [
"shortest_path",
"shortest_path_length",
"has_path",
]

Expand All @@ -28,3 +35,131 @@ def has_path(G, source, target):
except nx.NetworkXNoPath:
return False
return True


@networkx_algorithm(
extra_params=_dtype_param, version_added="24.04", _plc={"bfs", "sssp"}
)
def shortest_path(
G, source=None, target=None, weight=None, method="dijkstra", *, dtype=None
):
"""Negative weights are not yet supported, and method is ununsed."""
if method not in {"dijkstra", "bellman-ford"}:
raise ValueError(f"method not supported: {method}")
if weight is None:
method = "unweighted"
if source is None:
if target is None:
# All pairs
if method == "unweighted":
paths = nxcg.all_pairs_shortest_path(G)
else:
# method == "dijkstra":
# method == 'bellman-ford':
paths = nxcg.all_pairs_bellman_ford_path(G, weight=weight, dtype=dtype)
if nx.__version__[:3] <= "3.4":
paths = dict(paths)
# To target
elif method == "unweighted":
paths = nxcg.single_target_shortest_path(G, target)
else:
# method == "dijkstra":
# method == 'bellman-ford':
# XXX: it seems weird that `reverse_path=True` is necessary here
G = _to_graph(G, weight, 1, np.float32)
dtype = _get_float_dtype(dtype, graph=G, weight=weight)
paths = _sssp(
G, target, weight, return_type="path", dtype=dtype, reverse_path=True
)
elif target is None:
# From source
if method == "unweighted":
paths = nxcg.single_source_shortest_path(G, source)
else:
# method == "dijkstra":
# method == 'bellman-ford':
paths = nxcg.single_source_bellman_ford_path(
G, source, weight=weight, dtype=dtype
)
# From source to target
elif method == "unweighted":
paths = nxcg.bidirectional_shortest_path(G, source, target)
else:
# method == "dijkstra":
# method == 'bellman-ford':
paths = nxcg.bellman_ford_path(G, source, target, weight, dtype=dtype)
return paths


@shortest_path._can_run
def _(G, source=None, target=None, weight=None, method="dijkstra", *, dtype=None):
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(
extra_params=_dtype_param, version_added="24.04", _plc={"bfs", "sssp"}
)
def shortest_path_length(
G, source=None, target=None, weight=None, method="dijkstra", *, dtype=None
):
"""Negative weights are not yet supported, and method is ununsed."""
if method not in {"dijkstra", "bellman-ford"}:
raise ValueError(f"method not supported: {method}")
if weight is None:
method = "unweighted"
if source is None:
if target is None:
# All pairs
if method == "unweighted":
lengths = nxcg.all_pairs_shortest_path_length(G)
else:
# method == "dijkstra":
# method == 'bellman-ford':
lengths = nxcg.all_pairs_bellman_ford_path_length(
G, weight=weight, dtype=dtype
)
# To target
elif method == "unweighted":
lengths = nxcg.single_target_shortest_path_length(G, target)
if nx.__version__[:3] <= "3.4":
lengths = dict(lengths)
else:
# method == "dijkstra":
# method == 'bellman-ford':
lengths = nxcg.single_source_bellman_ford_path_length(
G, target, weight=weight, dtype=dtype
)
elif target is None:
# From source
if method == "unweighted":
lengths = nxcg.single_source_shortest_path_length(G, source)
else:
# method == "dijkstra":
# method == 'bellman-ford':
lengths = dict(
nxcg.single_source_bellman_ford_path_length(
G, source, weight=weight, dtype=dtype
)
)
# From source to target
elif method == "unweighted":
G = _to_graph(G)
lengths = _bfs(G, source, None, "Source", return_type="length", target=target)
else:
# method == "dijkstra":
# method == 'bellman-ford':
lengths = nxcg.bellman_ford_path_length(G, source, target, weight, dtype=dtype)
return lengths


@shortest_path_length._can_run
def _(G, source=None, target=None, weight=None, method="dijkstra", *, dtype=None):
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)
60 changes: 48 additions & 12 deletions python/nx-cugraph/nx_cugraph/algorithms/shortest_paths/weighted.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ def bellman_ford_path(G, source, target, weight="weight", *, dtype=None):

@bellman_ford_path._can_run
def _(G, source, target, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(extra_params=_dtype_param, version_added="24.04", _plc="sssp")
Expand All @@ -67,7 +71,11 @@ def bellman_ford_path_length(G, source, target, weight="weight", *, dtype=None):

@bellman_ford_path_length._can_run
def _(G, source, target, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(extra_params=_dtype_param, version_added="24.04", _plc="sssp")
Expand All @@ -80,7 +88,11 @@ def single_source_bellman_ford_path(G, source, weight="weight", *, dtype=None):

@single_source_bellman_ford_path._can_run
def _(G, source, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(extra_params=_dtype_param, version_added="24.04", _plc="sssp")
Expand All @@ -93,7 +105,11 @@ def single_source_bellman_ford_path_length(G, source, weight="weight", *, dtype=

@single_source_bellman_ford_path_length._can_run
def _(G, source, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(extra_params=_dtype_param, version_added="24.04", _plc="sssp")
Expand All @@ -106,7 +122,11 @@ def single_source_bellman_ford(G, source, target=None, weight="weight", *, dtype

@single_source_bellman_ford._can_run
def _(G, source, target=None, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(extra_params=_dtype_param, version_added="24.04", _plc="sssp")
Expand All @@ -121,7 +141,11 @@ def all_pairs_bellman_ford_path_length(G, weight="weight", *, dtype=None):

@all_pairs_bellman_ford_path_length._can_run
def _(G, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


@networkx_algorithm(extra_params=_dtype_param, version_added="24.04", _plc="sssp")
Expand All @@ -136,10 +160,14 @@ def all_pairs_bellman_ford_path(G, weight="weight", *, dtype=None):

@all_pairs_bellman_ford_path._can_run
def _(G, weight="weight", *, dtype=None):
return not callable(weight) and not nx.is_negatively_weighted(G, weight=weight)
return (
weight is None
or not callable(weight)
and not nx.is_negatively_weighted(G, weight=weight)
)


def _sssp(G, source, weight, target=None, *, return_type, dtype):
def _sssp(G, source, weight, target=None, *, return_type, dtype, reverse_path=False):
"""SSSP for weighted shortest paths.
Parameters
Expand Down Expand Up @@ -194,6 +222,7 @@ def _sssp(G, source, weight, target=None, *, return_type, dtype):
return_type=return_type,
target=target,
scale=edge_val,
reverse_path=reverse_path,
)

src_index = source if G.key_to_id is None else G.key_to_id[source]
Expand Down Expand Up @@ -226,8 +255,11 @@ def _sssp(G, source, weight, target=None, *, return_type, dtype):
cur = d[cur]
paths.append(cur)
if (id_to_key := G.id_to_key) is not None:
paths = [id_to_key[cur] for cur in reversed(paths)]
else:
if reverse_path:
paths = [id_to_key[cur] for cur in paths]
else:
paths = [id_to_key[cur] for cur in reversed(paths)]
elif not reverse_path:
paths.reverse()
else:
groups = _groupby(predecessors[mask], node_ids)
Expand All @@ -239,8 +271,12 @@ def _sssp(G, source, weight, target=None, *, return_type, dtype):
pred = preds.pop()
pred_path = paths[pred]
nodes = G._nodearray_to_list(groups[pred])
for node in nodes:
paths[node] = [*pred_path, node]
if reverse_path:
for node in nodes:
paths[node] = [node, *pred_path]
else:
for node in nodes:
paths[node] = [*pred_path, node]
preds.extend(nodes & groups.keys())
if return_type == "path":
return paths
Expand Down
30 changes: 14 additions & 16 deletions python/nx-cugraph/nx_cugraph/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def key(testpath):
no_multigraph = "multigraphs not currently supported"
louvain_different = "Louvain may be different due to RNG"
no_string_dtype = "string edge values not currently supported"
sssp_path_different = "sssp may choose a different valid path"

xfail = {
# This is removed while strongly_connected_components() is not
Expand All @@ -77,6 +78,19 @@ def key(testpath):
# "test_strongly_connected.py:"
# "TestStronglyConnected.test_condensation_mapping_and_members"
# ): "Strongly connected groups in different iteration order",
key(
"test_cycles.py:TestMinimumCycleBasis.test_unweighted_diamond"
): sssp_path_different,
key(
"test_cycles.py:TestMinimumCycleBasis.test_weighted_diamond"
): sssp_path_different,
key(
"test_cycles.py:TestMinimumCycleBasis.test_petersen_graph"
): sssp_path_different,
key(
"test_cycles.py:TestMinimumCycleBasis."
"test_gh6787_and_edge_attribute_names"
): sssp_path_different,
}

from packaging.version import parse
Expand Down Expand Up @@ -265,23 +279,7 @@ def key(testpath):
)

too_slow = "Too slow to run"
negative_cycle = "negative cycles are not yet handled"
skip = {
key(
"test_weighted.py:TestBellmanFordAndGoldbergRadzik.test_negative_cycle"
): negative_cycle,
key(
"test_weighted.py:TestBellmanFordAndGoldbergRadzik."
"test_negative_weight_bf_path"
): negative_cycle,
# key(
# "test_weighted.py:TestBellmanFordAndGoldbergRadzik."
# "test_negative_cycle_consistency"
# ): negative_cycle,
# key(
# "test_weighted.py:TestBellmanFordAndGoldbergRadzik."
# "test_zero_cycle"
# ): negative_cycle,
key("test_tree_isomorphism.py:test_positive"): too_slow,
key("test_tree_isomorphism.py:test_negative"): too_slow,
# These repeatedly call `bfs_layers`, which converts the graph every call
Expand Down
Empty file modified python/nx-cugraph/scripts/update_readme.py
100644 → 100755
Empty file.

0 comments on commit 0a85e73

Please sign in to comment.