wperf-cmn-visualizer 0.1.0__py3-none-any.whl
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.
- wperf_cmn_visualizer/__init__.py +34 -0
- wperf_cmn_visualizer/__main__.py +76 -0
- wperf_cmn_visualizer/canvas.py +305 -0
- wperf_cmn_visualizer/cmn.py +219 -0
- wperf_cmn_visualizer/cmn_metrics.py +290 -0
- wperf_cmn_visualizer/config.py +68 -0
- wperf_cmn_visualizer/renderer.py +617 -0
- wperf_cmn_visualizer/telemetry_loader.py +67 -0
- wperf_cmn_visualizer/time_scrubber.py +294 -0
- wperf_cmn_visualizer/topology_loader.py +61 -0
- wperf_cmn_visualizer/wperf_cmn_visualizer.py +125 -0
- wperf_cmn_visualizer-0.1.0.dist-info/METADATA +48 -0
- wperf_cmn_visualizer-0.1.0.dist-info/RECORD +17 -0
- wperf_cmn_visualizer-0.1.0.dist-info/WHEEL +5 -0
- wperf_cmn_visualizer-0.1.0.dist-info/entry_points.txt +2 -0
- wperf_cmn_visualizer-0.1.0.dist-info/licenses/LICENSE +29 -0
- wperf_cmn_visualizer-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# BSD 3-Clause License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025, Arm Limited
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
# list of conditions and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
# and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
wperf-cmn-visualizer - A topology visualization tool for CMN mesh networks.
|
|
33
|
+
"""
|
|
34
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# BSD 3-Clause License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025, Arm Limited
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
# list of conditions and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
# and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
Entry point for running cmn-visualizer as a module.
|
|
33
|
+
"""
|
|
34
|
+
import sys
|
|
35
|
+
import argparse
|
|
36
|
+
from . import __version__
|
|
37
|
+
from .config import Config
|
|
38
|
+
from .wperf_cmn_visualizer import wperfCmnVisualizer
|
|
39
|
+
try:
|
|
40
|
+
from ctypes import windll
|
|
41
|
+
except ImportError:
|
|
42
|
+
windll = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_args() -> argparse.Namespace:
|
|
46
|
+
parser = argparse.ArgumentParser(
|
|
47
|
+
description="A GUI-based visualizer for Arm CMN (Coherent Mesh Network)"
|
|
48
|
+
" systems using WindowsPerf backend"
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--version", action="version", version=f"{__version__}"
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--topology",
|
|
55
|
+
type=str,
|
|
56
|
+
default=Config.DEFAULT_TOPOLOGY_FILE,
|
|
57
|
+
help=f"Path to the topology JSON file (default: {Config.DEFAULT_TOPOLOGY_FILE})",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--telemetry",
|
|
61
|
+
type=str,
|
|
62
|
+
help="Path to the telemtry CSV file",
|
|
63
|
+
)
|
|
64
|
+
return parser.parse_args()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main() -> int:
|
|
68
|
+
args: argparse.Namespace = parse_args()
|
|
69
|
+
if windll:
|
|
70
|
+
windll.shcore.SetProcessDpiAwareness(1)
|
|
71
|
+
app: wperfCmnVisualizer = wperfCmnVisualizer(args)
|
|
72
|
+
return app.run()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
sys.exit(main())
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# BSD 3-Clause License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025, Arm Limited
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
# list of conditions and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
# and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
Zoomable and pannable canvas.
|
|
33
|
+
This will serve as the main body to which CMN objects are rendered.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import tkinter as tk
|
|
37
|
+
import tkinter.font as tkFont
|
|
38
|
+
import ttkbootstrap as tb
|
|
39
|
+
from functools import lru_cache
|
|
40
|
+
from typing import Callable, Optional, Tuple, Any
|
|
41
|
+
from .config import Config
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ZoomPanCanvas(tk.Canvas):
|
|
45
|
+
"""
|
|
46
|
+
A tkinter Canvas subclass that supports zooming and panning.
|
|
47
|
+
This canvas allows users to zoom in and out and pan across the content.
|
|
48
|
+
It maintains a zoom scale and offset to convert between world coordinates
|
|
49
|
+
and screen coordinates.
|
|
50
|
+
It also supports a hover callback for mouse movement in world coordinates.
|
|
51
|
+
Also a redraw callback automaticalled called when redraw required.
|
|
52
|
+
Attributes:
|
|
53
|
+
zoom_scale : float
|
|
54
|
+
The current zoom scale factor.
|
|
55
|
+
offset_x : float
|
|
56
|
+
Horizontal offset for panning.
|
|
57
|
+
offset_y : float
|
|
58
|
+
Vertical offset for panning.
|
|
59
|
+
hover_callback : Optional[Callable[[float, float], None]]
|
|
60
|
+
Optional callback for mouse hover events in world coordinates.
|
|
61
|
+
redraw_callback : Callable[[], None]
|
|
62
|
+
Callback to trigger canvas redraw; must take zero arguments.
|
|
63
|
+
"""
|
|
64
|
+
def __init__(self, master: tb.Window, redraw_callback: Callable[[], None], **kwargs: Any) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Args:
|
|
67
|
+
master : tb.Frame
|
|
68
|
+
The parent tkinter root
|
|
69
|
+
redraw_callback : Callable[[], None]
|
|
70
|
+
A callback function that is called whenever the canvas
|
|
71
|
+
needs to be redrawn, such as after panning or zooming.
|
|
72
|
+
This function must take no parameters.
|
|
73
|
+
kwargs : dict
|
|
74
|
+
Additional keyword arguments to be passed to the base tk.Canvas.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(master, **kwargs)
|
|
77
|
+
self.zoom_scale: float = Config.DEFAULT_ZOOM
|
|
78
|
+
self.offset_x: float = 0.0
|
|
79
|
+
self.offset_y: float = 0.0
|
|
80
|
+
self._pan_start_x: int = 0
|
|
81
|
+
self._pan_start_y: int = 0
|
|
82
|
+
self.hover_callback: Optional[Callable[[float, float], None]] = None
|
|
83
|
+
self.redraw_callback: Callable[[], None] = redraw_callback
|
|
84
|
+
self.font: tkFont.Font = tkFont.Font()
|
|
85
|
+
self._setup_bindings()
|
|
86
|
+
|
|
87
|
+
def get_dynamic_grid_cell_size(self) -> float:
|
|
88
|
+
"""Scale Current Grid Size."""
|
|
89
|
+
return Config.GRID_CELL_SIZE * (1.0 + (self.zoom_scale - 1.0) * 0.2)
|
|
90
|
+
|
|
91
|
+
def _setup_bindings(self) -> None:
|
|
92
|
+
"""Set up mouse and keyboard event bindings."""
|
|
93
|
+
self.bind("<ButtonPress-1>", self._start_pan)
|
|
94
|
+
self.bind("<B1-Motion>", self._do_pan)
|
|
95
|
+
self.bind("<MouseWheel>", self._do_zoom)
|
|
96
|
+
self.bind("<Configure>", self._on_resize)
|
|
97
|
+
self.bind("<Motion>", self._on_mouse_move)
|
|
98
|
+
self.bind_all("<Control-plus>", self._do_zoom)
|
|
99
|
+
self.bind_all("<Control-equal>", self._do_zoom)
|
|
100
|
+
self.bind_all("<Control-minus>", self._do_zoom)
|
|
101
|
+
|
|
102
|
+
def _start_pan(self, event: tk.Event) -> None:
|
|
103
|
+
"""Start panning operation."""
|
|
104
|
+
self._pan_start_x = event.x
|
|
105
|
+
self._pan_start_y = event.y
|
|
106
|
+
|
|
107
|
+
def _do_pan(self, event: tk.Event) -> None:
|
|
108
|
+
"""Handle panning motion."""
|
|
109
|
+
dx = event.x - self._pan_start_x
|
|
110
|
+
dy = event.y - self._pan_start_y
|
|
111
|
+
self.offset_x += dx
|
|
112
|
+
self.offset_y += dy
|
|
113
|
+
self._pan_start_x = event.x
|
|
114
|
+
self._pan_start_y = event.y
|
|
115
|
+
self.redraw_callback()
|
|
116
|
+
|
|
117
|
+
def _do_zoom(self, event: tk.Event) -> None:
|
|
118
|
+
"""Handle zoom operation from mouse wheel or keyboard shortcuts."""
|
|
119
|
+
if event.type == tk.EventType.MouseWheel:
|
|
120
|
+
if event.delta > 0 and self.zoom_scale >= Config.MAX_ZOOM:
|
|
121
|
+
return
|
|
122
|
+
if event.delta < 0 and self.zoom_scale <= Config.MIN_ZOOM:
|
|
123
|
+
return
|
|
124
|
+
factor = Config.ZOOM_FACTOR if event.delta > 0 else 1 / Config.ZOOM_FACTOR
|
|
125
|
+
mouse_x = self.canvasx(event.x)
|
|
126
|
+
mouse_y = self.canvasy(event.y)
|
|
127
|
+
|
|
128
|
+
elif event.type == tk.EventType.KeyPress:
|
|
129
|
+
key = event.keysym.lower()
|
|
130
|
+
if key in ("equal", "plus") and self.zoom_scale < Config.MAX_ZOOM:
|
|
131
|
+
factor = Config.ZOOM_FACTOR
|
|
132
|
+
elif key == "minus" and self.zoom_scale > Config.MIN_ZOOM:
|
|
133
|
+
factor = 1 / Config.ZOOM_FACTOR
|
|
134
|
+
else:
|
|
135
|
+
return
|
|
136
|
+
mouse_x = self.canvasx(self.winfo_width() / 2)
|
|
137
|
+
mouse_y = self.canvasy(self.winfo_height() / 2)
|
|
138
|
+
else:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
new_scale = max(Config.MIN_ZOOM, min(self.zoom_scale * factor, Config.MAX_ZOOM))
|
|
142
|
+
if new_scale != self.zoom_scale:
|
|
143
|
+
old_grid_size = self.get_dynamic_grid_cell_size()
|
|
144
|
+
old_world_x = (mouse_x - self.offset_x) / self.zoom_scale
|
|
145
|
+
old_world_y = (mouse_y - self.offset_y) / self.zoom_scale
|
|
146
|
+
self.zoom_scale = new_scale
|
|
147
|
+
new_grid_size = self.get_dynamic_grid_cell_size()
|
|
148
|
+
grid_scale_factor = new_grid_size / old_grid_size
|
|
149
|
+
new_world_x = old_world_x * grid_scale_factor
|
|
150
|
+
new_world_y = old_world_y * grid_scale_factor
|
|
151
|
+
self.offset_x = mouse_x - new_world_x * self.zoom_scale
|
|
152
|
+
self.offset_y = mouse_y - new_world_y * self.zoom_scale
|
|
153
|
+
|
|
154
|
+
self.redraw_callback()
|
|
155
|
+
|
|
156
|
+
def _on_resize(self, event: tk.Event) -> None:
|
|
157
|
+
"""Handle canvas resize."""
|
|
158
|
+
self.redraw_callback()
|
|
159
|
+
|
|
160
|
+
def _on_mouse_move(self, event: tk.Event) -> None:
|
|
161
|
+
"""Handle mouse movement for hover effects."""
|
|
162
|
+
if self.hover_callback:
|
|
163
|
+
wx = (event.x - self.offset_x) / self.zoom_scale
|
|
164
|
+
wy = (event.y - self.offset_y) / self.zoom_scale
|
|
165
|
+
self.hover_callback(wx, wy)
|
|
166
|
+
|
|
167
|
+
def world_to_screen(self, x: float, y: float) -> Tuple[float, float]:
|
|
168
|
+
"""Convert world coordinates to screen coordinates."""
|
|
169
|
+
return x * self.zoom_scale + self.offset_x, y * self.zoom_scale + self.offset_y
|
|
170
|
+
|
|
171
|
+
# Line thickness constants
|
|
172
|
+
MIN_THICKNESS = 1
|
|
173
|
+
MAX_THICKNESS = 3
|
|
174
|
+
|
|
175
|
+
def draw_line(
|
|
176
|
+
self,
|
|
177
|
+
x1: float, y1: float,
|
|
178
|
+
x2: float, y2: float,
|
|
179
|
+
color: str, thickness: float = 1.0,
|
|
180
|
+
scale_rate: float = 1.0,
|
|
181
|
+
tag: str = "") -> int:
|
|
182
|
+
"""Draw a line in world coordinates."""
|
|
183
|
+
|
|
184
|
+
sx1, sy1 = self.world_to_screen(x1, y1)
|
|
185
|
+
sx2, sy2 = self.world_to_screen(x2, y2)
|
|
186
|
+
effective_scale = self.zoom_scale ** scale_rate
|
|
187
|
+
base_thickness = thickness * effective_scale
|
|
188
|
+
scaled_thickness = max(self.MIN_THICKNESS, min(self.MAX_THICKNESS, base_thickness))
|
|
189
|
+
return self.create_line(
|
|
190
|
+
sx1, sy1, sx2, sy2,
|
|
191
|
+
fill=color,
|
|
192
|
+
width=scaled_thickness,
|
|
193
|
+
tags=tag
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def draw_rectangle(
|
|
197
|
+
self,
|
|
198
|
+
x: float, y: float,
|
|
199
|
+
width: float, height: float,
|
|
200
|
+
color: str, outline_color: str = "black",
|
|
201
|
+
outline_thickness=1.0,
|
|
202
|
+
scale_rate: float = 1.0,
|
|
203
|
+
tag: str = "") -> int:
|
|
204
|
+
"""Draw a filled rectangle in world coordinates."""
|
|
205
|
+
|
|
206
|
+
sx, sy = self.world_to_screen(x, y)
|
|
207
|
+
effective_scale = self.zoom_scale ** scale_rate
|
|
208
|
+
scaled_width = width * effective_scale
|
|
209
|
+
scaled_height = height * effective_scale
|
|
210
|
+
base_thickness = outline_thickness * effective_scale
|
|
211
|
+
scaled_thickness = max(self.MIN_THICKNESS, min(self.MAX_THICKNESS, base_thickness))
|
|
212
|
+
|
|
213
|
+
sx1 = sx - scaled_width / 2
|
|
214
|
+
sy1 = sy - scaled_height / 2
|
|
215
|
+
sx2 = sx + scaled_width / 2
|
|
216
|
+
sy2 = sy + scaled_height / 2
|
|
217
|
+
|
|
218
|
+
return self.create_rectangle(
|
|
219
|
+
sx1, sy1, sx2, sy2,
|
|
220
|
+
fill=color, outline=outline_color,
|
|
221
|
+
width=scaled_thickness,
|
|
222
|
+
tags=tag
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def draw_outline_rectangle(
|
|
226
|
+
self,
|
|
227
|
+
x: float, y: float,
|
|
228
|
+
width: float, height: float,
|
|
229
|
+
outline_color: str, thickness: float = 1.0,
|
|
230
|
+
scale_rate: float = 1.0,
|
|
231
|
+
tag: str = "") -> int:
|
|
232
|
+
"""Draw an outline rectangle in world coordinates."""
|
|
233
|
+
|
|
234
|
+
sx, sy = self.world_to_screen(x, y)
|
|
235
|
+
effective_scale = self.zoom_scale ** scale_rate
|
|
236
|
+
scaled_width = width * effective_scale
|
|
237
|
+
scaled_height = height * effective_scale
|
|
238
|
+
base_thickness = thickness * effective_scale
|
|
239
|
+
scaled_thickness = max(self.MIN_THICKNESS, min(self.MAX_THICKNESS, base_thickness))
|
|
240
|
+
|
|
241
|
+
sx1 = sx - scaled_width / 2
|
|
242
|
+
sy1 = sy - scaled_height / 2
|
|
243
|
+
sx2 = sx + scaled_width / 2
|
|
244
|
+
sy2 = sy + scaled_height / 2
|
|
245
|
+
|
|
246
|
+
return self.create_rectangle(
|
|
247
|
+
sx1, sy1, sx2, sy2,
|
|
248
|
+
fill="", outline=outline_color,
|
|
249
|
+
width=scaled_thickness,
|
|
250
|
+
tags=tag
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def draw_text(
|
|
254
|
+
self,
|
|
255
|
+
x: float, y: float,
|
|
256
|
+
text: str,
|
|
257
|
+
color: str = "white", font_size: int = 12,
|
|
258
|
+
angle: float = 0,
|
|
259
|
+
scale_rate: float = 1.0,
|
|
260
|
+
tag: str = "") -> int:
|
|
261
|
+
"""Draw text in world coordinates."""
|
|
262
|
+
|
|
263
|
+
sx, sy = self.world_to_screen(x, y)
|
|
264
|
+
effective_scale = self.zoom_scale ** scale_rate
|
|
265
|
+
scaled_font_size = max(1, int(font_size * effective_scale))
|
|
266
|
+
return self.create_text(
|
|
267
|
+
sx, sy, text=text,
|
|
268
|
+
fill=color, font=("", scaled_font_size),
|
|
269
|
+
angle=angle,
|
|
270
|
+
tags=tag
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
@lru_cache(maxsize=1024)
|
|
275
|
+
def _get_font(scaled_size: int) -> tkFont.Font:
|
|
276
|
+
"""Cache Font objects"""
|
|
277
|
+
return tkFont.Font(size=scaled_size)
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
@lru_cache(maxsize=4096)
|
|
281
|
+
def _measure_text_width_scaled(text: str, scaled_size: int) -> float:
|
|
282
|
+
"""Cache measuremeants"""
|
|
283
|
+
f = ZoomPanCanvas._get_font(scaled_size)
|
|
284
|
+
return f.measure(text)
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
@lru_cache(maxsize=1024)
|
|
288
|
+
def _measure_text_height_scaled(scaled_size: int) -> float:
|
|
289
|
+
"""Actual cached measurement with scaled font size"""
|
|
290
|
+
f = ZoomPanCanvas._get_font(scaled_size)
|
|
291
|
+
return f.metrics("linespace")
|
|
292
|
+
|
|
293
|
+
def get_text_width(self, text: str, font_size: int, scale_rate: float = 1.0) -> float:
|
|
294
|
+
zoom_scale = self.zoom_scale
|
|
295
|
+
effective_scale = zoom_scale ** scale_rate
|
|
296
|
+
scaled_size = max(1, int(font_size * effective_scale))
|
|
297
|
+
width_px = ZoomPanCanvas._measure_text_width_scaled(text, scaled_size)
|
|
298
|
+
return width_px / effective_scale
|
|
299
|
+
|
|
300
|
+
def get_text_height(self, font_size: int, scale_rate: float = 1.0) -> float:
|
|
301
|
+
zoom_scale = self.zoom_scale
|
|
302
|
+
effective_scale = zoom_scale ** scale_rate
|
|
303
|
+
scaled_size = max(1, int(font_size * effective_scale))
|
|
304
|
+
height_px = ZoomPanCanvas._measure_text_height_scaled(scaled_size)
|
|
305
|
+
return height_px / effective_scale
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# BSD 3-Clause License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025, Arm Limited
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
# list of conditions and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
# and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
CMN mesh data structures and operations.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
from numpy import dtype as np_dtype
|
|
37
|
+
from typing import Dict, Any, List
|
|
38
|
+
|
|
39
|
+
CMN_MAX_PORTS: int = 6
|
|
40
|
+
CMN_MAX_CHILDS: int = 32
|
|
41
|
+
CMN_MAX_MESH_WIDTH: int = 12
|
|
42
|
+
CMN_MAX_MESH_HEIGHT: int = 12
|
|
43
|
+
CMN_TYPE_STR_MAX_LEN: int = 128
|
|
44
|
+
CMN_XP_DEVICE_ID: int = 6
|
|
45
|
+
|
|
46
|
+
coord_dtype = np_dtype(
|
|
47
|
+
[
|
|
48
|
+
("x", "u1"),
|
|
49
|
+
("y", "u1"),
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
node_dtype = np_dtype(
|
|
54
|
+
[
|
|
55
|
+
("coord", coord_dtype),
|
|
56
|
+
("nodeid", "u2"),
|
|
57
|
+
("logicalid", "u2"),
|
|
58
|
+
("type", "u2"),
|
|
59
|
+
("type_str", "U128"),
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
port_dtype = np_dtype(
|
|
64
|
+
[
|
|
65
|
+
("type", "u1"),
|
|
66
|
+
("type_str", "U128"),
|
|
67
|
+
("cal", "b1"),
|
|
68
|
+
("num_devices", "u1"),
|
|
69
|
+
("devices", node_dtype, CMN_MAX_CHILDS),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
xp_dtype = np_dtype(
|
|
74
|
+
[
|
|
75
|
+
("node_info", node_dtype),
|
|
76
|
+
("dtc_domain", "u1"),
|
|
77
|
+
("num_device_ports", "u1"),
|
|
78
|
+
("ports", port_dtype, CMN_MAX_PORTS),
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
mesh_dtype = np_dtype(
|
|
83
|
+
[
|
|
84
|
+
("x_dim", "u1"),
|
|
85
|
+
("y_dim", "u1"),
|
|
86
|
+
("xps", xp_dtype, (CMN_MAX_MESH_HEIGHT, CMN_MAX_MESH_WIDTH)),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CMN:
|
|
92
|
+
"""
|
|
93
|
+
Top-level CMN topology container managing multiple meshes.
|
|
94
|
+
Attributes:
|
|
95
|
+
num_meshes (int): Number of meshes in the topology
|
|
96
|
+
meshes (np.ndarray): Array of mesh structures.
|
|
97
|
+
"""
|
|
98
|
+
def __init__(self, topology: Dict[str, Any]) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Initialise CMN topology from JSON configuration.
|
|
101
|
+
Args:
|
|
102
|
+
topology (Dict[str, Any]): JSON configuration
|
|
103
|
+
Raises:
|
|
104
|
+
AssertionError: On invalid input
|
|
105
|
+
"""
|
|
106
|
+
assert "elements" in topology, "topology data does not contain elements information"
|
|
107
|
+
self.num_meshes: int = len(topology["elements"])
|
|
108
|
+
assert self.num_meshes > 0, f"topology data contained {self.num_meshes} meshes"
|
|
109
|
+
self.meshes = np.zeros(self.num_meshes, dtype=mesh_dtype)
|
|
110
|
+
|
|
111
|
+
for i, element in enumerate(topology["elements"]):
|
|
112
|
+
assert "config" in element, f"element idx: {i} does not contain configuration data"
|
|
113
|
+
self._init_mesh(self.meshes[i], element["config"])
|
|
114
|
+
|
|
115
|
+
def _init_mesh(self, mesh: np.ndarray, config: Dict[str, Any]) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Initialise a single mesh element.
|
|
118
|
+
Args:
|
|
119
|
+
mesh (np.ndarray): The structured array element to populate
|
|
120
|
+
config (Dict[str, Any]): Configuration data for the mesh
|
|
121
|
+
"""
|
|
122
|
+
x_val = config.get("X", 0)
|
|
123
|
+
y_val = config.get("Y", 0)
|
|
124
|
+
|
|
125
|
+
x_dim = int(x_val)
|
|
126
|
+
y_dim = int(y_val)
|
|
127
|
+
|
|
128
|
+
assert 0 < x_dim <= CMN_MAX_MESH_WIDTH, f"invalid x dimension on mesh, got: {x_dim}"
|
|
129
|
+
assert 0 < y_dim <= CMN_MAX_MESH_HEIGHT, f"invalid y dimension on mesh, got: {y_dim}"
|
|
130
|
+
|
|
131
|
+
mesh["x_dim"] = x_dim
|
|
132
|
+
mesh["y_dim"] = y_dim
|
|
133
|
+
|
|
134
|
+
assert "xps" in config, "no XP information on mesh"
|
|
135
|
+
assert len(config["xps"]) > 0, "empty XP information on mesh"
|
|
136
|
+
|
|
137
|
+
self._load_data(mesh, config["xps"], x_dim, y_dim)
|
|
138
|
+
|
|
139
|
+
def _load_data(
|
|
140
|
+
self,
|
|
141
|
+
mesh: np.ndarray,
|
|
142
|
+
xps_data: List[Dict[str, Any]],
|
|
143
|
+
x_dim: int, y_dim: int
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Load XP data into the mesh structure.
|
|
147
|
+
Args:
|
|
148
|
+
mesh (np.ndarray): Mesh structure
|
|
149
|
+
xps_data (List[Dict[str, Any]]): List of XP data
|
|
150
|
+
x_dim (int): Width of mesh
|
|
151
|
+
y_dim (int): Height of mesh
|
|
152
|
+
"""
|
|
153
|
+
for xp in xps_data:
|
|
154
|
+
x = int(xp.get("X", 0))
|
|
155
|
+
y = int(xp.get("Y", 0))
|
|
156
|
+
|
|
157
|
+
if not (0 <= x < x_dim and 0 <= y < y_dim):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
node_xp = mesh["xps"][y, x]
|
|
161
|
+
|
|
162
|
+
node_xp["node_info"]["coord"]["x"] = x
|
|
163
|
+
node_xp["node_info"]["coord"]["y"] = y
|
|
164
|
+
node_xp["node_info"]["nodeid"] = xp.get("id", 0)
|
|
165
|
+
node_xp["node_info"]["logicalid"] = xp.get("logical_id", 0)
|
|
166
|
+
node_xp["node_info"]["type"] = CMN_XP_DEVICE_ID
|
|
167
|
+
node_xp["node_info"]["type_str"] = "XP"
|
|
168
|
+
|
|
169
|
+
node_xp["dtc_domain"] = xp.get("dtc", 0)
|
|
170
|
+
node_xp["num_device_ports"] = xp.get("n_ports", 0)
|
|
171
|
+
|
|
172
|
+
for port in xp.get("ports", [])[:CMN_MAX_PORTS]:
|
|
173
|
+
if "port" not in port or not (0 <= port["port"] < CMN_MAX_PORTS):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
po = node_xp["ports"][port["port"]]
|
|
177
|
+
po["type"] = port.get("type", 0)
|
|
178
|
+
po["type_str"] = port.get("type_s", "unknown")[:CMN_TYPE_STR_MAX_LEN]
|
|
179
|
+
po["cal"] = port.get("cal", False)
|
|
180
|
+
|
|
181
|
+
devices_list = port.get("devices", [])[:CMN_MAX_CHILDS]
|
|
182
|
+
po["num_devices"] = len(devices_list)
|
|
183
|
+
|
|
184
|
+
for d, device in enumerate(devices_list):
|
|
185
|
+
dev = po["devices"][d]
|
|
186
|
+
dev["coord"]["x"] = x
|
|
187
|
+
dev["coord"]["y"] = y
|
|
188
|
+
dev["nodeid"] = device.get("id", 0)
|
|
189
|
+
dev["logicalid"] = device.get("logical_id", 0)
|
|
190
|
+
dev["type"] = device.get("type", 0)
|
|
191
|
+
dev["type_str"] = device.get("type_s", "unknown")[:CMN_TYPE_STR_MAX_LEN]
|
|
192
|
+
|
|
193
|
+
def get_view(
|
|
194
|
+
self,
|
|
195
|
+
mesh_idx: int,
|
|
196
|
+
min_row: int, max_row: int,
|
|
197
|
+
min_col: int, max_col: int
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Get a view of the mesh array for the visible region (NumPy array slicing).
|
|
201
|
+
Args:
|
|
202
|
+
mesh_idx (int): Mesh index
|
|
203
|
+
min_row (int): Start row (inclusive)
|
|
204
|
+
max_row (int): End row (inclusive)
|
|
205
|
+
min_col (int): Start col (inclusive)
|
|
206
|
+
max_col (int): End col (inclusive)
|
|
207
|
+
Returns:
|
|
208
|
+
NDArray[np.ndarray]: Sliced 2D view of the mesh XP array
|
|
209
|
+
"""
|
|
210
|
+
mesh = self.meshes[mesh_idx]
|
|
211
|
+
x_dim = mesh["x_dim"]
|
|
212
|
+
y_dim = mesh["y_dim"]
|
|
213
|
+
|
|
214
|
+
min_row = max(0, min_row)
|
|
215
|
+
max_row = min(y_dim, max_row + 1)
|
|
216
|
+
min_col = max(0, min_col)
|
|
217
|
+
max_col = min(x_dim, max_col + 1)
|
|
218
|
+
|
|
219
|
+
return mesh["xps"][min_row:max_row, min_col:max_col]
|