sier2 0.37__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sier2
3
- Version: 0.37
3
+ Version: 1.0.0
4
4
  Summary: Blocks of code that are executed in dags
5
5
  Author: Algol60
6
6
  Author-email: algol60 <algol60@users.noreply.github.com>
@@ -23,7 +23,7 @@ a processing dag pipeline. Blocks are an improvement on libraries;
23
23
  if you have a library, you still need to build an application.
24
24
  Blocks are pieces of an application, you just have to connect them.
25
25
 
26
- See the ``examples`` directory for examples.
26
+ See the ``examples`` directory in the ``sier2-tutorial`` repository for examples.
27
27
 
28
28
  Description
29
29
  -----------
@@ -49,11 +49,11 @@ A typical block implementation looks like this.
49
49
  class Increment(Block):
50
50
  """A block that adds one to the input value."""
51
51
 
52
- int_in = param.Integer(label='The input', doc='An integer')
53
- int_out = param.Integer(label='The output', doc='The incremented value')
52
+ in_int = param.Integer(label='The input', doc='An integer')
53
+ out_int = param.Integer(label='The output', doc='The incremented value')
54
54
 
55
55
  def execute(self):
56
- self.int_out = self.int_in + 1
56
+ self.out_int = self.in_int + 1
57
57
 
58
58
  See the examples in ``examples`` (Python scripts) and ``examples-panel`` (scripts that use `Panel <https://panel.holoviz.org/>`_ as a UI).
59
59
 
@@ -6,7 +6,7 @@ a processing dag pipeline. Blocks are an improvement on libraries;
6
6
  if you have a library, you still need to build an application.
7
7
  Blocks are pieces of an application, you just have to connect them.
8
8
 
9
- See the ``examples`` directory for examples.
9
+ See the ``examples`` directory in the ``sier2-tutorial`` repository for examples.
10
10
 
11
11
  Description
12
12
  -----------
@@ -32,11 +32,11 @@ A typical block implementation looks like this.
32
32
  class Increment(Block):
33
33
  """A block that adds one to the input value."""
34
34
 
35
- int_in = param.Integer(label='The input', doc='An integer')
36
- int_out = param.Integer(label='The output', doc='The incremented value')
35
+ in_int = param.Integer(label='The input', doc='An integer')
36
+ out_int = param.Integer(label='The output', doc='The incremented value')
37
37
 
38
38
  def execute(self):
39
- self.int_out = self.int_in + 1
39
+ self.out_int = self.in_int + 1
40
40
 
41
41
  See the examples in ``examples`` (Python scripts) and ``examples-panel`` (scripts that use `Panel <https://panel.holoviz.org/>`_ as a UI).
42
42
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sier2"
3
- version = "0.37"
3
+ version = "1.0.0"
4
4
  description = "Blocks of code that are executed in dags"
