diff --git a/README.md b/README.md index 3e21cd7..92c06c3 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ dispatch pattern shown above. - is_k_regular - is_regular - Shortest Paths + - floyd_warshall - has_path - Simple Paths - is_simple_path diff --git a/graphblas_algorithms/algorithms/shortest_paths/__init__.py b/graphblas_algorithms/algorithms/shortest_paths/__init__.py index c9840bc..60613e6 100644 --- a/graphblas_algorithms/algorithms/shortest_paths/__init__.py +++ b/graphblas_algorithms/algorithms/shortest_paths/__init__.py @@ -1 +1,2 @@ +from .dense import * from .generic import * diff --git a/graphblas_algorithms/algorithms/shortest_paths/dense.py b/graphblas_algorithms/algorithms/shortest_paths/dense.py new file mode 100644 index 0000000..f53814f --- /dev/null +++ b/graphblas_algorithms/algorithms/shortest_paths/dense.py @@ -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 diff --git a/graphblas_algorithms/interface.py b/graphblas_algorithms/interface.py index eaee712..ddc1091 100644 --- a/graphblas_algorithms/interface.py +++ b/graphblas_algorithms/interface.py @@ -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 @@ -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)) diff --git a/graphblas_algorithms/nxapi/shortest_paths/__init__.py b/graphblas_algorithms/nxapi/shortest_paths/__init__.py index c9840bc..60613e6 100644 --- a/graphblas_algorithms/nxapi/shortest_paths/__init__.py +++ b/graphblas_algorithms/nxapi/shortest_paths/__init__.py @@ -1 +1,2 @@ +from .dense import * from .generic import * diff --git a/graphblas_algorithms/nxapi/shortest_paths/dense.py b/graphblas_algorithms/nxapi/shortest_paths/dense.py new file mode 100644 index 0000000..21a32be --- /dev/null +++ b/graphblas_algorithms/nxapi/shortest_paths/dense.py @@ -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)