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;
|