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.
- {sier2-0.37 → sier2-1.0.0}/PKG-INFO +5 -5
- {sier2-0.37 → sier2-1.0.0}/README.rst +4 -4
- {sier2-0.37 → sier2-1.0.0}/pyproject.toml +2 -2
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_block.py +1 -5
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_dag.py +20 -13
- sier2-1.0.0/src/sier2/panel/_chart.py +313 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/panel/_panel.py +47 -7
- sier2-1.0.0/src/sier2/panel/tabulator_test.py +25 -0
- sier2-0.37/src/sier2/panel/_chart.py +0 -106
- {sier2-0.37 → sier2-1.0.0}/LICENSE +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/__init__.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/__main__.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_config.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_library.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_logger.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_util.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/_version.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/panel/__init__.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/panel/_feedlogger.py +0 -0
- {sier2-0.37 → sier2-1.0.0}/src/sier2/panel/_panel_util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sier2
|
|
3
|
-
Version: 0.
|
|
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
|
-
|
|
53
|
-
|
|
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.
|
|
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
|
-
|
|
36
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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_``.
|
|
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
|
-
#
|
|
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()
|
|
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()
|
|
335
|
-
|
|
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
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
if is_input_block and not is_restart
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|