11import graphblas as gb
22import networkx as nx
3- from graphblas import Matrix , agg , select
4- from graphblas .semiring import any_pair , plus_pair
3+ from graphblas import binary , select
4+ from graphblas .semiring import plus_pair
55from networkx import average_clustering as _nx_average_clustering
66from networkx import clustering as _nx_clustering
77from networkx .utils import not_implemented_for
@@ -51,27 +51,24 @@ def get_degrees(G, mask=None, *, L=None, U=None, has_self_edges=True):
5151 if L is None or U is None :
5252 L , U = get_properties (G , "L U" , L = L , U = U )
5353 degrees = (
54- L .reduce_rowwise (agg .count ).new (mask = mask ) + U .reduce_rowwise (agg .count ).new (mask = mask )
54+ L .reduce_rowwise (gb .agg .count ).new (mask = mask )
55+ + U .reduce_rowwise (gb .agg .count ).new (mask = mask )
5556 ).new (name = "degrees" )
5657 else :
57- degrees = G .reduce_rowwise (agg .count ).new (mask = mask , name = "degrees" )
58+ degrees = G .reduce_rowwise (gb . agg .count ).new (mask = mask , name = "degrees" )
5859 return degrees
5960
6061
6162def single_triangle_core (G , index , * , L = None , has_self_edges = True ):
62- M = Matrix (bool , G .nrows , G .ncols )
63- M [index , index ] = True
64- C = any_pair (G .T @ M .T ).new (name = "C" ) # select.coleq(G.T, index)
63+ r = G [index , :].new ()
6564 has_self_edges = get_properties (G , "has_self_edges" , L = L , has_self_edges = has_self_edges )
66- if has_self_edges :
67- del C [index , index ] # Ignore self-edges
68- R = C .T .new (name = "R" )
6965 if has_self_edges :
7066 # Pretty much all the time is spent here taking TRIL, which is used to ignore self-edges
7167 L = get_properties (G , "L" , L = L )
72- return plus_pair (L @ R .T ).new (mask = C .S ).reduce_scalar (allow_empty = False ).value
68+ del r [index ] # Ignore self-edges
69+ return plus_pair (L @ r ).new (mask = r .S ).reduce (allow_empty = False ).value
7370 else :
74- return plus_pair (G @ R . T ).new (mask = C .S ).reduce_scalar (allow_empty = False ).value // 2
71+ return plus_pair (G @ r ).new (mask = r .S ).reduce (allow_empty = False ).value // 2
7572
7673
7774def triangles_core (G , mask = None , * , L = None , U = None ):
@@ -114,12 +111,28 @@ def transitivity_core(G, *, L=None, U=None, degrees=None):
114111 return 6 * numerator / denom
115112
116113
117- @not_implemented_for ("directed" ) # Should we implement it for directed?
114+ def transitivity_directed_core (G , * , has_self_edges = True ):
115+ # XXX" is transitivity supposed to work on directed graphs like this?
116+ if has_self_edges :
117+ A = select .offdiag (G )
118+ else :
119+ A = G
120+ numerator = plus_pair (A @ A .T ).new (mask = A .S ).reduce_scalar (allow_empty = False ).value
121+ if numerator == 0 :
122+ return 0
123+ deg = A .reduce_rowwise (gb .agg .count )
124+ denom = (deg * (deg - 1 )).reduce ().value
125+ return numerator / denom
126+
127+
118128def transitivity (G ):
119129 if len (G ) == 0 :
120130 return 0
121131 A = gb .io .from_networkx (G , weight = None , dtype = bool )
122- return transitivity_core (A )
132+ if isinstance (G , nx .DiGraph ):
133+ return transitivity_directed_core (A )
134+ else :
135+ return transitivity_core (A )
123136
124137
125138def clustering_core (G , mask = None , * , L = None , U = None , degrees = None ):
@@ -130,6 +143,29 @@ def clustering_core(G, mask=None, *, L=None, U=None, degrees=None):
130143 return (2 * tri / denom ).new (name = "clustering" )
131144
132145
146+ def clustering_directed_core (G , mask = None , * , has_self_edges = True ):
147+ # TODO: Alright, this introduces us to properties of directed graphs:
148+ # has_self_edges, offdiag, row_degrees, column_degrees, total_degrees, recip_degrees
149+ # (in_degrees, out_degrees?)
150+ if has_self_edges :
151+ A = select .offdiag (G )
152+ else :
153+ A = G
154+ AT = A .T .new ()
155+ temp = plus_pair (A @ A .T ).new (mask = A .S )
156+ tri = (
157+ temp .reduce_rowwise ().new (mask = mask )
158+ + temp .reduce_columnwise ().new (mask = mask )
159+ + plus_pair (AT @ A .T ).new (mask = A .S ).reduce_rowwise ().new (mask = mask )
160+ + plus_pair (AT @ AT .T ).new (mask = A .S ).reduce_columnwise ().new (mask = mask )
161+ )
162+ recip_degrees = binary .pair (A & AT ).reduce_rowwise ().new (mask = mask )
163+ total_degrees = (
164+ A .reduce_rowwise (gb .agg .count ).new (mask = mask ) + A .reduce_columnwise (gb .agg .count )
165+ ).new (mask = mask )
166+ return (tri / (total_degrees * (total_degrees - 1 ) - 2 * recip_degrees )).new (name = "clustering" )
167+
168+
133169def single_clustering_core (G , index , * , L = None , degrees = None , has_self_edges = True ):
134170 has_self_edges = get_properties (G , "has_self_edges" , L = L , has_self_edges = has_self_edges )
135171 tri = single_triangle_core (G , index , L = L , has_self_edges = has_self_edges )
@@ -139,24 +175,50 @@ def single_clustering_core(G, index, *, L=None, degrees=None, has_self_edges=Tru
139175 degrees = degrees [index ].value
140176 else :
141177 row = G [index , :].new ()
142- degrees = row .reduce ( agg . count ). value
178+ degrees = row .nvals
143179 if has_self_edges and row [index ].value is not None :
144180 degrees -= 1
145181 denom = degrees * (degrees - 1 )
146182 return 2 * tri / denom
147183
148184
185+ def single_clustering_directed_core (G , index , * , has_self_edges = True ):
186+ if has_self_edges :
187+ A = select .offdiag (G )
188+ else :
189+ A = G
190+ r = A [index , :].new ()
191+ c = A [:, index ].new ()
192+ tri = (
193+ plus_pair (A @ c ).new (mask = c .S ).reduce (allow_empty = False ).value
194+ + plus_pair (A @ c ).new (mask = r .S ).reduce (allow_empty = False ).value
195+ + plus_pair (A @ r ).new (mask = c .S ).reduce (allow_empty = False ).value
196+ + plus_pair (A @ r ).new (mask = r .S ).reduce (allow_empty = False ).value
197+ )
198+ if tri == 0 :
199+ return 0
200+ total_degrees = c .nvals + r .nvals
201+ recip_degrees = binary .pair (c & r ).nvals
202+ return tri / (total_degrees * (total_degrees - 1 ) - 2 * recip_degrees )
203+
204+
149205def clustering (G , nodes = None , weight = None ):
150206 if len (G ) == 0 :
151207 return {}
152- if isinstance ( G , nx . DiGraph ) or weight is not None :
153- # TODO: Not yet implemented. Clustering implemented only for undirected and unweighted.
208+ if weight is not None :
209+ # TODO: Not yet implemented. Clustering implemented only for unweighted.
154210 return _nx_clustering (G , nodes = nodes , weight = weight )
155211 A , key_to_id = graph_to_adjacency (G , weight = weight )
156212 if nodes in G :
157- return single_clustering_core (A , key_to_id [nodes ])
213+ if isinstance (G , nx .DiGraph ):
214+ return single_clustering_directed_core (A , key_to_id [nodes ])
215+ else :
216+ return single_clustering_core (A , key_to_id [nodes ])
158217 mask , id_to_key = list_to_mask (nodes , key_to_id )
159- result = clustering_core (A , mask = mask )
218+ if isinstance (G , nx .DiGraph ):
219+ result = clustering_directed_core (A , mask = mask )
220+ else :
221+ result = clustering_core (A , mask = mask )
160222 return vector_to_dict (result , key_to_id , id_to_key , mask = mask , fillvalue = 0.0 )
161223
162224
@@ -171,10 +233,26 @@ def average_clustering_core(G, mask=None, count_zeros=True, *, L=None, U=None, d
171233 return val / c .size
172234
173235
236+ def average_clustering_directed_core (G , mask = None , count_zeros = True , * , has_self_edges = True ):
237+ c = clustering_directed_core (G , mask = mask , has_self_edges = has_self_edges )
238+ val = c .reduce (allow_empty = False ).value
239+ if not count_zeros :
240+ return val / c .nvals
241+ elif mask is not None :
242+ return val / mask .parent .nvals
243+ else :
244+ return val / c .size
245+
246+
174247def average_clustering (G , nodes = None , weight = None , count_zeros = True ):
175- if len (G ) == 0 or isinstance (G , nx .DiGraph ) or weight is not None :
176- # TODO: Not yet implemented. Clustering implemented only for undirected and unweighted.
248+ if len (G ) == 0 :
249+ raise ZeroDivisionError () # Not covered
250+ if weight is not None :
251+ # TODO: Not yet implemented. Clustering implemented only for unweighted.
177252 return _nx_average_clustering (G , nodes = nodes , weight = weight , count_zeros = count_zeros )
178253 A , key_to_id = graph_to_adjacency (G , weight = weight )
179254 mask , _ = list_to_mask (nodes , key_to_id )
180- return average_clustering_core (A , mask = mask , count_zeros = count_zeros )
255+ if isinstance (G , nx .DiGraph ):
256+ return average_clustering_directed_core (A , mask = mask , count_zeros = count_zeros )
257+ else :
258+ return average_clustering_core (A , mask = mask , count_zeros = count_zeros )
0 commit comments