Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add floyd_warshall #42

Merged
merged 4 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion graphblas_algorithms/algorithms/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def k_truss(G: Graph, k) -> Graph:
S = C

# Remove isolate nodes
indices, _ = C.reduce_rowwise(monoid.any).to_coo()
indices = C.reduce_rowwise(monoid.any).to_coo(values=False)[0]
Ktruss = C[indices, indices].new()

# Convert back to networkx graph with correct node ids
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-")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we use some shorthand notation here. "degrees-" does not include self-edges (i.e., diagonals), but "degrees+" does include self-edges.


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="temp")
for i in nonempty_nodes:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Col << D[:, [i]]
Row << D[[i], :]
Outer << any_plus(Col @ Row)
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
1 change: 1 addition & 0 deletions graphblas_algorithms/classes/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def matrix_to_dicts(self, A, *, use_row_index=False, use_column_index=False):
and likewise for `use_column_index=True``.

"""
# TODO: use `A.to_dicts()`?
if isinstance(A, TransposedMatrix):
# Not covered
d = A.T.ss.export("hypercsc")
Expand Down
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))
2 changes: 1 addition & 1 deletion graphblas_algorithms/nxapi/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _split(L, k):
# TODO: should this move into algorithms?
def _square_clustering_split(G, node_ids=None, *, nsplits):
if node_ids is None:
node_ids = G._A.reduce_rowwise(monoid.any).to_coo()[0]
node_ids = G._A.reduce_rowwise(monoid.any).to_coo(values=False)[0]
result = None
for chunk_ids in _split(node_ids, nsplits):
res = algorithms.square_clustering(G, chunk_ids)
Expand Down
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)