Skip to content

Commit ef16f82

Browse files
authored
Enhance caching to allow things like plus_rowwise- and min_element+ (#9)
* Enhance caching to allow things like `plus_rowwise` and `min_element` * Oops. Fix reduce to scalar in caching.
1 parent 90bfa89 commit ef16f82

File tree

6 files changed

+529
-309
lines changed

6 files changed

+529
-309
lines changed

graphblas_algorithms/algorithms/cluster.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ def transitivity_directed_core(G):
6666
numerator = plus_pair(A @ A.T).new(mask=A.S).reduce_scalar(allow_empty=False).value
6767
if numerator == 0:
6868
return 0
69-
deg = A.reduce_rowwise("count")
70-
denom = (deg * (deg - 1)).reduce().value
69+
degrees = G.get_property("row_degrees-")
70+
denom = (degrees * (degrees - 1)).reduce().value
7171
return numerator / denom
7272

7373

@@ -76,9 +76,10 @@ def transitivity(G):
7676
if len(G) == 0:
7777
return 0
7878
if G.is_directed():
79-
return transitivity_directed_core(G)
79+
func = transitivity_directed_core
8080
else:
81-
return transitivity_core(G)
81+
func = transitivity_core
82+
return G._cacheit("transitivity", func, G)
8283

8384

8485
def clustering_core(G, mask=None):
@@ -101,10 +102,7 @@ def clustering_directed_core(G, mask=None):
101102
+ plus_pair(AT @ A.T).new(mask=A.S).reduce_rowwise().new(mask=mask)
102103
+ plus_pair(AT @ AT.T).new(mask=A.S).reduce_columnwise().new(mask=mask)
103104
)
104-
recip_degrees = binary.pair(A & AT).reduce_rowwise().new(mask=mask)
105-
total_degrees = A.reduce_rowwise("count").new(mask=mask) + A.reduce_columnwise("count").new(
106-
mask=mask
107-
)
105+
recip_degrees, total_degrees = G.get_properties("recip_degrees- total_degrees-", mask=mask)
108106
return (tri / (total_degrees * (total_degrees - 1) - 2 * recip_degrees)).new(name="clustering")
109107

110108

@@ -200,6 +198,12 @@ def average_clustering(G, nodes=None, weight=None, count_zeros=True):
200198
raise ZeroDivisionError() # Not covered
201199
mask = G.list_to_mask(nodes)
202200
if G.is_directed():
203-
return average_clustering_directed_core(G, mask=mask, count_zeros=count_zeros)
201+
func = average_clustering_directed_core
202+
else:
203+
func = average_clustering_core
204+
if mask is None:
205+
return G._cacheit(
206+
f"average_clustering(count_zeros={count_zeros})", func, G, count_zeros=count_zeros
207+
)
204208
else:
205-
return average_clustering_core(G, mask=mask, count_zeros=count_zeros)
209+
return func(G, mask=mask, count_zeros=count_zeros)

