sier2 0.37__tar.gz → 0.40__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.

Potentially problematic release.


This version of sier2 might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sier2
3
- Version: 0.37
3
+ Version: 0.40
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 = "0.40"
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]]
@@ -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.
@@ -219,6 +219,15 @@ class Dag:
219
219
  if any(not isinstance(c, Connection) for c in connections):
220
220
  raise BlockError('All arguments must be Connection instances')
221
221
 
222
+ # Because this is probably the first place that the Block instance is used,
223
+ # this is a convenient place to check that the block was correctly initialised.
224
+ #
225
+ # Pick an arbitrary attribute that should be present.
226
+ #
227
+ for b in src, dst:
228
+ if not hasattr(b, 'block_doc'):
229
+ raise BlockError(f'Did you call super().__init__() in {b}?')
230
+
222
231
  if _DISALLOW_CYCLES:
223
232
  if _has_cycle(self._block_pairs + [(src, dst)]):
224
233
  raise BlockError('This connection would create a cycle')
@@ -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()
@@ -6,12 +6,14 @@ import sys
6
6
  import threading
7
7
  from typing import Callable
8
8
 
9
+ import param.parameterized as paramp
10
+
9
11
  from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
10
12
  from .._dag import _InputValues
11
13
  from .._util import trim
12
14
  from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
13
15
  from ._panel_util import _get_state_color, dag_doc
14
- from ._chart import bokeh_graph
16
+ from ._chart import html_graph
15
17
 
16
18
  NTHREADS = 2
17
19
 
@@ -255,7 +257,11 @@ def _prepare_to_show(dag: Dag):
255
257
  pn.Column(
256
258
  switch,
257
259
  # 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),
260
+ pn.Row(
261
+ pn.panel(html_graph(dag)),
262
+ max_width=400,
263
+ max_height=200,
264
+ ),
259
265
  log_feed,
260
266
  info_fp_holder
261
267
  )
@@ -430,10 +436,30 @@ class BlockCard(pn.Card):
430
436
 
431
437
  self.header[-1] = self._get_state_light(_get_state_color(_block_state))
432
438
 
439
+ def _sier2_label_formatter(pname: str):
440
+ """Default formatter to turn parameter names into appropriate widget labels.
441
+
442
+ Make labels nicer for Panel.
443
+
444
+ Panel uses the label to display a caption for the corresponding input widgets.
445
+ The default label is taken from the name of the param, which means the default
446
+ caption starts with "In ".
447
+
448
+ Removes the "in_" prefix from input parameters, then passes the param name
449
+ to paramp.default_label_formatter.
450
+ """
451
+
452
+ if pname.startswith('in_'):
453
+ pname = pname[3:]
454
+
455
+ return paramp.default_label_formatter(pname)
456
+
433
457
  class PanelDag(Dag):
434
458
  def __init__(self, *, site: str='Panel Dag', title: str, doc: str):
435
459
  super().__init__(site=site, title=title, doc=doc)
436
460
 
461
+ paramp.label_formatter = _sier2_label_formatter
462
+
437
463
  def show(self):
438
464
  _show_dag(self)
439
465
 
@@ -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