5
5
  authors = [
6
6
  {name="Algol60", email="algol60 <algol60@users.noreply.github.com>"}
@@ -21,7 +21,7 @@ classifiers = [
21
21
  [dependencies]
22
22
  python = "^3.11"
23
23
 
24
- panel = ">=1.4.4"
24
+ panel = ">=1.6.3"
25
25
  param = ">=2.1.0"
26
26
 
27
27
  [[tool.mypy.overrides]]
@@ -70,7 +70,7 @@ class Block(param.Parameterized):
70
70
  There are three kinds of parameters:
71
71
  * Input parameters start with ``in_``. These parameters are set before a block is executed.
72
72
  * Output parameters start with ``out_``. The block sets these in its ``execute()`` method.
73
- * Block parameters start with ``block_``. THese are reserved for use by blocks.
73
+ * Block parameters start with ``block_``. These are reserved for use by blocks.
74
74
 
75
75
  A typical block will have at least one input parameter, and an ``execute()``
76
76
  method that is called when an input parameter value changes.
@@ -137,10 +137,6 @@ class Block(param.Parameterized):
137
137
  #
138
138
  self._block_out_params = []
139
139
 
140
- # self._block_context = _EmptyContext()
141
-
142
- # self._progress = None
143
-
144
140
  @classmethod
145
141
  def block_key(cls):
146
142
  """The unique key of this block class.
@@ -38,9 +38,7 @@ class _InputValues:
38
38
  dst: Block
39
39
 
40
40
  # The values to be set before the block executes.
41
- # For a normal block, values will be non-empty when execute() is called.
42
- # For an input block, if values is non-empty, prepare()
43
- # will be called, else execute() will be called
41
+ # Values will be non-empty when execute() is called.
44
42
  #
45
43
  values: dict[str, Any] = field(default_factory=dict)
46
44
 
@@ -219,6 +217,15 @@ class Dag:
219
217
  if any(not isinstance(c, Connection) for c in connections):
220
218
  raise BlockError('All arguments must be Connection instances')
221
219
 
220
+ # Because this is probably the first place that the Block instance is used,
221
+ # this is a convenient place to check that the block was correctly initialised.
222
+ #
223
+ # Pick an arbitrary attribute that should be present.
224
+ #
225
+ for b in src, dst:
226
+ if not hasattr(b, 'block_doc'):
227
+ raise BlockError(f'Did you call super().__init__() in {b}?')
228
+
222
229
  if _DISALLOW_CYCLES:
223
230
  if _has_cycle(self._block_pairs + [(src, dst)]):
224
231
  raise BlockError('This connection would create a cycle')
@@ -295,7 +302,7 @@ class Dag:
295
302
  self._block_queue.append(item)
296
303
 
297
304
  def execute_after_input(self, block: Block, *, dag_logger=None):
298
- """Execute the dag after running ``prepare()`` in an input block.
305
+ """Execute the dag after running ``prepare()``.
299
306
 
300
307
  After prepare() executes, and the user has possibly
301
308
  provided input, the dag must continue with execute() in the
@@ -331,8 +338,8 @@ class Dag:
331
338
  that block's execute() method.
332
339
 
333
340
  If the current destination block's ``block_pause_execution` is True,
334
- the loop will call ``block.prepare()` instead of ``block.execute()``,
335
- then stop; execute() will return the block that is puased on.
341
+ the loop will call ``block.prepare()``, then stop; execute()
342
+ will return the block that is puased on.
336
343
  The dag can then be restarted with ``dag.execute_after_input()``,
337
344
  using the paused block as the parameter.
338
345
 
@@ -394,17 +401,17 @@ class Dag:
394
401
  'sier2_block_': f'{item.dst}'
395
402
  }
396
403
 
397
- # If this is an input block, and there are input
398
- # values, call prepare() if it exists.
399
- #
400
- if is_input_block and not is_restart:# and item.values:
404
+ # If we need to wait for a user, just run prepare().
405
+ # If we are restarting, just run execute().
406
+ # Otherwise, run both.
407
+ if is_input_block and not is_restart:
401
408
  self.logging(g.prepare, **logging_params)()
409
+ elif is_restart:
410
+ self.logging(g.execute, **logging_params)()
402
411
  else:
412
+ self.logging(g.prepare, **logging_params)()
403
413
  self.logging(g.execute, **logging_params)()
404
414
 
405
- # print(f'{is_input_block=}')
406
- # print(f'{is_restart=}')
407
- # print(f'{item.values=}')
408
415
  if is_input_block and not is_restart:# and item.values:
409
416
  # If the current destination block requires user input,
410
417
  # stop executing the dag immediately, because we don't
@@ -0,0 +1,313 @@
1
+ # Generate a dag chart.
2
+ # We stick to raw python/html here to keep sier's dependencies to a minimum,
3
+ # even if other libraries would be easier to get plots from.
4
+ #
5
+
6
+ from sier2 import Block, Dag
7
+
8
+
9
+ import panel as pn
10
+ import math
11
+
12
+ class HTMLGraph:
13
+ """A class to generate network graph visualizations using Panel HTML pane"""
14
+
15
+ def __init__(self, width=400, height=400, background="#f8f9fa"):
16
+ self.width = width
17
+ self.height = height
18
+ self.background = background
19
+ self.nodes = {}
20
+ self.edges = []
21
+ self.node_styles = {
22
+ "default": {
23
+ "background-color": "#4a86e8",
24
+ "color": "black",
25
+ "border": "none",
26
+ "font-weight": "bold"
27
+ },
28
+ "input": {
29
+ "background-color": "#f2df0c",
30
+ "color": "black",
31
+ "font-weight": "bold"
32
+ }
33
+ }
34
+ self.edge_styles = {
35
+ "default": {
36
+ "background-color": "#666",
37
+ "height": "2px"
38
+ }
39
+ }
40
+
41
+ def add_node_style(self, style_name, style_dict):
42
+ """Add a new node style"""
43
+ self.node_styles[style_name] = style_dict
44
+
45
+ def add_edge_style(self, style_name, style_dict):
46
+ """Add a new edge style"""
47
+ self.edge_styles[style_name] = style_dict
48
+
49
+ def add_node(self, node_id, label, x=None, y=None, style="default"):
50
+ """Add a node to the graph"""
51
+ # If no position specified, assign random position
52
+ if x is None:
53
+ x = random.randint(50, self.width - 50)
54
+ if y is None:
55
+ y = random.randint(50, self.height - 50)
56
+
57
+ self.nodes[node_id] = {
58
+ "label": label,
59
+ "x": x,
60
+ "y": y,
61
+ "style": style
62
+ }
63
+
64
+ def add_edge(self, source_id, target_id, label=None, style="default"):
65
+ """Add an edge between two nodes"""
66
+ if source_id in self.nodes and target_id in self.nodes:
67
+ self.edges.append({
68
+ "source": source_id,
69
+ "target": target_id,
70
+ "label": label,
71
+ "style": style
72
+ })
73
+
74
+ # def layout_circle(self, center_x=None, center_y=None, radius=None):
75
+ # """Arrange nodes in a circle"""
76
+ # if center_x is None:
77
+ # center_x = self.width / 2
78
+ # if center_y is None:
79
+ # center_y = self.height / 2
80
+ # if radius is None:
81
+ # radius = min(self.width, self.height) / 3
82
+
83
+ # node_ids = list(self.nodes.keys())
84
+ # for i, node_id in enumerate(node_ids):
85
+ # angle = 2 * math.pi * i / len(node_ids)
86
+ # self.nodes[node_id]["x"] = center_x + radius * math.cos(angle)
87
+ # self.nodes[node_id]["y"] = center_y + radius * math.sin(angle)
88
+
89
+ def generate_html(self):
90
+ """Generate HTML representation of the network graph"""
91
+ html = f"""
92
+ <style>
93
+ .graph-container {{
94
+ position: relative;
95
+ width: {self.width}px;
96
+ height: {self.height}px;
97
+ background-color: {self.background};
98
+ border: 1px solid #ddd;
99
+ overflow: hidden;
100
+ }}
101
+
102
+ .node {{
103
+ position: absolute;
104
+ width: 60px;
105
+ height: 60px;
106
+ border-radius: 50%;
107
+ display: flex;
108
+ justify-content: center;
109
+ align-items: center;
110
+ text-align: center;
111
+ font-size: 14px;
112
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
113
+ cursor: pointer;
114
+ transform-origin: center;
115
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
116
+ z-index: 10;
117
+ }}
118
+
119
+ # .node:hover {{
120
+ # transform: scale(1.1);
121
+ # box-shadow: 0 4px 8px rgba(0,0,0,0.3);
122
+ # z-index: 20;
123
+ # }}
124
+
125
+ .edge {{
126
+ position: absolute;
127
+ height: 2px;
128
+ transform-origin: left center;
129
+ z-index: 5;
130
+ }}
131
+
132
+ .edge-label {{
133
+ position: absolute;
134
+ background-color: white;
135
+ padding: 2px 6px;
136
+ border-radius: 3px;
137
+ font-size: 12px;
138
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
139
+ z-index: 15;
140
+ }}
141
+ </style>
142
+
143
+ <div class="graph-container">
144
+ """
145
+
146
+ # Add edges first (so they're behind nodes)
147
+ for edge in self.edges:
148
+ source = self.nodes[edge["source"]]
149
+ target = self.nodes[edge["target"]]
150
+
151
+ # Calculate edge properties
152
+ x1, y1 = source["x"], source["y"]
153
+ x2, y2 = target["x"], target["y"]
154
+
155
+ # Calculate length and angle
156
+ length = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
157
+ angle = math.atan2(y2 - y1, x2 - x1) * (180 / math.pi)
158
+
159
+ # Get style properties
160
+ style_props = self.edge_styles[edge["style"]]
161
+ style_attr = "; ".join([f"{k}: {v}" for k, v in style_props.items()])
162
+
163
+ # Create edge element
164
+ html += f"""
165
+ <div class="edge"
166
+ style="width: {length}px;
167
+ left: {x1}px;
168
+ top: {y1}px;
169
+ transform: rotate({angle}deg);
170
+ {style_attr}">
171
+ </div>
172
+ """
173
+
174
+ # Add edge label if specified
175
+ if edge["label"]:
176
+ # Position label at the middle of the edge
177
+ label_x = (x1 + x2) / 2 - 15
178
+ label_y = (y1 + y2) / 2 - 10
179
+
180
+ html += f"""
181
+ <div class="edge-label" style="left: {label_x}px; top: {label_y}px;">
182
+ {edge["label"]}
183
+ </div>
184
+ """
185
+
186
+ # Add nodes
187
+ for node_id, node in self.nodes.items():
188
+ # Get style properties
189
+ style_props = self.node_styles[node["style"]]
190
+ style_attr = "; ".join([f"{k}: {v}" for k, v in style_props.items()])
191
+
192
+ # Create node element
193
+ html += f"""
194
+ <div class="node" id="node-{node_id}"
195
+ style="left: {node['x'] - 30}px;
196
+ top: {node['y'] - 30}px;
197
+ {style_attr}"
198
+ onclick="highlightNode('{node_id}')">
199
+ {node["label"]}
200
+ </div>
201
+ """
202
+
203
+ # # Add some basic interactivity with JavaScript
204
+ # html += """
205
+ # <script>
206
+ # function highlightNode(nodeId) {
207
+ # // Reset all nodes
208
+ # const nodes = document.querySelectorAll('.node');
209
+ # nodes.forEach(n => n.style.transform = 'scale(1)');
210
+
211
+ # // Highlight the selected node
212
+ # const node = document.getElementById('node-' + nodeId);
213
+ # node.style.transform = 'scale(1.2)';
214
+ # }
215
+ # </script>
216
+ # """
217
+
218
+ html += "</div>"
219
+ return html
220
+
221
+ def get_pane(self, width=None, height=None):
222
+ """Get a Panel pane with the graph"""
223
+ if width is None:
224
+ width = self.width + 50
225
+ if height is None:
226
+ height = self.height + 50
227
+
228
+ return pn.pane.HTML(self.generate_html(), width=width, height=height)
229
+
230
+ def html_graph(dag: Dag):
231
+ """Build a Bokeh figure to visualise the block connections."""
232
+
233
+ src: list[Block] = []
234
+ dst: list[Block] = []
235
+
236
+ def build_layers():
237
+ """Traverse the block pairs and organise them into layers.
238
+
239
+ The first layer contains the root (no input) nodes.
240
+ """
241
+
242
+ ranks = {}
243
+ remaining = dag._block_pairs[:]
244
+
245
+ # Find the root nodes and assign them a layer.
246
+ #
247
+ src[:], dst[:] = zip(*remaining)
248
+ S = list(set([s for s in src if s not in dst]))
249
+ for s in S:
250
+ ranks[s.name] = 0
251
+
252
+ n_layers = 1
253
+ while remaining:
254
+ for s, d in remaining:
255
+ if s.name in ranks:
256
+ # This destination could be from sources at different layers.
257
+ # Make sure the deepest one is used.
258
+ #
259
+ ranks[d.name] = max(ranks.get(d.name, 0), ranks[s.name] + 1)
260
+ n_layers = max(n_layers, ranks[d.name])
261
+
262
+ remaining = [(s,d) for s,d in remaining if d.name not in ranks]
263
+
264
+ return n_layers, ranks
265
+
266
+ def layout():
267
+ """Arrange the graph nodes."""
268
+
269
+ max_width = 0
270
+
271
+ # Arrange the graph y by layer from top to bottom.
272
+ # For x, for now we start at 0 and +1 in each layer.
273
+ #
274
+ yx = {y:0 for y in ranks.values()}
275
+ gxy = {}
276
+ for g, y in ranks.items():
277
+ gxy[g] = [yx[y], y]
278
+ yx[y] += 1
279
+ max_width = max(max_width, yx[y])
280
+
281
+ # Balance out the x in each layer.
282
+ #
283
+ for y in range(n_layers+1):
284
+ layer = {name: xy for name,xy in gxy.items() if xy[1]==y}
285
+ if len(layer)<max_width:
286
+ for x, (name, xy) in enumerate(layer.items(), 1):
287
+ gxy[name][0] = x/max_width
288
+
289
+ return gxy
290
+
291
+ graph = HTMLGraph()
292
+
293
+ n_layers, ranks = build_layers()
294
+
295
+ ly = layout()
296
+
297
+ max_layer_width = max([ly[n][0] for n in ly.keys()])
298
+
299
+ vertical_scale = graph.height / (n_layers + 2)
300
+ horizontal_scale = graph.width / (max_layer_width + 2)
301
+
302
+ for node in ly.keys():
303
+ graph.add_node(
304
+ node, node,
305
+ x=(ly[node][0] + 1) * horizontal_scale,
306
+ y=(ly[node][1] + 1) * vertical_scale,
307
+ style='default' if not dag.block_by_name(node).block_pause_execution else 'input'
308
+ )
309
+
310
+ for s, d in dag._block_pairs:
311
+ graph.add_edge(s.name, d.name)
312
+
313
+ return graph.get_pane()
@@ -4,14 +4,18 @@ import html
4
4
  import panel as pn
5
5
  import sys
6
6
  import threading
7
+ import warnings
7
8
  from typing import Callable
8
9
 
10
+ import param.parameterized as paramp
11
+ from param.parameters import DataFrame
12
+
9
13
  from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
10
14
  from .._dag import _InputValues
11
15
  from .._util import trim
12
16
  from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
13
17
  from ._panel_util import _get_state_color, dag_doc
14
- from ._chart import bokeh_graph
18
+ from ._chart import html_graph
15
19
 
16
20
  NTHREADS = 2
17
21
 
@@ -250,12 +254,16 @@ def _prepare_to_show(dag: Dag):
250
254
 
251
255
  cards.extend(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted() if gw.block_visible)
252
256
 
253
- template.main.append(pn.Column(*cards))
257
+ template.main.append(pn.panel(pn.Column(*cards)))
254
258
  template.sidebar.append(
255
259
  pn.Column(
256
260
  switch,
257
261
  # pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
258
- pn.Row(pn.panel(bokeh_graph(dag)), max_width=400, max_height=200),
262
+ pn.Row(
263
+ pn.panel(html_graph(dag)),
264
+ max_width=400,
265
+ max_height=200,
266
+ ),
259
267
  log_feed,
260
268
  info_fp_holder
261
269
  )
@@ -296,14 +304,26 @@ def _serveable_dag(dag: Dag):
296
304
  template.servable()
297
305
 
298
306
  def _default_panel(self) -> Callable[[Block], pn.Param]:
299
- """Provide a default __panel__() implementation for blocks that don't have one.
307
+ """Provide a default __panel__() implementation for blocks that don't have one.param.parameters.
300
308
 
301
309
  This default will display the in_ parameters.
302
310
  """
303
-
311
+
304
312
  in_names = [name for name in self.param.values() if name.startswith('in_')]
305
313
 
306
- return pn.Param(self, parameters=in_names, show_name=False)
314
+ # Check if we need tabulator installed.
315
+ # Ostensibly param uses the DataFrame widget if the tabulator extension isn't present,
316
+ # but this doesn't seem to work properly.
317
+ #
318
+ if any([isinstance(self.param[name], DataFrame) for name in in_names]):
319
+ if 'tabulator' not in pn.extension._loaded_extensions:
320
+ tabulator_warning = f'One of your blocks ({self.__class__.__name__}) requires Tabulator, a panel extension for showing data frames. You should explicitly load this with "pn.extension(\'tabulator\')" in your block'
321
+ warnings.warn(tabulator_warning)
322
+ pn.extension('tabulator')
323
+
324
+ param_pane = pn.Param(self, parameters=in_names, show_name=False)
325
+
326
+ return param_pane
307
327
 
308
328
  class BlockCard(pn.Card):
309
329
  """A custom card to wrap around a block.
@@ -430,12 +450,32 @@ class BlockCard(pn.Card):
430
450
 
431
451
  self.header[-1] = self._get_state_light(_get_state_color(_block_state))
432
452
 
453
+ def _sier2_label_formatter(pname: str):
454
+ """Default formatter to turn parameter names into appropriate widget labels.
455
+
456
+ Make labels nicer for Panel.
457
+
458
+ Panel uses the label to display a caption for the corresponding input widgets.
459
+ The default label is taken from the name of the param, which means the default
460
+ caption starts with "In ".
461
+
462
+ Removes the "in_" prefix from input parameters, then passes the param name
463
+ to paramp.default_label_formatter.
464
+ """
465
+
466
+ if pname.startswith('in_'):
467
+ pname = pname[3:]
468
+
469
+ return paramp.default_label_formatter(pname)
470
+
433
471
  class PanelDag(Dag):
434
472
  def __init__(self, *, site: str='Panel Dag', title: str, doc: str):
435
473
  super().__init__(site=site, title=title, doc=doc)
436
474
 
475
+ paramp.label_formatter = _sier2_label_formatter
476
+
437
477
  def show(self):
438
478
  _show_dag(self)
439
479
 
440
480
  def servable(self):
441
- _serveable_dag(self)
481
+ _serveable_dag(self)
@@ -0,0 +1,25 @@
1
+ from sier2 import Block, Connection
2
+ from sier2.panel import PanelDag
3
+ import param
4
+ import time
5
+ import pandas as pd
6
+ import panel as pn
7
+
8
+ class StartBlock(Block):
9
+ """Starts the test dag"""
10
+
11
+ out_data = param.DataFrame()
12
+
13
+ def execute(self):
14
+ time.sleep(3)
15
+ self.out_data = pd.DataFrame({'A':[1,2,3,4], 'B':[5,6,7,8]})
16
+
17
+ class FinishBlock(Block):
18
+ """Finishes the test dag"""
19
+ in_data = param.DataFrame()
20
+
21
+ sb = StartBlock(block_pause_execution=True)
22
+ fb = FinishBlock()
23
+ dag = PanelDag(doc='test', title='test')
24
+ dag.connect(sb, fb, Connection('out_data', 'in_data'))
25
+ dag.show()
@@ -1,106 +0,0 @@
1
- # Generate a dag chart.
2
- #
3
-
4
- from sier2 import Block, Dag
5
-
6
- from bokeh.plotting import figure, show, ColumnDataSource, curdoc, output_notebook
7
- from bokeh.models import HoverTool
8
-
9
- from bokeh.resources import INLINE
10
- output_notebook(resources=INLINE)
11
-
12
- def bokeh_graph(dag: Dag):
13
- """Build a Bokeh figure to visualise the block connections."""
14
-
15
- src: list[Block] = []
16
- dst: list[Block] = []
17
-
18
- def build_layers():
19
- """Traverse the block pairs and organise them into layers.
20
-
21
- The first layer contains the root (no input) nodes.
22
- """
23
-
24
- ranks = {}
25
- remaining = dag._block_pairs[:]
26
-
27
- # Find the root nodes and assign them a layer.
28
- #
29
- src[:], dst[:] = zip(*remaining)
30
- S = list(set([s for s in src if s not in dst]))
31
- for s in S:
32
- ranks[s.name] = 0
33
-
34
- n_layers = 1
35
- while remaining:
36
- for s, d in remaining:
37
- if s.name in ranks:
38
- # This destination could be from sources at different layers.
39
- # Make sure the deepest one is used.
40
- #
41
- ranks[d.name] = max(ranks.get(d.name, 0), ranks[s.name] + 1)
42
- n_layers = max(n_layers, ranks[d.name])
43
-
44
- remaining = [(s,d) for s,d in remaining if d.name not in ranks]
45
-
46
- return n_layers, ranks
47
-
48
- def layout():
49
- """Arrange the graph nodes."""
50
-
51
- max_width = 0
52
-
53
- # Arrange the graph y by layer from top to bottom.
54
- # For x, for now we start at 0 and +1 in each layer.
55
- #
56
- yx = {y:0 for y in ranks.values()}
57
- gxy = {}
58
- for g, y in ranks.items():
59
- gxy[g] = [yx[y], y]
60
- yx[y] += 1
61
- max_width = max(max_width, yx[y])
62
-
63
- # Balance out the x in each layer.
64
- #
65
- for y in range(n_layers+1):
66
- layer = {name: xy for name,xy in gxy.items() if xy[1]==y}
67
- if len(layer)<max_width:
68
- for x, (name, xy) in enumerate(layer.items(), 1):
69
- gxy[name][0] = x/max_width
70
-
71
- return gxy
72
-
73
- n_layers, ranks = build_layers()
74
-
75
- ly = layout()
76
-
77
- linexs = []
78
- lineys = []
79
- for s, d in dag._block_pairs:
80
- print(s.name, d.name)
81
- linexs.append((ly[s.name][0], ly[d.name][0]))
82
- lineys.append((ly[s.name][1], ly[d.name][1]))
83
-
84
- xs, ys = zip(*ly.values())
85
-
86
- c_source = ColumnDataSource({
87
- 'xs': xs,
88
- 'ys': ys,
89
- 'names': list(ly.keys())
90
- })
91
- l_source = ColumnDataSource({
92
- 'linexs': linexs,
93
- 'lineys': lineys
94
- })
95
-
96
- curdoc().theme = 'dark_minimal'
97
- p = figure(tools='pan,wheel_zoom,box_zoom,reset', height=300, width=300)
98
- p.axis.visible = False
99
- p.xgrid.visible = False
100
- p.ygrid.visible = False
101
- p.y_range.flipped = True # y-axis goes down instead of up.
102
- l = p.multi_line(xs='linexs', ys='lineys', source=l_source)
103
- c = p.circle(x='xs', y='ys', radius=0.05, line_color='black', fill_color='steelblue', hover_fill_color='#7f7f7f', source=c_source)
104
- p.add_tools(HoverTool(tooltips=[('Block', '@names')], renderers=[c]))
105
-
106
- return p
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes