darabos commited on
Commit
f407b8d
·
unverified ·
2 Parent(s): 03c259a cfc0b8a

Merge pull request #181 from biggraph/darabos-multi-output

Browse files
examples/Multi-output demo.lynxkite.json ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "edges": [
3
+ {
4
+ "id": "Multi-output example 1 one View tables 1 bundle",
5
+ "source": "Multi-output example 1",
6
+ "sourceHandle": "one",
7
+ "target": "View tables 1",
8
+ "targetHandle": "bundle"
9
+ },
10
+ {
11
+ "id": "Multi-output example 1 two View tables 2 bundle",
12
+ "source": "Multi-output example 1",
13
+ "sourceHandle": "two",
14
+ "target": "View tables 2",
15
+ "targetHandle": "bundle"
16
+ }
17
+ ],
18
+ "env": "LynxKite Graph Analytics",
19
+ "nodes": [
20
+ {
21
+ "data": {
22
+ "__execution_delay": 0.0,
23
+ "collapsed": false,
24
+ "display": null,
25
+ "error": null,
26
+ "input_metadata": [],
27
+ "meta": {
28
+ "color": "orange",
29
+ "doc": [
30
+ {
31
+ "kind": "text",
32
+ "value": "Returns two outputs. Also demonstrates Numpy-style docstrings."
33
+ },
34
+ {
35
+ "kind": "parameters",
36
+ "value": [
37
+ {
38
+ "annotation": "int",
39
+ "description": "Number of elements in output \"one\".",
40
+ "name": "a_limit"
41
+ },
42
+ {
43
+ "annotation": "int",
44
+ "description": "Number of elements in output \"two\".",
45
+ "name": "b_limit"
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ "kind": "returns",
51
+ "value": [
52
+ {
53
+ "annotation": "A dict with two DataFrames in it.",
54
+ "description": "",
55
+ "name": ""
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ "inputs": [],
61
+ "name": "Multi-output example",
62
+ "outputs": [
63
+ {
64
+ "name": "one",
65
+ "position": "right",
66
+ "type": {
67
+ "type": "None"
68
+ }
69
+ },
70
+ {
71
+ "name": "two",
72
+ "position": "right",
73
+ "type": {
74
+ "type": "None"
75
+ }
76
+ }
77
+ ],
78
+ "params": [
79
+ {
80
+ "default": 4,
81
+ "name": "a_limit",
82
+ "type": {
83
+ "type": "<class 'int'>"
84
+ }
85
+ },
86
+ {
87
+ "default": 10,
88
+ "name": "b_limit",
89
+ "type": {
90
+ "type": "<class 'int'>"
91
+ }
92
+ }
93
+ ],
94
+ "type": "basic"
95
+ },
96
+ "params": {
97
+ "a_limit": "2",
98
+ "b_limit": "10"
99
+ },
100
+ "status": "done",
101
+ "title": "Multi-output example"
102
+ },
103
+ "dragHandle": ".bg-primary",
104
+ "height": 275.0,
105
+ "id": "Multi-output example 1",
106
+ "position": {
107
+ "x": 86.0,
108
+ "y": 33.0
109
+ },
110
+ "type": "basic",
111
+ "width": 200.0
112
+ },
113
+ {
114
+ "data": {
115
+ "display": {
116
+ "dataframes": {
117
+ "df": {
118
+ "columns": [
119
+ "a"
120
+ ],
121
+ "data": [
122
+ [
123
+ 0
124
+ ],
125
+ [
126
+ 1
127
+ ]
128
+ ]
129
+ }
130
+ },
131
+ "other": {},
132
+ "relations": []
133
+ },
134
+ "error": null,
135
+ "input_metadata": [
136
+ {
137
+ "dataframes": {
138
+ "df": {
139
+ "columns": [
140
+ "a"
141
+ ]
142
+ }
143
+ },
144
+ "other": {},
145
+ "relations": []
146
+ }
147
+ ],
148
+ "meta": {
149
+ "color": "orange",
150
+ "doc": null,
151
+ "inputs": [
152
+ {
153
+ "name": "bundle",
154
+ "position": "left",
155
+ "type": {
156
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
157
+ }
158
+ }
159
+ ],
160
+ "name": "View tables",
161
+ "outputs": [],
162
+ "params": [
163
+ {
164
+ "default": 100,
165
+ "name": "limit",
166
+ "type": {
167
+ "type": "<class 'int'>"
168
+ }
169
+ }
170
+ ],
171
+ "type": "table_view"
172
+ },
173
+ "params": {
174
+ "limit": 100.0
175
+ },
176
+ "status": "done",
177
+ "title": "View tables"
178
+ },
179
+ "dragHandle": ".bg-primary",
180
+ "height": 200.0,
181
+ "id": "View tables 1",
182
+ "position": {
183
+ "x": 485.0,
184
+ "y": -31.0
185
+ },
186
+ "type": "table_view",
187
+ "width": 200.0
188
+ },
189
+ {
190
+ "data": {
191
+ "display": {
192
+ "dataframes": {
193
+ "df": {
194
+ "columns": [
195
+ "b"
196
+ ],
197
+ "data": [
198
+ [
199
+ 0
200
+ ],
201
+ [
202
+ 1
203
+ ],
204
+ [
205
+ 2
206
+ ],
207
+ [
208
+ 3
209
+ ],
210
+ [
211
+ 4
212
+ ],
213
+ [
214
+ 5
215
+ ],
216
+ [
217
+ 6
218
+ ],
219
+ [
220
+ 7
221
+ ],
222
+ [
223
+ 8
224
+ ],
225
+ [
226
+ 9
227
+ ]
228
+ ]
229
+ }
230
+ },
231
+ "other": {},
232
+ "relations": []
233
+ },
234
+ "error": null,
235
+ "input_metadata": [
236
+ {
237
+ "dataframes": {
238
+ "df": {
239
+ "columns": [
240
+ "b"
241
+ ]
242
+ }
243
+ },
244
+ "other": {},
245
+ "relations": []
246
+ }
247
+ ],
248
+ "meta": {
249
+ "color": "orange",
250
+ "doc": null,
251
+ "inputs": [
252
+ {
253
+ "name": "bundle",
254
+ "position": "left",
255
+ "type": {
256
+ "type": "<class 'lynxkite_graph_analytics.core.Bundle'>"
257
+ }
258
+ }
259
+ ],
260
+ "name": "View tables",
261
+ "outputs": [],
262
+ "params": [
263
+ {
264
+ "default": 100,
265
+ "name": "limit",
266
+ "type": {
267
+ "type": "<class 'int'>"
268
+ }
269
+ }
270
+ ],
271
+ "type": "table_view"
272
+ },
273
+ "params": {
274
+ "limit": 100.0
275
+ },
276
+ "status": "done",
277
+ "title": "View tables"
278
+ },
279
+ "dragHandle": ".bg-primary",
280
+ "height": 215.0,
281
+ "id": "View tables 2",
282
+ "position": {
283
+ "x": 480.0,
284
+ "y": 191.0
285
+ },
286
+ "type": "table_view",
287
+ "width": 225.0
288
+ }
289
+ ]
290
+ }
examples/multi_output_demo.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lynxkite.core.ops import op
2
+ import pandas as pd
3
+
4
+
5
+ @op("LynxKite Graph Analytics", "Multi-output example", outputs=["one", "two"])
6
+ def multi_output(*, a_limit=4, b_limit=10):
7
+ """
8
+ Returns two outputs. Also demonstrates Numpy-style docstrings.
9
+
10
+ Parameters
11
+ ----------
12
+ a_limit : int
13
+ Number of elements in output "one".
14
+ b_limit : int
15
+ Number of elements in output "two".
16
+
17
+ Returns
18
+ -------
19
+ A dict with two DataFrames in it.
20
+ """
21
+ return {
22
+ "one": pd.DataFrame({"a": range(a_limit)}),
23
+ "two": pd.DataFrame({"b": range(b_limit)}),
24
+ }
lynxkite-app/web/src/index.css CHANGED
@@ -100,6 +100,15 @@ body {
100
  font-size: 16px;
101
  font-weight: initial;
102
  max-width: 300px;
 
 
 
 
 
 
 
 
 
103
  }
104
 
105
  .expanded .lynxkite-node {
 
100
  font-size: 16px;
101
  font-weight: initial;
102
  max-width: 300px;
103
+
104
+ p {
105
+ margin-top: 1em;
106
+ line-height: normal;
107
+ }
108
+
109
+ p:first-child {
110
+ margin-top: 0;
111
+ }
112
  }
113
 
114
  .expanded .lynxkite-node {
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx CHANGED
@@ -42,10 +42,12 @@ function getHandles(inputs: any[], outputs: any[]) {
42
  e.index = counts[e.position];
43
  counts[e.position]++;
44
  }
 
 
 
 
45
  for (const e of handles) {
46
  e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
47
- const simpleHorizontal = counts.top === 0 && counts.bottom === 0 && handles.length <= 2;
48
- const simpleVertical = counts.left === 0 && counts.right === 0 && handles.length <= 2;
49
  e.showLabel = !simpleHorizontal && !simpleVertical;
50
  }
51
  return handles;
 
42
  e.index = counts[e.position];
43
  counts[e.position]++;
44
  }
45
+ const simpleHorizontal =
46
+ counts.top === 0 && counts.bottom === 0 && counts.left <= 1 && counts.right <= 1;
47
+ const simpleVertical =
48
+ counts.left === 0 && counts.right === 0 && counts.top <= 1 && counts.bottom <= 1;
49
  for (const e of handles) {
50
  e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
 
 
51
  e.showLabel = !simpleHorizontal && !simpleVertical;
52
  }
53
  return handles;
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -440,5 +440,8 @@ def get_doc(func):
440
  return func.__doc__
441
  if func.__doc__ is None:
442
  return None
443
- doc = griffe.Docstring(func.__doc__).parse("google")
 
 
 
444
  return json.loads(json.dumps(doc, cls=griffe.JSONEncoder))
 
440
  return func.__doc__
441
  if func.__doc__ is None:
442
  return None
443
+ if "----" in func.__doc__:
444
+ doc = griffe.Docstring(func.__doc__).parse("numpy")
445
+ else:
446
+ doc = griffe.Docstring(func.__doc__).parse("google")
447
  return json.loads(json.dumps(doc, cls=griffe.JSONEncoder))
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py CHANGED
@@ -161,11 +161,15 @@ def disambiguate_edges(ws: workspace.Workspace):
161
  seen.add((edge.target, edge.targetHandle))
162
 
163
 
 
 
 
 
164
  @ops.register_executor(ENV)
165
  async def execute(ws: workspace.Workspace):
166
- catalog: dict[str, ops.Op] = ops.CATALOGS[ws.env]
167
  disambiguate_edges(ws)
168
- outputs = {}
169
  nodes = {node.id: node for node in ws.nodes}
170
  todo = set(nodes.keys())
171
  progress = True
@@ -173,8 +177,12 @@ async def execute(ws: workspace.Workspace):
173
  progress = False
174
  for id in list(todo):
175
  node = nodes[id]
176
- input_nodes = [edge.source for edge in ws.edges if edge.target == id]
177
- if all(input in outputs for input in input_nodes):
 
 
 
 
178
  # All inputs for this node are ready, we can compute the output.
179
  todo.remove(id)
180
  progress = True
@@ -187,7 +195,9 @@ async def await_if_needed(obj):
187
  return obj
188
 
189
 
190
- async def _execute_node(node, ws, catalog, outputs):
 
 
191
  params = {**node.data.params}
192
  op = catalog.get(node.data.title)
193
  if not op:
@@ -196,7 +206,9 @@ async def _execute_node(node, ws, catalog, outputs):
196
  node.publish_started()
197
  # TODO: Handle multi-inputs.
198
  input_map = {
199
- edge.targetHandle: outputs[edge.source] for edge in ws.edges if edge.target == node.id
 
 
200
  }
201
  # Convert inputs types to match operation signature.
202
  try:
@@ -228,8 +240,12 @@ async def _execute_node(node, ws, catalog, outputs):
228
  traceback.print_exc()
229
  result = ops.Result(error=str(e))
230
  result.input_metadata = [_get_metadata(i) for i in inputs]
231
- if result.output is not None:
232
- outputs[node.id] = result.output
 
 
 
 
233
  node.publish_result(result)
234
 
235
 
 
161
  seen.add((edge.target, edge.targetHandle))
162
 
163
 
164
+ # Outputs are tracked by node ID and output ID.
165
+ Outputs = dict[tuple[str, str], typing.Any]
166
+
167
+
168
  @ops.register_executor(ENV)
169
  async def execute(ws: workspace.Workspace):
170
+ catalog = ops.CATALOGS[ws.env]
171
  disambiguate_edges(ws)
172
+ outputs: Outputs = {}
173
  nodes = {node.id: node for node in ws.nodes}
174
  todo = set(nodes.keys())
175
  progress = True
 
177
  progress = False
178
  for id in list(todo):
179
  node = nodes[id]
180
+ inputs_done = [
181
+ (edge.source, edge.sourceHandle) in outputs
182
+ for edge in ws.edges
183
+ if edge.target == id
184
+ ]
185
+ if all(inputs_done):
186
  # All inputs for this node are ready, we can compute the output.
187
  todo.remove(id)
188
  progress = True
 
195
  return obj
196
 
197
 
198
+ async def _execute_node(
199
+ node: workspace.WorkspaceNode, ws: workspace.Workspace, catalog: ops.Catalog, outputs: Outputs
200
+ ):
201
  params = {**node.data.params}
202
  op = catalog.get(node.data.title)
203
  if not op:
 
206
  node.publish_started()
207
  # TODO: Handle multi-inputs.
208
  input_map = {
209
+ edge.targetHandle: outputs[edge.source, edge.sourceHandle]
210
+ for edge in ws.edges
211
+ if edge.target == node.id
212
  }
213
  # Convert inputs types to match operation signature.
214
  try:
 
240
  traceback.print_exc()
241
  result = ops.Result(error=str(e))
242
  result.input_metadata = [_get_metadata(i) for i in inputs]
243
+ if isinstance(result.output, dict):
244
+ for k, v in result.output.items():
245
+ outputs[node.id, k] = v
246
+ elif result.output is not None:
247
+ [k] = op.outputs
248
+ outputs[node.id, k.name] = result.output
249
  node.publish_result(result)
250
 
251