sier2 0.35__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.
- {sier2-0.35 → sier2-0.40}/PKG-INFO +6 -6
- {sier2-0.35 → sier2-0.40}/README.rst +4 -4
- {sier2-0.35 → sier2-0.40}/pyproject.toml +4 -7
- {sier2-0.35 → sier2-0.40}/src/sier2/_block.py +19 -5
- {sier2-0.35 → sier2-0.40}/src/sier2/_config.py +28 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/_dag.py +10 -71
- sier2-0.40/src/sier2/panel/_chart.py +313 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/panel/_panel.py +30 -2
- {sier2-0.35 → sier2-0.40}/LICENSE +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/__init__.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/__main__.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/_library.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/_logger.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/_util.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/_version.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/panel/__init__.py +0 -0
- {sier2-0.35 → sier2-0.40}/src/sier2/panel/_feedlogger.py +0 -0
- {sier2-0.35 → sier2-0.40}/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: 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>
|
|
@@ -12,7 +12,7 @@ Classifier: Intended Audience :: Science/Research
|
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Topic :: Scientific/Engineering
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
-
Project-URL: Homepage, https://github.com/
|
|
15
|
+
Project-URL: Homepage, https://github.com/sier2/sier2
|
|
16
16
|
Description-Content-Type: text/x-rst
|
|
17
17
|
|
|
18
18
|
Sier2
|
|
@@ -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 = "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,20 +21,17 @@ classifiers = [
|
|
|
21
21
|
[dependencies]
|
|
22
22
|
python = "^3.11"
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
panel = ">=1.4.4"
|
|
24
|
+
panel = ">=1.6.3"
|
|
26
25
|
param = ">=2.1.0"
|
|
27
26
|
|
|
28
27
|
[[tool.mypy.overrides]]
|
|
29
28
|
module = [
|
|
30
|
-
"
|
|
31
|
-
"param",
|
|
32
|
-
"networkx",
|
|
29
|
+
"param"
|
|
33
30
|
]
|
|
34
31
|
ignore_missing_imports = true
|
|
35
32
|
|
|
36
33
|
[project.urls]
|
|
37
|
-
Homepage = "https://github.com/
|
|
34
|
+
Homepage = "https://github.com/sier2/sier2"
|
|
38
35
|
|
|
39
36
|
[build-system]
|
|
40
37
|
requires = ["poetry-core>=2.1.1"]
|
|
@@ -41,6 +41,20 @@ same input block be used immediately.) This causes the block's
|
|
|
41
41
|
Dag execution then continues as normal.
|
|
42
42
|
'''
|
|
43
43
|
|
|
44
|
+
_VISIBLE_DOC = '''If True, the block will be visible in a GUI.
|
|
45
|
+
|
|
46
|
+
A block may not need to be visible in a dag with a GUI. For example,
|
|
47
|
+
it may be applying a pre-defined filter, or running an algorithm that
|
|
48
|
+
takes an indeterminate amount of time. Setting this parameter to False
|
|
49
|
+
tells the GUI not display this block. Dag execution will otherwise
|
|
50
|
+
proceed as normal.
|
|
51
|
+
|
|
52
|
+
This is also useful if a GUI application only requires a single block.
|
|
53
|
+
A dag requires at least two blocks, because blocks can only be added
|
|
54
|
+
by connecting them to another block. By making one block a "dummy"
|
|
55
|
+
that is not visible, the GUI effectivly has a single block.
|
|
56
|
+
'''
|
|
57
|
+
|
|
44
58
|
class Block(param.Parameterized):
|
|
45
59
|
"""The base class for blocks.
|
|
46
60
|
|
|
@@ -82,17 +96,20 @@ class Block(param.Parameterized):
|
|
|
82
96
|
"""
|
|
83
97
|
|
|
84
98
|
block_pause_execution = param.Boolean(default=False, label='Pause execution', doc=_PAUSE_EXECUTION_DOC)
|
|
99
|
+
block_visible = param.Boolean(default=True, label='Display block', doc=_VISIBLE_DOC)
|
|
85
100
|
|
|
86
101
|
_block_state = param.String(default=BlockState.READY)
|
|
87
102
|
|
|
88
103
|
SIER2_KEY = '_sier2__key'
|
|
89
104
|
|
|
90
|
-
def __init__(self, *args, block_pause_execution: bool=False, block_doc: str|None=None, continue_label='Continue', **kwargs):
|
|
105
|
+
def __init__(self, *args, block_pause_execution: bool=False, block_visible: bool=True, block_doc: str|None=None, continue_label='Continue', **kwargs):
|
|
91
106
|
"""
|
|
92
107
|
Parameters
|
|
93
108
|
----------
|
|
94
109
|
block_pause_execution: bool
|
|
95
110
|
If True, ``prepare()`` is called and dag execution stops.
|
|
111
|
+
block_visible: bool
|
|
112
|
+
If True (the default), the block will be visible in a GUI.
|
|
96
113
|
block_doc: str|None
|
|
97
114
|
Markdown documentation that may displayed in the user interface.
|
|
98
115
|
"""
|
|
@@ -102,6 +119,7 @@ class Block(param.Parameterized):
|
|
|
102
119
|
raise BlockError(f'Class {self.__class__} must have a docstring')
|
|
103
120
|
|
|
104
121
|
self.block_pause_execution = block_pause_execution
|
|
122
|
+
self.block_visible = block_visible
|
|
105
123
|
self.block_doc = block_doc
|
|
106
124
|
self.continue_label = continue_label
|
|
107
125
|
# self._block_state = BlockState.READY
|
|
@@ -119,10 +137,6 @@ class Block(param.Parameterized):
|
|
|
119
137
|
#
|
|
120
138
|
self._block_out_params = []
|
|
121
139
|
|
|
122
|
-
# self._block_context = _EmptyContext()
|
|
123
|
-
|
|
124
|
-
# self._progress = None
|
|
125
|
-
|
|
126
140
|
@classmethod
|
|
127
141
|
def block_key(cls):
|
|
128
142
|
"""The unique key of this block class.
|
|
@@ -13,9 +13,37 @@ from typing import Any
|
|
|
13
13
|
CONFIG_UPDATE = 'config_update'
|
|
14
14
|
|
|
15
15
|
def _default_config_file():
|
|
16
|
+
"""Determine the location of the config file sier2.ini.
|
|
17
|
+
|
|
18
|
+
If the environment variable ``SIER2_INI`` is set,
|
|
19
|
+
it specifies the path of the config file.
|
|
20
|
+
|
|
21
|
+
Otherwise, if Windows, use ``$env:APPDATA/sier2/sier2.ini``.
|
|
22
|
+
|
|
23
|
+
Otherwise, if ``XDG_CONFIG_HOME`` is set, use ``$XDG_CONFIG_HOME/sier2/sier2.ini``.
|
|
24
|
+
|
|
25
|
+
Otherwise, use ``$HOME/.config/sier2/sier2.ini``.
|
|
26
|
+
|
|
27
|
+
If not using ``SIER2_INI``, the ``sier2`` directory will be created
|
|
28
|
+
if it does not exist.
|
|
29
|
+
|
|
30
|
+
TODO don't create the sier2 directory until the ini file is written.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# If a config file has been explicitly set in an environment variable,
|
|
34
|
+
# use it.
|
|
35
|
+
#
|
|
36
|
+
ini = os.environ.get('SIER2_INI', None)
|
|
37
|
+
if ini:
|
|
38
|
+
return Path(ini)
|
|
39
|
+
|
|
16
40
|
if os.name=='nt':
|
|
41
|
+
# Windows.
|
|
42
|
+
#
|
|
17
43
|
prdir = Path(os.environ['APPDATA']) / 'sier2'
|
|
18
44
|
else:
|
|
45
|
+
# Linux.
|
|
46
|
+
#
|
|
19
47
|
prdir = os.environ.get('XDG_CONFIG_HOME', None)
|
|
20
48
|
if prdir:
|
|
21
49
|
prdir = Path(prdir)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from ._block import Block, BlockError, BlockValidateError, BlockState
|
|
2
2
|
from dataclasses import dataclass, field #, KW_ONLY, field
|
|
3
|
-
from collections import
|
|
4
|
-
import holoviews as hv
|
|
3
|
+
from collections import deque
|
|
5
4
|
from importlib.metadata import entry_points
|
|
6
5
|
import threading
|
|
7
6
|
import sys
|
|
@@ -220,6 +219,15 @@ class Dag:
|
|
|
220
219
|
if any(not isinstance(c, Connection) for c in connections):
|
|
221
220
|
raise BlockError('All arguments must be Connection instances')
|
|
222
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
|
+
|
|
223
231
|
if _DISALLOW_CYCLES:
|
|
224
232
|
if _has_cycle(self._block_pairs + [(src, dst)]):
|
|
225
233
|
raise BlockError('This connection would create a cycle')
|
|
@@ -578,75 +586,6 @@ class Dag:
|
|
|
578
586
|
'connections': connections
|
|
579
587
|
}
|
|
580
588
|
|
|
581
|
-
def hv_graph(self):
|
|
582
|
-
"""Build a HoloViews Graph to visualise the block connections."""
|
|
583
|
-
|
|
584
|
-
src: list[Block] = []
|
|
585
|
-
dst: list[Block] = []
|
|
586
|
-
|
|
587
|
-
def build_layers():
|
|
588
|
-
"""Traverse the block pairs and organise them into layers.
|
|
589
|
-
|
|
590
|
-
The first layer contains the root (no input) nodes.
|
|
591
|
-
"""
|
|
592
|
-
|
|
593
|
-
ranks = {}
|
|
594
|
-
remaining = self._block_pairs[:]
|
|
595
|
-
|
|
596
|
-
# Find the root nodes and assign them a layer.
|
|
597
|
-
#
|
|
598
|
-
src[:], dst[:] = zip(*remaining)
|
|
599
|
-
S = list(set([s for s in src if s not in dst]))
|
|
600
|
-
for s in S:
|
|
601
|
-
ranks[s.name] = 0
|
|
602
|
-
|
|
603
|
-
n_layers = 1
|
|
604
|
-
while remaining:
|
|
605
|
-
for s, d in remaining:
|
|
606
|
-
if s.name in ranks:
|
|
607
|
-
# This destination could be from sources at different layers.
|
|
608
|
-
# Make sure the deepest one is used.
|
|
609
|
-
#
|
|
610
|
-
ranks[d.name] = max(ranks.get(d.name, 0), ranks[s.name] + 1)
|
|
611
|
-
n_layers = max(n_layers, ranks[d.name])
|
|
612
|
-
|
|
613
|
-
remaining = [(s,d) for s,d in remaining if d.name not in ranks]
|
|
614
|
-
|
|
615
|
-
return n_layers, ranks
|
|
616
|
-
|
|
617
|
-
def layout(_):
|
|
618
|
-
"""Arrange the graph nodes."""
|
|
619
|
-
|
|
620
|
-
max_width = 0
|
|
621
|
-
|
|
622
|
-
# Arrange the graph y by layer from top to bottom.
|
|
623
|
-
# For x, for no we start at 0 and +1 in each layer.
|
|
624
|
-
#
|
|
625
|
-
yx = {y:0 for y in ranks.values()}
|
|
626
|
-
gxy = {}
|
|
627
|
-
for g, y in ranks.items():
|
|
628
|
-
gxy[g] = [yx[y], y]
|
|
629
|
-
yx[y] += 1
|
|
630
|
-
max_width = max(max_width, yx[y])
|
|
631
|
-
|
|
632
|
-
# Balance out the x in each layer.
|
|
633
|
-
#
|
|
634
|
-
for y in range(n_layers+1):
|
|
635
|
-
layer = {name: xy for name,xy in gxy.items() if xy[1]==y}
|
|
636
|
-
if len(layer)<max_width:
|
|
637
|
-
for x, (name, xy) in enumerate(layer.items(), 1):
|
|
638
|
-
gxy[name][0] = x/max_width
|
|
639
|
-
|
|
640
|
-
return gxy
|
|
641
|
-
|
|
642
|
-
n_layers, ranks = build_layers()
|
|
643
|
-
|
|
644
|
-
src_names = [g.name for g in src]
|
|
645
|
-
dst_names = [g.name for g in dst]
|
|
646
|
-
g = hv.Graph(((src_names, dst_names),))
|
|
647
|
-
|
|
648
|
-
return hv.element.graphs.layout_nodes(g, layout=layout)
|
|
649
|
-
|
|
650
589
|
def topological_sort(pairs):
|
|
651
590
|
"""Implement a topological sort as described at
|
|
652
591
|
`Topological sorting <https://en.wikipedia.org/wiki/Topological_sorting>`_.
|
|
@@ -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,11 +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
|
|
16
|
+
from ._chart import html_graph
|
|
14
17
|
|
|
15
18
|
NTHREADS = 2
|
|
16
19
|
|
|
@@ -247,13 +250,18 @@ def _prepare_to_show(dag: Dag):
|
|
|
247
250
|
card = pn.Card(pn.pane.Markdown(doc, sizing_mode='stretch_width'), header=pn.Row(name_text), sizing_mode='stretch_width')
|
|
248
251
|
cards.append(card)
|
|
249
252
|
|
|
250
|
-
cards.extend(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted())
|
|
253
|
+
cards.extend(BlockCard(parent_template=template, dag=dag, w=gw, dag_logger=dag_logger) for gw in dag.get_sorted() if gw.block_visible)
|
|
251
254
|
|
|
252
255
|
template.main.append(pn.Column(*cards))
|
|
253
256
|
template.sidebar.append(
|
|
254
257
|
pn.Column(
|
|
255
258
|
switch,
|
|
256
|
-
pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
|
|
259
|
+
# pn.panel(dag.hv_graph().opts(invert_yaxis=True, xaxis=None, yaxis=None)),
|
|
260
|
+
pn.Row(
|
|
261
|
+
pn.panel(html_graph(dag)),
|
|
262
|
+
max_width=400,
|
|
263
|
+
max_height=200,
|
|
264
|
+
),
|
|
257
265
|
log_feed,
|
|
258
266
|
info_fp_holder
|
|
259
267
|
)
|
|
@@ -428,10 +436,30 @@ class BlockCard(pn.Card):
|
|
|
428
436
|
|
|
429
437
|
self.header[-1] = self._get_state_light(_get_state_color(_block_state))
|
|
430
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
|
+
|
|
431
457
|
class PanelDag(Dag):
|
|
432
458
|
def __init__(self, *, site: str='Panel Dag', title: str, doc: str):
|
|
433
459
|
super().__init__(site=site, title=title, doc=doc)
|
|
434
460
|
|
|
461
|
+
paramp.label_formatter = _sier2_label_formatter
|
|
462
|
+
|
|
435
463
|
def show(self):
|
|
436
464
|
_show_dag(self)
|
|
437
465
|
|
|
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
|