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.
@@ -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]