Spaces:
Running
Running
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 (
|
140 |
return None if value == "" else _param_to_type(name, value, type)
|
141 |
-
case (type,
|
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
|
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
|
27 |
if t.endswith(", optional"):
|
28 |
w = doc_to_type(name, t.removesuffix(", optional").strip())
|
29 |
-
if w is
|
30 |
-
return
|
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 |
-
|
56 |
elif t == '"node (optional)"':
|
57 |
-
return
|
58 |
elif t == '"edge"':
|
59 |
-
|
60 |
elif t == '"edge (optional)"':
|
61 |
-
return
|
62 |
elif t in ["class", "data type"]:
|
63 |
-
|
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 |
-
|
74 |
elif t == "set":
|
75 |
-
|
76 |
elif t == "list of floats":
|
77 |
-
|
78 |
elif t == "list of floats or float":
|
79 |
return float
|
80 |
elif t in ["dict", "dictionary"]:
|
81 |
-
|
82 |
elif t == "scalar or dictionary":
|
83 |
return float
|
84 |
elif t == "none or dict":
|
85 |
-
return
|
86 |
elif t in ["function", "callable"]:
|
87 |
-
|
88 |
elif t in [
|
89 |
"collection",
|
90 |
"container of nodes",
|
91 |
"list of nodes",
|
92 |
]:
|
93 |
-
|
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 |
-
|
106 |
elif t == "generator of sets":
|
107 |
-
|
108 |
elif t == "dict or a set of 2 or 3 tuples":
|
109 |
-
|
110 |
elif t == "set of 2 or 3 tuples":
|
111 |
-
|
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 |
-
|
137 |
-
return
|
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 |
-
|
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 |
-
|
190 |
-
except
|
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,
|