Skip to content

Commit 6dd93bd

Browse files
authored
Add floyd_warshall (#42)
* 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")
1 parent 140bea8 commit 6dd93bd

File tree

6 files changed

+71
-8
lines changed

6 files changed

+71
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ dispatch pattern shown above.
147147
- is_k_regular
148148
- is_regular
149149
- Shortest Paths
150+
- floyd_warshall
150151
- has_path
151152
- Simple Paths
152153
- is_simple_path
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .dense import *
12
from .generic import *
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from graphblas import Matrix, Vector, binary
2+
from graphblas.select import offdiag
3+
from graphblas.semiring import any_plus
4+
5+
__all__ = ["floyd_warshall"]
6+
7+
8+
def floyd_warshall(G, is_weighted=False):
9+
# By using `offdiag` instead of `G._A`, we ensure that D will not become dense.
10+
# Dense D may be better at times, but not including the diagonal will result in less work.
11+
# Typically, Floyd-Warshall algorithms sets the diagonal of D to 0 at the beginning.
12+
# This is unnecessary with sparse matrices, and we set the diagonal to 0 at the end.
13+
# We also don't iterate over index `i` if either row i or column i are empty.
14+
if G.is_directed():
15+
A, row_degrees, column_degrees = G.get_properties("offdiag row_degrees- column_degrees-")
16+
nonempty_nodes = binary.pair(row_degrees & column_degrees).new(name="nonempty_nodes")
17+
else:
18+
A, nonempty_nodes = G.get_properties("offdiag degrees-")
19+
20+
if A.dtype == bool or not is_weighted:
21+
dtype = int
22+
else:
23+
dtype = A.dtype
24+
n = A.nrows
25+
D = Matrix(dtype, nrows=n, ncols=n, name="floyd_warshall")
26+
if is_weighted:
27+
D << A
28+
else:
29+
D(A.S) << 1 # Like `D << unary.one[int](A)`
30+
del A
31+
32+
Row = Matrix(dtype, nrows=1, ncols=n, name="Row")
33+
Col = Matrix(dtype, nrows=n, ncols=1, name="Col")
34+
Outer = Matrix(dtype, nrows=n, ncols=n, name="Outer")
35+
for i in nonempty_nodes:
36+
Col << D[:, [i]]
37+
Row << D[[i], :]
38+
Outer << any_plus(Col @ Row) # Like `col.outer(row, binary.plus)`
39+
D(binary.min) << offdiag(Outer)
40+
41+
# Set diagonal values to 0 (this way seems fast).
42+
# The missing values are implied to be infinity, so we set diagonals explicitly to 0.
43+
mask = Vector(bool, size=n, name="mask")
44+
mask << True
45+
Mask = mask.diag(name="Mask")
46+
D(Mask.S) << 0
47+
return D

graphblas_algorithms/interface.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class Dispatcher:
5555
is_k_regular = nxapi.regular.is_k_regular
5656
is_regular = nxapi.regular.is_regular
5757
# Shortest Paths
58+
floyd_warshall = nxapi.shortest_paths.dense.floyd_warshall
5859
has_path = nxapi.shortest_paths.generic.has_path
5960
# Simple Paths
6061
is_simple_path = nxapi.simple_paths.is_simple_path
@@ -99,14 +100,16 @@ def on_start_tests(items):
99100
import pytest
100101
except ImportError: # pragma: no cover (import)
101102
return
102-
skip = [
103-
("test_attributes", {"TestBoruvka", "test_mst.py"}),
104-
("test_weight_attribute", {"TestBoruvka", "test_mst.py"}),
105-
]
103+
multi_attributed = "unable to handle multi-attributed graphs"
104+
multidigraph = "unable to handle MultiDiGraph"
105+
freeze = frozenset
106+
skip = {
107+
("test_attributes", freeze({"TestBoruvka", "test_mst.py"})): multi_attributed,
108+
("test_weight_attribute", freeze({"TestBoruvka", "test_mst.py"})): multi_attributed,
109+
("test_zero_weight", freeze({"TestFloyd", "test_dense.py"})): multidigraph,
110+
}
106111
for item in items:
107112
kset = set(item.keywords)
108-
for test_name, keywords in skip:
113+
for (test_name, keywords), reason in skip.items():
109114
if item.name == test_name and keywords.issubset(kset):
110-
item.add_marker(
111-
pytest.mark.xfail(reason="unable to handle multi-attributed graphs")
112-
)
115+
item.add_marker(pytest.mark.xfail(reason=reason))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .dense import *
12
from .generic import *
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from graphblas_algorithms import algorithms
2+
from graphblas_algorithms.classes.digraph import to_graph
3+
4+
__all__ = ["floyd_warshall"]
5+
6+
7+
def floyd_warshall(G, weight="weight"):
8+
G = to_graph(G, weight=weight)
9+
D = algorithms.floyd_warshall(G, is_weighted=weight is not None)
10+
return G.matrix_to_dicts(D)

0 commit comments

Comments
 (0)