Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 1180e1c

Browse files
authored
Add {all_pairs,single_source}_bellman_ford_path_length (#44)
* Add `{all_pairs,single_source}_bellman_ford_path_length` That is, add these to `nxapi`: - `all_pairs_bellman_ford_path_length` - `single_source_bellman_ford_path_length` * Implement `algorithms.bellman_ford_path_lengths` to compute in chunks * Ignore diagonals during Bellman-Ford * Add comment, and use `offdiag` more places. * Do level BFS for Bellman-Ford when iso-valued (and non-negative) * Use `"iso_value"` property more places instead of `A.ss.iso_value` * Fail fast in these unlikely, but easily detected, cases * Allow garbage collector to be enabled during benchmarks * Automatically choose appropriate chunksize * Use `nsplits="auto"` in square_clustering (default to 256 MB chunks)
1 parent 0b649b2 commit 1180e1c

File tree

21 files changed

+533
-81
lines changed

21 files changed

+533
-81
lines changed

‎.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ repos:
5656
# These versions need updated manually
5757
- flake8==6.0.0
5858
- flake8-comprehensions==3.10.1
59-
- flake8-bugbear==23.1.20
59+
- flake8-bugbear==23.2.13
6060
- flake8-simplify==0.19.3
6161
- repo: https://github.com/asottile/yesqa
6262
rev: v1.4.0

‎graphblas_algorithms/algorithms/centrality/katz.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ def katz_centrality(
3535
raise GraphBlasAlgorithmException("beta must have a value for every node")
3636

3737
A = G._A
38-
if A.ss.is_iso:
38+
if (iso_value:=G.get_property("iso_value")) isnotNone:
3939
# Fold iso-value into alpha
40-
alpha *= A.ss.iso_value.get(1.0)
40+
alpha *= iso_value.get(1.0)
4141
semiring = plus_first[float]
4242
else:
4343
semiring = plus_times[float]

‎graphblas_algorithms/algorithms/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def k_truss(G: Graph, k) -> Graph:
2828
S = C
2929

3030
# Remove isolate nodes
31-
indices, _ = C.reduce_rowwise(monoid.any).to_coo()
31+
indices, _ = C.reduce_rowwise(monoid.any).to_coo(values=False)
3232
Ktruss = C[indices, indices].new()
3333

3434
# Convert back to networkx graph with correct node ids

‎graphblas_algorithms/algorithms/dag.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def descendants(G, source):
99
if source not in G._key_to_id:
1010
raise KeyError(f"The node {source} is not in the graph")
1111
index = G._key_to_id[source]
12-
A = G._A
12+
A = G.get_property("offdiag")
1313
q = Vector.from_coo(index, True, size=A.nrows, name="q")
1414
rv = q.dup(name="descendants")
1515
for _ in range(A.nrows):
@@ -25,7 +25,7 @@ def ancestors(G, source):
2525
if source not in G._key_to_id:
2626
raise KeyError(f"The node {source} is not in the graph")
2727
index = G._key_to_id[source]
28-
A = G._A
28+
A = G.get_property("offdiag")
2929
q = Vector.from_coo(index, True, size=A.nrows, name="q")
3030
rv = q.dup(name="descendants")
3131
for _ in range(A.nrows):

‎graphblas_algorithms/algorithms/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ class EmptyGraphError(GraphBlasAlgorithmException):
1212

1313
class PointlessConcept(GraphBlasAlgorithmException):
1414
pass
15+
16+
17+
class Unbounded(GraphBlasAlgorithmException):
18+
pass

‎graphblas_algorithms/algorithms/link_analysis/pagerank_alg.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,10 @@ def pagerank(
4949
row_degrees = G.get_property("plus_rowwise+") # XXX: What about self-edges?
5050
S = (alpha / row_degrees).new(name="S")
5151

52-
if A.ss.is_iso:
52+
if (iso_value:=G.get_property("iso_value")) isnotNone:
5353
# Fold iso-value of A into S
5454
# This lets us use the plus_first semiring, which is faster
55-
iso_value = A.ss.iso_value
56-
if iso_value != 1:
55+
if iso_value.get(1) != 1:
5756
S *= iso_value
5857
semiring = plus_first[float]
5958
else:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .dense import *
22
from .generic import *
3+
from .weighted import *

‎graphblas_algorithms/algorithms/shortest_paths/generic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def has_path(G, source, target):
1010
dst = G._key_to_id[target]
1111
if src == dst:
1212
return True
13-
A = G._A
13+
A = G.get_property("offdiag")
1414
q_src = Vector.from_coo(src, True, size=A.nrows, name="q_src")
1515
seen_src = q_src.dup(name="seen_src")
1616
q_dst = Vector.from_coo(dst, True, size=A.nrows, name="q_dst")
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import numpy as np
2+
from graphblas import Matrix, Vector, binary, monoid, replace, select, unary
3+
from graphblas.semiring import any_pair, min_plus
4+
5+
from ..exceptions import Unbounded
6+
7+
__all__ = [
8+
"single_source_bellman_ford_path_length",
9+
"bellman_ford_path_lengths",
10+
]
11+
12+
13+
def single_source_bellman_ford_path_length(G, source):
14+
# No need for `is_weighted=` keyword, b/c this is assumed to be weighted (I think)
15+
index = G._key_to_id[source]
16+
if G.get_property("is_iso"):
17+
# If the edges are iso-valued (and positive), then we can simply do level BFS
18+
is_negative, iso_value = G.get_properties("has_negative_edges+ iso_value")
19+
if not is_negative:
20+
d = _bfs_level(G, source, dtype=iso_value.dtype)
21+
if iso_value != 1:
22+
d *= iso_value
23+
return d
24+
# It's difficult to detect negative cycles with BFS
25+
if G._A[index, index].get() is not None:
26+
raise Unbounded("Negative cycle detected.")
27+
if not G.is_directed() and G._A[index, :].nvals > 0:
28+
# For undirected graphs, any negative edge is a cycle
29+
raise Unbounded("Negative cycle detected.")
30+
31+
# Use `offdiag` instead of `A`, b/c self-loops don't contribute to the result,
32+
# and negative self-loops are easy negative cycles to avoid.
33+
# We check if we hit a self-loop negative cycle at the end.
34+
A, has_negative_diagonal = G.get_properties("offdiag has_negative_diagonal")
35+
if A.dtype == bool:
36+
# Should we upcast e.g. INT8 to INT64 as well?
37+
dtype = int
38+
else:
39+
dtype = A.dtype
40+
n = A.nrows
41+
d = Vector(dtype, n, name="single_source_bellman_ford_path_length")
42+
d[index] = 0
43+
cur = d.dup(name="cur")
44+
mask = Vector(bool, n, name="mask")
45+
one = unary.one[bool]
46+
for _i in range(n - 1):
47+
# This is a slightly modified Bellman-Ford algorithm.
48+
# `cur` is the current frontier of values that improved in the previous iteration.
49+
# This means that in this iteration we drop values from `cur` that are not better.
50+
cur << min_plus(cur @ A)
51+
52+
# Mask is True where cur not in d or cur < d
53+
mask << one(cur)
54+
mask(binary.second) << binary.lt(cur & d)
55+
56+
# Drop values from `cur` that didn't improve
57+
cur(mask.V, replace) << cur
58+
if cur.nvals == 0:
59+
break
60+
# Update `d` with values that improved
61+
d(cur.S) << cur
62+
else:
63+
# Check for negative cycle when for loop completes without breaking
64+
cur << min_plus(cur @ A)
65+
mask << binary.lt(cur & d)
66+
if mask.reduce(monoid.lor):
67+
raise Unbounded("Negative cycle detected.")
68+
if has_negative_diagonal:
69+
# We removed diagonal entries above, so check if we visited one with a negative weight
70+
diag = G.get_property("diag")
71+
cur << select.valuelt(diag, 0)
72+
if any_pair(d @ cur):
73+
raise Unbounded("Negative cycle detected.")
74+
return d
75+
76+
77+
def bellman_ford_path_lengths(G, nodes=None, *, expand_output=False):
78+
"""
79+
80+
Parameters
81+
----------
82+
expand_output : bool, default False
83+
When False, the returned Matrix has one row per node in nodes.
84+
When True, the returned Matrix has the same shape as the input Matrix.
85+
"""
86+
# Same algorithms as in `single_source_bellman_ford_path_length`, but with
87+
# `Cur` as a Matrix with each row corresponding to a source node.
88+
if G.get_property("is_iso"):
89+
is_negative, iso_value = G.get_properties("has_negative_edges+ iso_value")
90+
if not is_negative:
91+
D = _bfs_levels(G, nodes, dtype=iso_value.dtype)
92+
if iso_value != 1:
93+
D *= iso_value
94+
if nodes is not None and expand_output and D.ncols != D.nrows:
95+
ids = G.list_to_ids(nodes)
96+
rv = Matrix(D.dtype, D.ncols, D.ncols, name=D.name)
97+
rv[ids, :] = D
98+
return rv
99+
return D
100+
if not G.is_directed():
101+
# For undirected graphs, any negative edge is a cycle
102+
if nodes is not None:
103+
ids = G.list_to_ids(nodes)
104+
if G._A[ids, :].nvals > 0:
105+
raise Unbounded("Negative cycle detected.")
106+
elif G._A.nvals > 0:
107+
raise Unbounded("Negative cycle detected.")
108+
109+
A, has_negative_diagonal = G.get_properties("offdiag has_negative_diagonal")
110+
if A.dtype == bool:
111+
dtype = int
112+
else:
113+
dtype = A.dtype
114+
n = A.nrows
115+
if nodes is None:
116+
# TODO: `D = Vector.from_scalar(0, n, dtype).diag()`
117+
D = Vector(dtype, n, name="bellman_ford_path_lengths_vector")
118+
D << 0
119+
D = D.diag(name="bellman_ford_path_lengths")
120+
else:
121+
ids = G.list_to_ids(nodes)
122+
D = Matrix.from_coo(
123+
np.arange(len(ids), dtype=np.uint64),
124+
ids,
125+
0,
126+
dtype,
127+
nrows=len(ids),
128+
ncols=n,
129+
name="bellman_ford_path_lengths",
130+
)
131+
Cur = D.dup(name="Cur")
132+
Mask = Matrix(bool, D.nrows, D.ncols, name="Mask")
133+
one = unary.one[bool]
134+
for _i in range(n - 1):
135+
Cur << min_plus(Cur @ A)
136+
Mask << one(Cur)
137+
Mask(binary.second) << binary.lt(Cur & D)
138+
Cur(Mask.V, replace) << Cur
139+
if Cur.nvals == 0:
140+
break
141+
D(Cur.S) << Cur
142+
else:
143+
Cur << min_plus(Cur @ A)
144+
Mask << binary.lt(Cur & D)
145+
if Mask.reduce_scalar(monoid.lor):
146+
raise Unbounded("Negative cycle detected.")
147+
if has_negative_diagonal:
148+
diag = G.get_property("diag")
149+
cur = select.valuelt(diag, 0)
150+
if any_pair(D @ cur).nvals > 0:
151+
raise Unbounded("Negative cycle detected.")
152+
if nodes is not None and expand_output and D.ncols != D.nrows:
153+
rv = Matrix(D.dtype, n, n, name=D.name)
154+
rv[ids, :] = D
155+
return rv
156+
return D
157+
158+
159+
def _bfs_level(G, source, *, dtype=int):
160+
if dtype == bool:
161+
dtype = int
162+
index = G._key_to_id[source]
163+
A = G.get_property("offdiag")
164+
n = A.nrows
165+
v = Vector(dtype, n, name="bfs_level")
166+
q = Vector(bool, n, name="q")
167+
v[index] = 0
168+
q[index] = True
169+
any_pair_bool = any_pair[bool]
170+
for i in range(1, n):
171+
q(~v.S, replace) << any_pair_bool(q @ A)
172+
if q.nvals == 0:
173+
break
174+
v(q.S) << i
175+
return v
176+
177+
178+
def _bfs_levels(G, nodes=None, *, dtype=int):
179+
if dtype == bool:
180+
dtype = int
181+
A = G.get_property("offdiag")
182+
n = A.nrows
183+
if nodes is None:
184+
# TODO: `D = Vector.from_scalar(0, n, dtype).diag()`
185+
D = Vector(dtype, n, name="bfs_levels_vector")
186+
D << 0
187+
D = D.diag(name="bfs_levels")
188+
else:
189+
ids = G.list_to_ids(nodes)
190+
D = Matrix.from_coo(
191+
np.arange(len(ids), dtype=np.uint64),
192+
ids,
193+
0,
194+
dtype,
195+
nrows=len(ids),
196+
ncols=n,
197+
name="bfs_levels",
198+
)
199+
Q = Matrix(bool, D.nrows, D.ncols, name="Q")
200+
Q << unary.one[bool](D)
201+
any_pair_bool = any_pair[bool]
202+
for i in range(1, n):
203+
Q(~D.S, replace) << any_pair_bool(Q @ A)
204+
if Q.nvals == 0:
205+
break
206+
D(Q.S) << i
207+
return D

‎graphblas_algorithms/algorithms/simple_paths.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def is_simple_path(G, nodes):
88
return False
99
if len(nodes) == 1:
1010
return nodes[0] in G
11-
A = G._A
11+
A = G._A# offdiag instead?
1212
if A.nvals < len(nodes) - 1:
1313
return False
1414
key_to_id = G._key_to_id

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /