darabos commited on
Commit
e7eaaf3
·
1 Parent(s): a8323d4

More NetworkX fixes.

Browse files
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -136,9 +136,9 @@ def _param_to_type(name, value, type):
136
  return type[value]
137
  if isinstance(type, types.UnionType):
138
  match type.__args__:
139
- case (None, type):
140
  return None if value == "" else _param_to_type(name, value, type)
141
- case (type, None):
142
  return None if value == "" else _param_to_type(name, value, type)
143
  return value
144
 
 
136
  return type[value]
137
  if isinstance(type, types.UnionType):
138
  match type.__args__:
139
+ case (types.NoneType, type):
140
  return None if value == "" else _param_to_type(name, value, type)
141
+ case (type, types.NoneType):
142
  return None if value == "" else _param_to_type(name, value, type)
143
  return value
144
 
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py CHANGED
@@ -122,25 +122,6 @@ def import_osm(*, location: str):
122
  return ox.graph.graph_from_place(location, network_type="drive")
123
 
124
 
125
- @op("Create scale-free graph")
126
- def create_scale_free_graph(*, nodes: int = 10):
127
- """Creates a scale-free graph with the given number of nodes."""
128
- return nx.scale_free_graph(nodes)
129
-
130
-
131
- @op("Compute PageRank")
132
- @core.nx_node_attribute_func("pagerank")
133
- def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=100):
134
- # TODO: This requires scipy to be installed.
135
- return nx.pagerank(graph, alpha=damping, max_iter=iterations)
136
-
137
-
138
- @op("Compute betweenness centrality")
139
- @core.nx_node_attribute_func("betweenness_centrality")
140
- def compute_betweenness_centrality(graph: nx.Graph, *, k=10):
141
- return nx.betweenness_centrality(graph, k=k)
142
-
143
-
144
  @op("Discard loop edges")
145
  def discard_loop_edges(graph: nx.Graph):
146
  graph = graph.copy()
 
122
  return ox.graph.graph_from_place(location, network_type="drive")
123
 
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  @op("Discard loop edges")
126
  def discard_loop_edges(graph: nx.Graph):
127
  graph = graph.copy()
lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py CHANGED
@@ -1,6 +1,7 @@
1
  """Automatically wraps all NetworkX functions as LynxKite operations."""
2
 
3
  import collections
 
4
  from lynxkite.core import ops
5
  import functools
6
  import inspect
@@ -12,10 +13,13 @@ import pandas as pd
12
  ENV = "LynxKite Graph Analytics"
13
 
14
 
15
- class UnsupportedType(Exception):
16
  pass
17
 
18
 
 
 
 
19
  nx.ladder_graph
20
 
21
 
@@ -23,12 +27,12 @@ def doc_to_type(name: str, t: str) -> type:
23
  t = t.lower()
24
  t = re.sub("[(][^)]+[)]", "", t).strip().strip(".")
25
  if " " in name or "http" in name:
26
- return None # Not a parameter type.
27
  if t.endswith(", optional"):
28
  w = doc_to_type(name, t.removesuffix(", optional").strip())
29
- if w is None:
30
- return None
31
- return w | None
32
  if t in [
33
  "a digraph or multidigraph",
34
  "a graph g",
@@ -52,15 +56,15 @@ def doc_to_type(name: str, t: str) -> type:
52
  ]:
53
  return nx.DiGraph
54
  elif t == "node":
55
- raise UnsupportedType(t)
56
  elif t == '"node (optional)"':
57
- return None
58
  elif t == '"edge"':
59
- raise UnsupportedType(t)
60
  elif t == '"edge (optional)"':
61
- return None
62
  elif t in ["class", "data type"]:
63
- raise UnsupportedType(t)
64
  elif t in ["string", "str", "node label"]:
65
  return str
66
  elif t in ["string or none", "none or string", "string, or none"]:
@@ -70,27 +74,27 @@ def doc_to_type(name: str, t: str) -> type:
70
  elif t in ["bool", "boolean"]:
71
  return bool
72
  elif t == "tuple":
73
- raise UnsupportedType(t)
74
  elif t == "set":
75
- raise UnsupportedType(t)
76
  elif t == "list of floats":
77
- raise UnsupportedType(t)
78
  elif t == "list of floats or float":
79
  return float
80
  elif t in ["dict", "dictionary"]:
81
- raise UnsupportedType(t)
82
  elif t == "scalar or dictionary":
83
  return float
84
  elif t == "none or dict":
85
- return None
86
  elif t in ["function", "callable"]:
87
- raise UnsupportedType(t)
88
  elif t in [
89
  "collection",
90
  "container of nodes",
91
  "list of nodes",
92
  ]:
93
- raise UnsupportedType(t)
94
  elif t in [
95
  "container",
96
  "generator",
@@ -102,13 +106,13 @@ def doc_to_type(name: str, t: str) -> type:
102
  "list or tuple",
103
  "list",
104
  ]:
105
- raise UnsupportedType(t)
106
  elif t == "generator of sets":
107
- raise UnsupportedType(t)
108
  elif t == "dict or a set of 2 or 3 tuples":
109
- raise UnsupportedType(t)
110
  elif t == "set of 2 or 3 tuples":
111
- raise UnsupportedType(t)
112
  elif t == "none, string or function":
113
  return str | None
114
  elif t == "string or function" and name == "weight":
@@ -133,8 +137,8 @@ def doc_to_type(name: str, t: str) -> type:
133
  elif name == "weight":
134
  return str
135
  elif t == "object":
136
- raise UnsupportedType(t)
137
- return None
138
 
139
 
140
  def types_from_doc(doc: str) -> dict[str, type]:
@@ -144,9 +148,7 @@ def types_from_doc(doc: str) -> dict[str, type]:
144
  a, b = line.split(":", 1)
145
  for a in a.split(","):
146
  a = a.strip()
147
- t = doc_to_type(a, b)
148
- if t is not None:
149
- types[a] = t
150
  return types
151
 
152
 
@@ -157,12 +159,19 @@ def wrapped(name: str, func):
157
  if v == "None":
158
  kwargs[k] = None
159
  res = func(*args, **kwargs)
160
- if isinstance(res, nx.Graph):
161
- return res
162
  # Figure out what the returned value is.
163
  if isinstance(res, nx.Graph):
164
  return res
 
 
 
 
 
 
 
165
  if isinstance(res, collections.abc.Sized):
 
 
166
  for a in args:
167
  if isinstance(a, nx.Graph):
168
  if a.number_of_nodes() == len(res):
@@ -179,36 +188,78 @@ def wrapped(name: str, func):
179
  return wrapper
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def register_networkx(env: str):
183
  cat = ops.CATALOGS.setdefault(env, {})
184
  counter = 0
185
  for name, func in nx.__dict__.items():
186
  if hasattr(func, "graphs"):
187
- sig = inspect.signature(func)
188
  try:
189
- types = types_from_doc(func.__doc__)
190
- except UnsupportedType:
191
  continue
192
- for k, param in sig.parameters.items():
193
- if k in types:
194
- continue
195
- if param.annotation is not param.empty:
196
- types[k] = param.annotation
197
- if k in ["i", "j", "n"]:
198
- types[k] = int
199
  inputs = {k: ops.Input(name=k, type=nx.Graph) for k in func.graphs}
200
- params = {
201
- name: ops.Parameter.basic(
202
- name=name,
203
- default=str(param.default)
204
- if type(param.default) in [str, int, float]
205
- else None,
206
- type=types[name],
207
- )
208
- for name, param in sig.parameters.items()
209
- if name in types and types[name] not in [nx.Graph, nx.DiGraph]
210
- }
211
  nicename = "NX › " + name.replace("_", " ").title()
 
 
