Skip to content

Commit

Permalink
Add floyd_warshall (#42)
Browse files Browse the repository at this point in the history
* Add `floyd_warshall`

* Optimization: better handle sparsity such as skip empty nodes

* Simplify (so this PR can be a reference)

* Better name ("Outer", not "temp")
  • Loading branch information
eriknw authored Feb 2, 2023
1 parent 140bea8 commit 6dd93bd
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ dispatch pattern shown above.
- is_k_regular
- is_regular
- Shortest Paths
- floyd_warshall
- has_path
- Simple Paths
- is_simple_path
Expand Down
1 change: 1 addition & 0 deletions graphblas_algorithms/algorithms/shortest_paths/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .dense import *
from .generic import *
47 changes: 47 additions & 0 deletions graphblas_algorithms/algorithms/shortest_paths/dense.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from graphblas import Matrix, Vector, binary
from graphblas.select import offdiag
from graphblas.semiring import any_plus

__all__ = ["floyd_warshall"]


def floyd_warshall(G, is_weighted=False):
# By using `offdiag` instead of `G._A`, we ensure that D will not become dense.
# Dense D may be better at times, but not including the diagonal will result in less work.
# Typically, Floyd-Warshall algorithms sets the diagonal of D to 0 at the beginning.
# This is unnecessary with sparse matrices, and we set the diagonal to 0 at the end.
# We also don't iterate over index `i` if either row i or column i are empty.
if G.is_directed():
A, row_degrees, column_degrees = G.get_properties("offdiag row_degrees- column_degrees-")
nonempty_nodes = binary.pair(row_degrees & column_degrees).new(name="nonempty_nodes")
else:
A, nonempty_nodes = G.get_properties("offdiag degrees-")

if A.dtype == bool or not is_weighted:
dtype = int
else:
dtype = A.dtype
n = A.nrows
D = Matrix(dtype, nrows=n, ncols=n, name="floyd_warshall")
if is_weighted:
D << A
else:
D(A.S) << 1 # Like `D << unary.one[int](A)`
del A

Row = Matrix(dtype, nrows=1, ncols=n, name="Row")
Col = Matrix(dtype, nrows=n, ncols=1, name="Col")
Outer = Matrix(dtype, nrows=n, ncols=n, name="Outer")
for i in nonempty_nodes:
Col << D[:, [i]]
Row << D[[i], :]
Outer << any_plus(Col @ Row) # Like `col.outer(row, binary.plus)`
D(binary.min) << offdiag(Outer)

# Set diagonal values to 0 (this way seems fast).
# The missing values are implied to be infinity, so we set diagonals explicitly to 0.
mask = Vector(bool, size=n, name="mask")
mask << True
Mask = mask.diag(name="Mask")
D(Mask.S) << 0
return D
19 changes: 11 additions & 8 deletions graphblas_algorithms/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Dispatcher:
is_k_regular = nxapi.regular.is_k_regular
is_regular = nxapi.regular.is_regular
# Shortest Paths
floyd_warshall = nxapi.shortest_paths.dense.floyd_warshall
has_path = nxapi.shortest_paths.generic.has_path
# Simple Paths
is_simple_path = nxapi.simple_paths.is_simple_path
Expand Down Expand Up @@ -99,14 +100,16 @@ def on_start_tests(items):
import pytest
except ImportError: # pragma: no cover (import)
return
skip = [
("test_attributes", {"TestBoruvka", "test_mst.py"}),
("test_weight_attribute", {"TestBoruvka", "test_mst.py"}),
]
multi_attributed = "unable to handle multi-attributed graphs"
multidigraph = "unable to handle MultiDiGraph"
freeze = frozenset
skip = {
("test_attributes", freeze({"TestBoruvka", "test_mst.py"})): multi_attributed,
("test_weight_attribute", freeze({"TestBoruvka", "test_mst.py"})): multi_attributed,
("test_zero_weight", freeze({"TestFloyd", "test_dense.py"})): multidigraph,
}
for item in items:
kset = set(item.keywords)
for test_name, keywords in skip:
for (test_name, keywords), reason in skip.items():
if item.name == test_name and keywords.issubset(kset):
item.add_marker(
pytest.mark.xfail(reason="unable to handle multi-attributed graphs")
)
item.add_marker(pytest.mark.xfail(reason=reason))
1 change: 1 addition & 0 deletions graphblas_algorithms/nxapi/shortest_paths/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .dense import *
from .generic import *
10 changes: 10 additions & 0 deletions graphblas_algorithms/nxapi/shortest_paths/dense.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from graphblas_algorithms import algorithms
from graphblas_algorithms.classes.digraph import to_graph

__all__ = ["floyd_warshall"]


def floyd_warshall(G, weight="weight"):
G = to_graph(G, weight=weight)
D = algorithms.floyd_warshall(G, is_weighted=weight is not None)
return G.matrix_to_dicts(D)

0 comments on commit 6dd93bd

Please sign in to comment.