graphblas_algorithms/algorithms/link_analysis/pagerank_alg.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,8 @@ def pagerank_core(
4545
# Inverse of row_degrees
4646
# Fold alpha constant into S
4747
if row_degrees is None:
48-
S = A.reduce_rowwise().new(float, name="S") # XXX: What about self-edges
49-
S << alpha / S
50-
else:
51-
S = (alpha / row_degrees).new(name="S")
48+
row_degrees = G.get_property("plus_rowwise+") # XXX: What about self-edges?
49+
S = (alpha / row_degrees).new(name="S")
5250

5351
if A.ss.is_iso:
5452
# Fold iso-value of A into S
@@ -124,8 +122,7 @@ def pagerank(
124122
# We'll normalize initial, personalization, and dangling vectors later
125123
x = G.dict_to_vector(nstart, dtype=float, name="nstart")
126124
p = G.dict_to_vector(personalization, dtype=float, name="personalization")
127-
row_degrees = G._A.reduce_rowwise().new(name="row_degrees") # XXX: What about self-edges?
128-
# row_degrees = G.get_property('plus_rowwise+') # Maybe?
125+
row_degrees = G.get_property("plus_rowwise+") # XXX: What about self-edges?
129126
if dangling is not None and row_degrees.nvals < N:
130127
dangling_weights = G.dict_to_vector(dangling, dtype=float, name="dangling")
131128
else:
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from graphblas import agg, op, operator
2+
3+
4+
def get_reduce_to_vector(key, opname, methodname):
5+
try:
6+
op_ = op.from_string(opname)
7+
except ValueError:
8+
op_ = agg.from_string(opname)
9+
op_, opclass = operator.find_opclass(op_)
10+
keybase = key[:-1]
11+
if key[-1] == "-":
12+
13+
def get_reduction(G, mask=None):
14+
cache = G._cache
15+
if mask is not None:
16+
if key in cache:
17+
return cache[key].dup(mask=mask)
18+
elif cache.get("has_self_edges") is False and f"{keybase}+" in cache:
19+
cache[key] = cache[f"{keybase}+"]
20+
return cache[key].dup(mask=mask)
21+
elif "offdiag" in cache:
22+
return getattr(cache["offdiag"], methodname)(op_).new(mask=mask, name=key)
23+
elif (
24+
"L-" in cache
25+
and "U-" in cache
26+
and opclass in {"BinaryOp", "Monoid"}
27+
and G.get_property("has_self_edges")
28+
):
29+
return op_(
30+
getattr(cache["L-"], methodname)(op_).new(mask=mask)
31+
| getattr(cache["U-"], methodname)(op_).new(mask=mask)
32+
).new(name=key)
33+
elif not G.get_property("has_self_edges"):
34+
return G.get_property(f"{keybase}+", mask=mask)
35+
else:
36+
return getattr(G.get_property("offdiag"), methodname)(op_).new(
37+
mask=mask, name=key
38+
)
39+
if key not in cache:
40+
if cache.get("has_self_edges") is False and f"{keybase}+" in cache:
41+
cache[key] = cache[f"{keybase}+"]
42+
elif "offdiag" in cache:
43+
cache[key] = getattr(cache["offdiag"], methodname)(op_).new(name=key)
44+
elif (
45+
"L-" in cache
46+
and "U-" in cache
47+
and opclass in {"BinaryOp", "Monoid"}
48+
and G.get_property("has_self_edges")
49+
):
50+
cache[key] = op_(
51+
getattr(cache["L-"], methodname)(op_)
52+
| getattr(cache["U-"], methodname)(op_)
53+
).new(name=key)
54+
elif not G.get_property("has_self_edges"):
55+
cache[key] = G.get_property(f"{keybase}+")
56+
else:
57+
cache[key] = getattr(G.get_property("offdiag"), methodname)(op_).new(name=key)
58+
if (
59+
"has_self_edges" not in cache
60+
and f"{keybase}+" in cache
61+
and cache[key].nvals != cache[f"{keybase}+"].nvals
62+
):
63+
cache["has_self_edges"] = True
64+
elif cache.get("has_self_edges") is False:
65+
cache[f"{keybase}+"] = cache[key]
66+
return cache[key]
67+
68+
else:
69+
70+
def get_reduction(G, mask=None):
71+
A = G._A
72+
cache = G._cache
73+
if mask is not None:
74+
if key in cache:
75+
return cache[key].dup(mask=mask)
76+
elif cache.get("has_self_edges") is False and f"{keybase}-" in cache:
77+
cache[key] = cache[f"{keybase}-"]
78+
return cache[key].dup(mask=mask)
79+
elif methodname == "reduce_columnwise" and "AT" in cache:
80+
return cache["AT"].reduce_rowwise(op_).new(mask=mask, name=key)
81+
else:
82+
return getattr(A, methodname)(op_).new(mask=mask, name=key)
83+
if key not in cache:
84+
if cache.get("has_self_edges") is False and f"{keybase}-" in cache:
85+
cache[key] = cache[f"{keybase}-"]
86+
elif methodname == "reduce_columnwise" and "AT" in cache:
87+
cache[key] = cache["AT"].reduce_rowwise(op_).new(name=key)
88+
else:
89+
cache[key] = getattr(A, methodname)(op_).new(name=key)
90+
if (
91+
"has_self_edges" not in cache
92+
and f"{keybase}-" in cache
93+
and cache[key].nvals != cache[f"{keybase}-"].nvals
94+
):
95+
cache["has_self_edges"] = True
96+
elif cache.get("has_self_edges") is False:
97+
cache[f"{keybase}-"] = cache[key]
98+
return cache[key]
99+
100+
return get_reduction
101+
102+
103+
def get_reduce_to_scalar(key, opname):
104+
try:
105+
op_ = op.from_string(opname)
106+
except ValueError:
107+
op_ = agg.from_string(opname)
108+
op_, opclass = operator.find_opclass(op_)
109+
keybase = key[:-1]
110+
if key[-1] == "-":
111+
112+
def get_reduction(G, mask=None):
113+
cache = G._cache
114+
if key not in cache:
115+
if cache.get("has_self_edges") is False and f"{keybase}+" in cache:
116+
cache[key] = cache[f"{keybase}+"]
117+
elif f"{opname}_rowwise-" in cache:
118+
cache[key] = cache[f"{opname}_rowwise-"].reduce(op_).new(name=key)
119+
elif f"{opname}_columnwise-" in cache:
120+
cache[key] = cache[f"{opname}_columnwise-"].reduce(op_).new(name=key)
121+
elif cache.get("has_self_edges") is False and f"{opname}_rowwise+" in cache:
122+
cache[key] = cache[f"{opname}_rowwise+"].reduce(op_).new(name=key)
123+
elif cache.get("has_self_edges") is False and f"{opname}_columnwise+" in cache:
124+
cache[key] = cache[f"{opname}_columnwise+"].reduce(op_).new(name=key)
125+
elif "offdiag" in cache:
126+
cache[key] = cache["offdiag"].reduce_scalar(op_).new(name=key)
127+
elif (
128+
"L-" in cache
129+
and "U-" in cache
130+
and opclass in {"BinaryOp", "Monoid"}
131+
and G.get_property("has_self_edges")
132+
):
133+
return op_(
134+
cache["L-"].reduce(op_)._as_vector() | cache["U-"].reduce(op_)._as_vector()
135+
)[0].new(name=key)
136+
elif not G.get_property("has_self_edges"):
137+
cache[key] = G.get_property(f"{keybase}+")
138+
else:
139+
cache[key] = G.get_property("offdiag").reduce_scalar(op_).new(name=key)
140+
if (
141+
"has_self_edges" not in cache
142+
and f"{keybase}+" in cache
143+
and cache[key] != cache[f"{keybase}+"]
144+
):
145+
cache["has_self_edges"] = True
146+
elif cache.get("has_self_edges") is False:
147+
cache[f"{keybase}+"] = cache[key]
148+
return cache[key]
149+
150+
else:
151+
152+
def get_reduction(G, mask=None):
153+
A = G._A
154+
cache = G._cache
155+
if key not in cache:
156+
if cache.get("has_self_edges") is False and f"{keybase}-" in cache:
157+
cache[key] = cache[f"{keybase}-"]
158+
elif f"{opname}_rowwise+" in cache:
159+
cache[key] = cache[f"{opname}_rowwise+"].reduce(op_).new(name=key)
160+
elif f"{opname}_columnwise+" in cache:
161+
cache[key] = cache[f"{opname}_columnwise+"].reduce(op_).new(name=key)
162+
elif cache.get("has_self_edges") is False and f"{opname}_rowwise-" in cache:
163+
cache[key] = cache[f"{opname}_rowwise-"].reduce(op_).new(name=key)
164+
elif cache.get("has_self_edges") is False and f"{opname}_columnwise-" in cache:
165+
cache[key] = cache[f"{opname}_columnwise-"].reduce(op_).new(name=key)
166+
else:
167+
cache[key] = A.reduce_scalar(op_).new(name=key)
168+
if (
169+
"has_self_edges" not in cache
170+
and f"{keybase}-" in cache
171+
and cache[key] != cache[f"{keybase}-"]
172+
):
173+
cache["has_self_edges"] = True
174+
elif cache.get("has_self_edges") is False:
175+
cache[f"{keybase}-"] = cache[key]
176+
return cache[key]
177+
178+
return get_reduction

graphblas_algorithms/classes/_utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,21 @@ def from_graphblas(cls, A):
2323

2424

2525
def get_property(self, name, *, mask=None):
26-
return self._get_property[name](self._A, self._cache, mask)
26+
return self._get_property[self._cache_aliases.get(name, name)](self, mask)
2727

2828

2929
def get_properties(self, names, *, mask=None):
3030
if isinstance(names, str):
3131
# Separated by commas and/or spaces
32-
names = [name for name in names.replace(" ", ",").split(",") if name]
32+
names = [
33+
self._cache_aliases.get(name, name)
34+
for name in names.replace(" ", ",").split(",")
35+
if name
36+
]
37+
else:
38+
names = [self._cache_aliases.get(name, name) for name in names]
3339
results = {
34-
name: self._get_property[name](self._A, self._cache, mask)
40+
name: self._get_property[name](self, mask)
3541
for name in sorted(names, key=self._property_priority.__getitem__)
3642
}
3743
return [results[name] for name in names]
@@ -70,3 +76,9 @@ def vector_to_dict(self, v, *, mask=None, fillvalue=None):
7076
elif fillvalue is not None and v.nvals < v.size:
7177
v(mask=~v.S) << fillvalue
7278
return {self._id_to_key[index]: value for index, value in zip(*v.to_values(sort=False))}
79+
80+
81+
def _cacheit(self, key, func, *args, **kwargs):
82+
if key not in self._cache:
83+
self._cache[key] = func(*args, **kwargs)
84+
return self._cache[key]

0 commit comments

Comments
 (0)