darabos commited on
Commit
0c44583
·
1 Parent(s): 9d53329

Try automatically creating an op for every NetworkX function.

Browse files
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 = graph.copy()
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) -> 'graph_view':
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) -> 'table_view':
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(sig.parameters[p].default)
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
- nodes = pd.DataFrame.from_dict(dict(graph.nodes(data=True)), orient='index')
 
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 op(name):
 
 
 
 
 
 
 
 
 
 
 
 
 
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 type == 'basic' else {} # Maybe more fancy later.
97
- op = Op(func, name, params=params, inputs=inputs, outputs=outputs, type=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: 170px;
64
- max-width: 300px;
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;