1- import graphblas as gb
2- import networkx as nx
3- from graphblas import binary , select
1+ from graphblas import binary
42from graphblas .semiring import plus_pair
53from networkx import average_clustering as _nx_average_clustering
64from networkx import clustering as _nx_clustering
7- from networkx .utils import not_implemented_for
85
9- from graphblas_algorithms ._utils import graph_to_adjacency , list_to_mask , vector_to_dict
6+ from graphblas_algorithms .classes .digraph import to_graph
7+ from graphblas_algorithms .classes .graph import to_undirected_graph
8+ from graphblas_algorithms .utils import not_implemented_for
109
1110
12- def get_properties (G , names , * , L = None , U = None , degrees = None , has_self_edges = True ):
13- """Calculate properties of undirected graph"""
14- if isinstance (names , str ):
15- # Separated by commas and/or spaces
16- names = [name for name in names .replace (" " , "," ).split ("," ) if name ]
17- rv = []
18- for name in names :
19- if name == "L" :
20- if L is None :
21- L = select .tril (G , - 1 ).new (name = "L" )
22- rv .append (L )
23- elif name == "U" :
24- if U is None :
25- U = select .triu (G , 1 ).new (name = "U" )
26- rv .append (U )
27- elif name == "degrees" :
28- if degrees is None :
29- degrees = get_degrees (G , L = L , U = U , has_self_edges = has_self_edges )
30- rv .append (degrees )
31- elif name == "has_self_edges" :
32- # Compute if cheap
33- if L is not None :
34- has_self_edges = G .nvals > 2 * L .nvals
35- elif U is not None :
36- has_self_edges = G .nvals > 2 * U .nvals
37- rv .append (has_self_edges )
38- else :
39- raise ValueError (f"Unknown property name: { name } " )
40- if len (rv ) == 1 :
41- return rv [0 ]
42- return rv
43-
44-
45- def get_degrees (G , mask = None , * , L = None , U = None , has_self_edges = True ):
46- if L is not None :
47- has_self_edges = G .nvals > 2 * L .nvals
48- elif U is not None :
49- has_self_edges = G .nvals > 2 * U .nvals
50- if has_self_edges :
51- if L is None or U is None :
52- L , U = get_properties (G , "L U" , L = L , U = U )
53- degrees = (
54- L .reduce_rowwise ("count" ).new (mask = mask ) + U .reduce_rowwise ("count" ).new (mask = mask )
55- ).new (name = "degrees" )
56- else :
57- degrees = G .reduce_rowwise ("count" ).new (mask = mask , name = "degrees" )
58- return degrees
59-
60-
61- def single_triangle_core (G , index , * , L = None , has_self_edges = True ):
62- r = G [index , :].new ()
63- has_self_edges = get_properties (G , "has_self_edges" , L = L , has_self_edges = has_self_edges )
64- if has_self_edges :
65- # Pretty much all the time is spent here taking TRIL, which is used to ignore self-edges
66- L = get_properties (G , "L" , L = L )
11+ def single_triangle_core (G , node ):
12+ index = G ._key_to_id [node ]
13+ r = G ._A [index , :].new ()
14+ # Pretty much all the time is spent here taking TRIL, which is used to ignore self-edges
15+ L = G .get_property ("L-" )
16+ if G .get_property ("has_self_edges" ):
6717 del r [index ] # Ignore self-edges
68- return plus_pair (L @ r ).new (mask = r .S ).reduce (allow_empty = False ).value
69- else :
70- return plus_pair (G @ r ).new (mask = r .S ).reduce (allow_empty = False ).value // 2
18+ return plus_pair (L @ r ).new (mask = r .S ).reduce (allow_empty = False ).value
7119
7220
73- def triangles_core (G , mask = None , * , L = None , U = None ):
21+ def triangles_core (G , mask = None ):
7422 # Ignores self-edges
75- L , U = get_properties (G , "L U" , L = L , U = U )
23+ L , U = G . get_properties ("L- U-" )
7624 C = plus_pair (L @ L .T ).new (mask = L .S )
7725 return (
7826 C .reduce_rowwise ().new (mask = mask )
@@ -83,39 +31,38 @@ def triangles_core(G, mask=None, *, L=None, U=None):
8331
8432@not_implemented_for ("directed" )
8533def triangles (G , nodes = None ):
34+ G = to_undirected_graph (G , dtype = bool )
8635 if len (G ) == 0 :
8736 return {}
88- A , key_to_id = graph_to_adjacency (G , dtype = bool )
8937 if nodes in G :
90- return single_triangle_core (A , key_to_id [ nodes ] )
91- mask , id_to_key = list_to_mask (nodes , key_to_id )
92- result = triangles_core (A , mask = mask )
93- return vector_to_dict (result , key_to_id , id_to_key , mask = mask , fillvalue = 0 )
38+ return single_triangle_core (G , nodes )
39+ mask = G . list_to_mask (nodes )
40+ result = triangles_core (G , mask = mask )
41+ return G . vector_to_dict (result , mask = mask , fillvalue = 0 )
9442
9543
96- def total_triangles_core (G , * , L = None , U = None ):
44+ def total_triangles_core (G ):
9745 # We use SandiaDot method, because it's usually the fastest on large graphs.
9846 # For smaller graphs, Sandia method is usually faster: plus_pair(L @ L).new(mask=L.S)
99- L , U = get_properties (G , "L U" , L = L , U = U )
47+ L , U = G . get_properties ("L- U-" )
10048 return plus_pair (L @ U .T ).new (mask = L .S ).reduce_scalar (allow_empty = False ).value
10149
10250
103- def transitivity_core (G , * , L = None , U = None , degrees = None ):
104- L , U = get_properties (G , "L U" , L = L , U = U )
105- numerator = total_triangles_core (G , L = L , U = U )
51+ def transitivity_core (G ):
52+ numerator = total_triangles_core (G )
10653 if numerator == 0 :
10754 return 0
108- degrees = get_properties ( G , "degrees" , L = L , U = U , degrees = degrees )
55+ degrees = G . get_property ( "degrees-" )
10956 denom = (degrees * (degrees - 1 )).reduce ().value
11057 return 6 * numerator / denom
11158
11259
113- def transitivity_directed_core (G , * , has_self_edges = True ):
60+ def transitivity_directed_core (G ):
11461 # XXX" is transitivity supposed to work on directed graphs like this?
115- if has_self_edges :
116- A = select . offdiag ( G )
62+ if G . get_property ( " has_self_edges" ) :
63+ A = G . get_property ( "offdiag" )
11764 else :
118- A = G
65+ A = G . _A
11966 numerator = plus_pair (A @ A .T ).new (mask = A .S ).reduce_scalar (allow_empty = False ).value
12067 if numerator == 0 :
12168 return 0
@@ -125,32 +72,28 @@ def transitivity_directed_core(G, *, has_self_edges=True):
12572
12673
12774def transitivity (G ):
75+ G = to_graph (G , dtype = bool ) # directed or undirected
12876 if len (G ) == 0 :
12977 return 0
130- A = gb .io .from_networkx (G , weight = None , dtype = bool )
131- if isinstance (G , nx .DiGraph ):
132- return transitivity_directed_core (A )
78+ if G .is_directed ():
79+ return transitivity_directed_core (G )
13380 else :
134- return transitivity_core (A )
81+ return transitivity_core (G )
13582
13683
137- def clustering_core (G , mask = None , * , L = None , U = None , degrees = None ):
138- L , U = get_properties (G , "L U" , L = L , U = U )
139- tri = triangles_core (G , mask = mask , L = L , U = U )
140- degrees = get_degrees (G , mask = mask , L = L , U = U )
84+ def clustering_core (G , mask = None ):
85+ tri = triangles_core (G , mask = mask )
86+ degrees = G .get_property ("degrees-" )
14187 denom = degrees * (degrees - 1 )
14288 return (2 * tri / denom ).new (name = "clustering" )
14389
14490
145- def clustering_directed_core (G , mask = None , * , has_self_edges = True ):
146- # TODO: Alright, this introduces us to properties of directed graphs:
147- # has_self_edges, offdiag, row_degrees, column_degrees, total_degrees, recip_degrees
148- # (in_degrees, out_degrees?)
149- if has_self_edges :
150- A = select .offdiag (G )
91+ def clustering_directed_core (G , mask = None ):
92+ if G .get_property ("has_self_edges" ):
93+ A = G .get_property ("offdiag" )
15194 else :
152- A = G
153- AT = A . T . new ( )
95+ A = G . _A
96+ AT = G . get_property ( "AT" )
15497 temp = plus_pair (A @ A .T ).new (mask = A .S )
15598 tri = (
15699 temp .reduce_rowwise ().new (mask = mask )
@@ -165,27 +108,32 @@ def clustering_directed_core(G, mask=None, *, has_self_edges=True):
165108 return (tri / (total_degrees * (total_degrees - 1 ) - 2 * recip_degrees )).new (name = "clustering" )
166109
167110
168- def single_clustering_core (G , index , * , L = None , degrees = None , has_self_edges = True ):
169- has_self_edges = get_properties (G , "has_self_edges" , L = L , has_self_edges = has_self_edges )
170- tri = single_triangle_core (G , index , L = L , has_self_edges = has_self_edges )
111+ def single_clustering_core (G , node ):
112+ tri = single_triangle_core (G , node )
171113 if tri == 0 :
172114 return 0
173- if degrees is not None :
174- degrees = degrees [index ].value
115+ index = G ._key_to_id [node ]
116+ if "degrees-" in G ._cache :
117+ degrees = G .get_property ("degrees-" )[index ].value
118+ elif "degrees+" in G ._cache :
119+ degrees = G .get_property ("degrees+" )[index ].value
120+ if G .get_property ("has_self_edges" ) and G ._A [index , index ].value is not None :
121+ degrees -= 1
175122 else :
176- row = G [index , :].new ()
123+ row = G . _A [index , :].new ()
177124 degrees = row .nvals
178- if has_self_edges and row [index ].value is not None :
125+ if G . get_property ( " has_self_edges" ) and row [index ].value is not None :
179126 degrees -= 1
180127 denom = degrees * (degrees - 1 )
181128 return 2 * tri / denom
182129
183130
184- def single_clustering_directed_core (G , index , * , has_self_edges = True ):
185- if has_self_edges :
186- A = select . offdiag ( G )
131+ def single_clustering_directed_core (G , node , * , has_self_edges = True ):
132+ if G . get_property ( " has_self_edges" ) :
133+ A = G . get_property ( "offdiag" )
187134 else :
188- A = G
135+ A = G ._A
136+ index = G ._key_to_id [node ]
189137 r = A [index , :].new ()
190138 c = A [:, index ].new ()
191139 tri = (
@@ -202,27 +150,27 @@ def single_clustering_directed_core(G, index, *, has_self_edges=True):
202150
203151
204152def clustering (G , nodes = None , weight = None ):
205- if len (G ) == 0 :
206- return {}
207153 if weight is not None :
208154 # TODO: Not yet implemented. Clustering implemented only for unweighted.
209155 return _nx_clustering (G , nodes = nodes , weight = weight )
210- A , key_to_id = graph_to_adjacency (G , weight = weight )
156+ G = to_graph (G , weight = weight ) # to directed or undirected
157+ if len (G ) == 0 :
158+ return {}
211159 if nodes in G :
212- if isinstance ( G , nx . DiGraph ):
213- return single_clustering_directed_core (A , key_to_id [ nodes ] )
160+ if G . is_directed ( ):
161+ return single_clustering_directed_core (G , nodes )
214162 else :
215- return single_clustering_core (A , key_to_id [ nodes ] )
216- mask , id_to_key = list_to_mask (nodes , key_to_id )
217- if isinstance ( G , nx . DiGraph ):
218- result = clustering_directed_core (A , mask = mask )
163+ return single_clustering_core (G , nodes )
164+ mask = G . list_to_mask (nodes )
165+ if G . is_directed ( ):
166+ result = clustering_directed_core (G , mask = mask )
219167 else :
220- result = clustering_core (A , mask = mask )
221- return vector_to_dict (result , key_to_id , id_to_key , mask = mask , fillvalue = 0.0 )
168+ result = clustering_core (G , mask = mask )
169+ return G . vector_to_dict (result , mask = mask , fillvalue = 0.0 )
222170
223171
224- def average_clustering_core (G , mask = None , count_zeros = True , * , L = None , U = None , degrees = None ):
225- c = clustering_core (G , mask = mask , L = L , U = U , degrees = degrees )
172+ def average_clustering_core (G , mask = None , count_zeros = True ):
173+ c = clustering_core (G , mask = mask )
226174 val = c .reduce (allow_empty = False ).value
227175 if not count_zeros :
228176 return val / c .nvals
@@ -232,8 +180,8 @@ def average_clustering_core(G, mask=None, count_zeros=True, *, L=None, U=None, d
232180 return val / c .size
233181
234182
235- def average_clustering_directed_core (G , mask = None , count_zeros = True , * , has_self_edges = True ):
236- c = clustering_directed_core (G , mask = mask , has_self_edges = has_self_edges )
183+ def average_clustering_directed_core (G , mask = None , count_zeros = True ):
184+ c = clustering_directed_core (G , mask = mask )
237185 val = c .reduce (allow_empty = False ).value
238186 if not count_zeros :
239187 return val / c .nvals
@@ -244,14 +192,14 @@ def average_clustering_directed_core(G, mask=None, count_zeros=True, *, has_self
244192
245193
246194def average_clustering (G , nodes = None , weight = None , count_zeros = True ):
247- if len (G ) == 0 :
248- raise ZeroDivisionError () # Not covered
249195 if weight is not None :
250196 # TODO: Not yet implemented. Clustering implemented only for unweighted.
251197 return _nx_average_clustering (G , nodes = nodes , weight = weight , count_zeros = count_zeros )
252- A , key_to_id = graph_to_adjacency (G , weight = weight )
253- mask , _ = list_to_mask (nodes , key_to_id )
254- if isinstance (G , nx .DiGraph ):
255- return average_clustering_directed_core (A , mask = mask , count_zeros = count_zeros )
198+ G = to_graph (G , weight = weight ) # to directed or undirected
199+ if len (G ) == 0 :
200+ raise ZeroDivisionError () # Not covered
201+ mask = G .list_to_mask (nodes )
202+ if G .is_directed ():
203+ return average_clustering_directed_core (G , mask = mask , count_zeros = count_zeros )
256204 else :
257- return average_clustering_core (A , mask = mask , count_zeros = count_zeros )
205+ return average_clustering_core (G , mask = mask , count_zeros = count_zeros )
0 commit comments