212
  op = ops.Op(
213
  func=wrapped(name, func),
214
  name=nicename,
 
1
  """Automatically wraps all NetworkX functions as LynxKite operations."""
2
 
3
  import collections
4
+ import types
5
  from lynxkite.core import ops
6
  import functools
7
  import inspect
 
13
  ENV = "LynxKite Graph Analytics"
14
 
15
 
16
+ class UnsupportedParameterType(Exception):
17
  pass
18
 
19
 
20
+ _UNSUPPORTED = object()
21
+ _SKIP = object()
22
+
23
  nx.ladder_graph
24
 
25
 
 
27
  t = t.lower()
28
  t = re.sub("[(][^)]+[)]", "", t).strip().strip(".")
29
  if " " in name or "http" in name:
30
+ return _UNSUPPORTED # Not a parameter type.
31
  if t.endswith(", optional"):
32
  w = doc_to_type(name, t.removesuffix(", optional").strip())
33
+ if w is _UNSUPPORTED:
34
+ return _SKIP
35
+ return w if w is _SKIP else w | None
36
  if t in [
37
  "a digraph or multidigraph",
38
  "a graph g",
 
56
  ]:
57
  return nx.DiGraph
58
  elif t == "node":
59
+ return _UNSUPPORTED
60
  elif t == '"node (optional)"':
61
+ return _SKIP
62
  elif t == '"edge"':
63
+ return _UNSUPPORTED
64
  elif t == '"edge (optional)"':
65
+ return _SKIP
66
  elif t in ["class", "data type"]:
67
+ return _UNSUPPORTED
68
  elif t in ["string", "str", "node label"]:
69
  return str
70
  elif t in ["string or none", "none or string", "string, or none"]:
 
74
  elif t in ["bool", "boolean"]:
75
  return bool
76
  elif t == "tuple":
77
+ return _UNSUPPORTED
78
  elif t == "set":
79
+ return _UNSUPPORTED
80
  elif t == "list of floats":
81
+ return _UNSUPPORTED
82
  elif t == "list of floats or float":
83
  return float
84
  elif t in ["dict", "dictionary"]:
85
+ return _UNSUPPORTED
86
  elif t == "scalar or dictionary":
87
  return float
88
  elif t == "none or dict":
89
+ return _SKIP
90
  elif t in ["function", "callable"]:
91
+ return _UNSUPPORTED
92
  elif t in [
93
  "collection",
94
  "container of nodes",
95
  "list of nodes",
96
  ]:
97
+ return _UNSUPPORTED
98
  elif t in [
99
  "container",
100
  "generator",
 
106
  "list or tuple",
107
  "list",
108
  ]:
109
+ return _UNSUPPORTED
110
  elif t == "generator of sets":
111
+ return _UNSUPPORTED
112
  elif t == "dict or a set of 2 or 3 tuples":
113
+ return _UNSUPPORTED
114
  elif t == "set of 2 or 3 tuples":
115
+ return _UNSUPPORTED
116
  elif t == "none, string or function":
117
  return str | None
118
  elif t == "string or function" and name == "weight":
 
137
  elif name == "weight":
138
  return str
139
  elif t == "object":
140
+ return _UNSUPPORTED
141
+ return _SKIP
142
 
143
 
144
  def types_from_doc(doc: str) -> dict[str, type]:
 
148
  a, b = line.split(":", 1)
149
  for a in a.split(","):
150
  a = a.strip()
151
+ types[a] = doc_to_type(a, b)
 
 
152
  return types
153
 
154
 
 
159
  if v == "None":
160
  kwargs[k] = None
161
  res = func(*args, **kwargs)
 
 
162
  # Figure out what the returned value is.
163
  if isinstance(res, nx.Graph):
164
  return res
165
+ if isinstance(res, types.GeneratorType):
166
+ res = list(res)
167
+ if name in ["articulation_points"]:
168
+ graph = args[0].copy()
169
+ nx.set_node_attributes(graph, 0, name=name)
170
+ nx.set_node_attributes(graph, {r: 1 for r in res}, name=name)
171
+ return graph
172
  if isinstance(res, collections.abc.Sized):
173
+ if len(res) == 0:
174
+ return pd.DataFrame()
175
  for a in args:
176
  if isinstance(a, nx.Graph):
177
  if a.number_of_nodes() == len(res):
 
188
  return wrapper
189
 
190
 
191
+ def _get_params(func) -> dict | None:
192
+ sig = inspect.signature(func)
193
+ # Get types from docstring.
194
+ types = types_from_doc(func.__doc__)
195
+ # Always hide these.
196
+ for k in ["backend", "backend_kwargs", "create_using"]:
197
+ types[k] = _SKIP
198
+ # Add in types based on signature.
199
+ for k, param in sig.parameters.items():
200
+ if k in types:
201
+ continue
202
+ if param.annotation is not param.empty:
203
+ types[k] = param.annotation
204
+ if k in ["i", "j", "n"]:
205
+ types[k] = int
206
+ params = {}
207
+ for name, param in sig.parameters.items():
208
+ _type = types.get(name, _UNSUPPORTED)
209
+ if _type is _UNSUPPORTED:
210
+ raise UnsupportedParameterType(name)
211
+ if _type is _SKIP or _type in [nx.Graph, nx.DiGraph]:
212
+ continue
213
+ params[name] = ops.Parameter.basic(
214
+ name=name,
215
+ default=str(param.default)
216
+ if type(param.default) in [str, int, float]
217
+ else None,
218
+ type=_type,
219
+ )
220
+ return params
221
+
222
+
223
+ _REPLACEMENTS = [
224
+ ("Barabasi Albert", "Barabasi–Albert"),
225
+ ("Bellman Ford", "Bellman–Ford"),
226
+ ("Bethe Hessian", "Bethe–Hessian"),
227
+ ("Bfs", "BFS"),
228
+ ("Dag ", "DAG "),
229
+ ("Dfs", "DFS"),
230
+ ("Dorogovtsev Goltsev Mendes", "Dorogovtsev–Goltsev–Mendes"),
231
+ ("Erdos Renyi", "Erdos–Renyi"),
232
+ ("Floyd Warshall", "Floyd–Warshall"),
233
+ ("Gnc", "G(n,c)"),
234
+ ("Gnm", "G(n,m)"),
235
+ ("Gnp", "G(n,p)"),
236
+ ("Gnr", "G(n,r)"),
237
+ ("Havel Hakimi", "Havel–Hakimi"),
238
+ ("Hkn", "H(k,n)"),
239
+ ("Hnm", "H(n,m)"),
240
+ ("Kl ", "KL "),
241
+ ("Moebius Kantor", "Moebius–Kantor"),
242
+ ("Pagerank", "PageRank"),
243
+ ("Scale Free", "Scale-Free"),
244
+ ("Vf2Pp", "VF2++"),
245
+ ("Watts Strogatz", "Watts–Strogatz"),
246
+ ("Weisfeiler Lehman", "Weisfeiler–Lehman"),
247
+ ]
248
+
249
+
250
  def register_networkx(env: str):
251
  cat = ops.CATALOGS.setdefault(env, {})
252
  counter = 0
253
  for name, func in nx.__dict__.items():
254
  if hasattr(func, "graphs"):
 
255
  try:
256
+ params = _get_params(func)
257
+ except UnsupportedParameterType:
258
  continue
 
 
 
 
 
 
 
259
  inputs = {k: ops.Input(name=k, type=nx.Graph) for k in func.graphs}
 
 
 
 
 
 
 
 
 
 
 
260
  nicename = "NX › " + name.replace("_", " ").title()
261
+ for a, b in _REPLACEMENTS:
262
+ nicename = nicename.replace(a, b)
263
  op = ops.Op(
264
  func=wrapped(name, func),
265
  name=nicename,