Spaces:
Running
Running
Try automatically creating an op for every NetworkX function.
Browse files- server/basic_ops.py +6 -8
- server/main.py +1 -0
- server/networkx_ops.py +44 -0
- server/ops.py +20 -6
- web/src/LynxKiteNode.svelte +2 -2
server/basic_ops.py
CHANGED
@@ -15,11 +15,9 @@ def create_scale_free_graph(*, nodes: int = 10):
|
|
15 |
return nx.scale_free_graph(nodes)
|
16 |
|
17 |
@ops.op("Compute PageRank")
|
|
|
18 |
def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=3):
|
19 |
-
graph =
|
20 |
-
pr = nx.pagerank(graph, alpha=damping, max_iter=iterations)
|
21 |
-
nx.set_node_attributes(graph, pr, 'pagerank')
|
22 |
-
return graph
|
23 |
|
24 |
|
25 |
def _map_color(value):
|
@@ -28,8 +26,8 @@ def _map_color(value):
|
|
28 |
rgba = cmap(value)
|
29 |
return ['#{:02x}{:02x}{:02x}'.format(int(r*255), int(g*255), int(b*255)) for r, g, b in rgba[:, :3]]
|
30 |
|
31 |
-
@ops.op("Visualize graph")
|
32 |
-
def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = None)
|
33 |
nodes = graph.dfs['nodes'].copy()
|
34 |
node_attributes = sorted(nodes.columns)
|
35 |
if color_nodes_by:
|
@@ -53,8 +51,8 @@ def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = Non
|
|
53 |
}
|
54 |
return v
|
55 |
|
56 |
-
@ops.op("View tables")
|
57 |
-
def view_tables(dfs: ops.Bundle)
|
58 |
v = {
|
59 |
'dataframes': { name: {
|
60 |
'columns': [str(c) for c in df.columns],
|
|
|
15 |
return nx.scale_free_graph(nodes)
|
16 |
|
17 |
@ops.op("Compute PageRank")
|
18 |
+
@ops.nx_node_attribute_func('pagerank')
|
19 |
def compute_pagerank(graph: nx.Graph, *, damping=0.85, iterations=3):
|
20 |
+
return nx.pagerank(graph, alpha=damping, max_iter=iterations)
|
|
|
|
|
|
|
21 |
|
22 |
|
23 |
def _map_color(value):
|
|
|
26 |
rgba = cmap(value)
|
27 |
return ['#{:02x}{:02x}{:02x}'.format(int(r*255), int(g*255), int(b*255)) for r, g, b in rgba[:, :3]]
|
28 |
|
29 |
+
@ops.op("Visualize graph", view="graph_view")
|
30 |
+
def visualize_graph(graph: ops.Bundle, *, color_nodes_by: 'node_attribute' = None):
|
31 |
nodes = graph.dfs['nodes'].copy()
|
32 |
node_attributes = sorted(nodes.columns)
|
33 |
if color_nodes_by:
|
|
|
51 |
}
|
52 |
return v
|
53 |
|
54 |
+
@ops.op("View tables", view="table_view")
|
55 |
+
def view_tables(dfs: ops.Bundle):
|
56 |
v = {
|
57 |
'dataframes': { name: {
|
58 |
'columns': [str(c) for c in df.columns],
|
server/main.py
CHANGED
@@ -7,6 +7,7 @@ import pydantic
|
|
7 |
import traceback
|
8 |
from . import ops
|
9 |
from . import basic_ops
|
|
|
10 |
|
11 |
class BaseConfig(pydantic.BaseModel):
|
12 |
model_config = pydantic.ConfigDict(
|
|
|
7 |
import traceback
|
8 |
from . import ops
|
9 |
from . import basic_ops
|
10 |
+
from . import networkx_ops
|
11 |
|
12 |
class BaseConfig(pydantic.BaseModel):
|
13 |
model_config = pydantic.ConfigDict(
|
server/networkx_ops.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Automatically wraps all NetworkX functions as LynxKite operations."""
|
2 |
+
from . import ops
|
3 |
+
import functools
|
4 |
+
import inspect
|
5 |
+
import networkx as nx
|
6 |
+
|
7 |
+
|
8 |
+
def wrapped(func):
|
9 |
+
@functools.wraps(func)
|
10 |
+
def wrapper(*args, **kwargs):
|
11 |
+
for k, v in kwargs.items():
|
12 |
+
if v == 'None':
|
13 |
+
kwargs[k] = None
|
14 |
+
res = func(*args, **kwargs)
|
15 |
+
if isinstance(res, nx.Graph):
|
16 |
+
return res
|
17 |
+
# Otherwise it's a node attribute.
|
18 |
+
graph = args[0].copy()
|
19 |
+
nx.set_node_attributes(graph, 'attr', name)
|
20 |
+
return graph
|
21 |
+
return wrapper
|
22 |
+
|
23 |
+
|
24 |
+
for (name, func) in nx.__dict__.items():
|
25 |
+
if type(func) == nx.utils.backends._dispatch:
|
26 |
+
sig = inspect.signature(func)
|
27 |
+
inputs = {'G': nx.Graph} if 'G' in sig.parameters else {}
|
28 |
+
params = {
|
29 |
+
name:
|
30 |
+
str(param.default)
|
31 |
+
if type(param.default) in [str, int, float]
|
32 |
+
else None
|
33 |
+
for name, param in sig.parameters.items()
|
34 |
+
if name not in ['G', 'backend', 'backend_kwargs']}
|
35 |
+
for k, v in params.items():
|
36 |
+
if sig.parameters[k].annotation is inspect._empty and v is None:
|
37 |
+
# No annotation, no default — we must guess the type.
|
38 |
+
if len(k) == 1:
|
39 |
+
params[k] = 1
|
40 |
+
if name == 'ladder_graph':
|
41 |
+
print(params)
|
42 |
+
name = "NX › " + name.replace('_', ' ').title()
|
43 |
+
op = ops.Op(wrapped(func), name, params=params, inputs=inputs, outputs={'output': 'yes'}, type='basic')
|
44 |
+
ops.ALL_OPS[name] = op
|
server/ops.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
'''API for implementing LynxKite operations.'''
|
2 |
import dataclasses
|
|
|
3 |
import inspect
|
4 |
import networkx as nx
|
5 |
import pandas as pd
|
@@ -22,7 +23,7 @@ class Op:
|
|
22 |
if p in self.params:
|
23 |
t = sig.parameters[p].annotation
|
24 |
if t is inspect._empty:
|
25 |
-
t = type(
|
26 |
if t == int:
|
27 |
params[p] = int(params[p])
|
28 |
elif t == float:
|
@@ -56,7 +57,8 @@ class Bundle:
|
|
56 |
@classmethod
|
57 |
def from_nx(cls, graph: nx.Graph):
|
58 |
edges = nx.to_pandas_edgelist(graph)
|
59 |
-
|
|
|
60 |
nodes['id'] = nodes.index
|
61 |
return cls(
|
62 |
dfs={'edges': edges, 'nodes': nodes},
|
@@ -79,10 +81,22 @@ class Bundle:
|
|
79 |
return graph
|
80 |
|
81 |
|
82 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
'''Decorator for defining an operation.'''
|
84 |
def decorator(func):
|
85 |
-
type = func.__annotations__.get('return') or 'basic'
|
86 |
sig = inspect.signature(func)
|
87 |
# Positional arguments are inputs.
|
88 |
inputs = {
|
@@ -93,8 +107,8 @@ def op(name):
|
|
93 |
name: param.default if param.default is not inspect._empty else None
|
94 |
for name, param in sig.parameters.items()
|
95 |
if param.kind == param.KEYWORD_ONLY}
|
96 |
-
outputs = {'output': 'yes'} if
|
97 |
-
op = Op(func, name, params=params, inputs=inputs, outputs=outputs, type=
|
98 |
ALL_OPS[name] = op
|
99 |
return func
|
100 |
return decorator
|
|
|
1 |
'''API for implementing LynxKite operations.'''
|
2 |
import dataclasses
|
3 |
+
import functools
|
4 |
import inspect
|
5 |
import networkx as nx
|
6 |
import pandas as pd
|
|
|
23 |
if p in self.params:
|
24 |
t = sig.parameters[p].annotation
|
25 |
if t is inspect._empty:
|
26 |
+
t = type(self.params[p])
|
27 |
if t == int:
|
28 |
params[p] = int(params[p])
|
29 |
elif t == float:
|
|
|
57 |
@classmethod
|
58 |
def from_nx(cls, graph: nx.Graph):
|
59 |
edges = nx.to_pandas_edgelist(graph)
|
60 |
+
d = dict(graph.nodes(data=True))
|
61 |
+
nodes = pd.DataFrame(d.values(), index=d.keys())
|
62 |
nodes['id'] = nodes.index
|
63 |
return cls(
|
64 |
dfs={'edges': edges, 'nodes': nodes},
|
|
|
81 |
return graph
|
82 |
|
83 |
|
84 |
+
def nx_node_attribute_func(name):
|
85 |
+
'''Decorator for wrapping a function that adds a NetworkX node attribute.'''
|
86 |
+
def decorator(func):
|
87 |
+
@functools.wraps(func)
|
88 |
+
def wrapper(graph: nx.Graph, **kwargs):
|
89 |
+
graph = graph.copy()
|
90 |
+
attr = func(graph, **kwargs)
|
91 |
+
nx.set_node_attributes(graph, attr, name)
|
92 |
+
return graph
|
93 |
+
return wrapper
|
94 |
+
return decorator
|
95 |
+
|
96 |
+
|
97 |
+
def op(name, *, view='basic'):
|
98 |
'''Decorator for defining an operation.'''
|
99 |
def decorator(func):
|
|
|
100 |
sig = inspect.signature(func)
|
101 |
# Positional arguments are inputs.
|
102 |
inputs = {
|
|
|
107 |
name: param.default if param.default is not inspect._empty else None
|
108 |
for name, param in sig.parameters.items()
|
109 |
if param.kind == param.KEYWORD_ONLY}
|
110 |
+
outputs = {'output': 'yes'} if view == 'basic' else {} # Maybe more fancy later.
|
111 |
+
op = Op(func, name, params=params, inputs=inputs, outputs=outputs, type=view)
|
112 |
ALL_OPS[name] = op
|
113 |
return func
|
114 |
return decorator
|
web/src/LynxKiteNode.svelte
CHANGED
@@ -60,8 +60,8 @@
|
|
60 |
.lynxkite-node {
|
61 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
62 |
background: white;
|
63 |
-
min-width:
|
64 |
-
max-width:
|
65 |
max-height: 400px;
|
66 |
overflow-y: auto;
|
67 |
border-radius: 1px;
|
|
|
60 |
.lynxkite-node {
|
61 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
62 |
background: white;
|
63 |
+
min-width: 200px;
|
64 |
+
max-width: 400px;
|
65 |
max-height: 400px;
|
66 |
overflow-y: auto;
|
67 |
border-radius: 1px;
|