siliconcompiler 0.34.2__py3-none-any.whl → 0.34.3__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.
Files changed (121) hide show
  1. siliconcompiler/__init__.py +12 -5
  2. siliconcompiler/__main__.py +1 -7
  3. siliconcompiler/_metadata.py +1 -1
  4. siliconcompiler/apps/_common.py +104 -23
  5. siliconcompiler/apps/sc.py +4 -8
  6. siliconcompiler/apps/sc_dashboard.py +6 -4
  7. siliconcompiler/apps/sc_install.py +10 -6
  8. siliconcompiler/apps/sc_issue.py +7 -5
  9. siliconcompiler/apps/sc_remote.py +1 -1
  10. siliconcompiler/apps/sc_server.py +9 -14
  11. siliconcompiler/apps/sc_show.py +6 -5
  12. siliconcompiler/apps/smake.py +130 -94
  13. siliconcompiler/apps/utils/replay.py +4 -7
  14. siliconcompiler/apps/utils/summarize.py +3 -5
  15. siliconcompiler/asic.py +420 -0
  16. siliconcompiler/checklist.py +25 -2
  17. siliconcompiler/cmdlineschema.py +534 -0
  18. siliconcompiler/constraints/asic_component.py +2 -2
  19. siliconcompiler/constraints/asic_pins.py +2 -2
  20. siliconcompiler/constraints/asic_timing.py +3 -3
  21. siliconcompiler/core.py +7 -32
  22. siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
  23. siliconcompiler/dependencyschema.py +89 -31
  24. siliconcompiler/design.py +176 -207
  25. siliconcompiler/filesetschema.py +250 -0
  26. siliconcompiler/flowgraph.py +274 -95
  27. siliconcompiler/fpga.py +124 -1
  28. siliconcompiler/library.py +218 -20
  29. siliconcompiler/metric.py +233 -20
  30. siliconcompiler/package/__init__.py +271 -50
  31. siliconcompiler/package/git.py +92 -16
  32. siliconcompiler/package/github.py +108 -12
  33. siliconcompiler/package/https.py +79 -16
  34. siliconcompiler/packageschema.py +88 -7
  35. siliconcompiler/pathschema.py +31 -2
  36. siliconcompiler/pdk.py +566 -1
  37. siliconcompiler/project.py +1095 -94
  38. siliconcompiler/record.py +38 -1
  39. siliconcompiler/remote/__init__.py +5 -2
  40. siliconcompiler/remote/client.py +11 -6
  41. siliconcompiler/remote/schema.py +5 -23
  42. siliconcompiler/remote/server.py +41 -54
  43. siliconcompiler/report/__init__.py +3 -3
  44. siliconcompiler/report/dashboard/__init__.py +48 -14
  45. siliconcompiler/report/dashboard/cli/__init__.py +99 -21
  46. siliconcompiler/report/dashboard/cli/board.py +364 -179
  47. siliconcompiler/report/dashboard/web/__init__.py +90 -12
  48. siliconcompiler/report/dashboard/web/components/__init__.py +219 -240
  49. siliconcompiler/report/dashboard/web/components/flowgraph.py +49 -26
  50. siliconcompiler/report/dashboard/web/components/graph.py +139 -100
  51. siliconcompiler/report/dashboard/web/layouts/__init__.py +29 -1
  52. siliconcompiler/report/dashboard/web/layouts/_common.py +38 -2
  53. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph.py +39 -26
  54. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_node_tab.py +50 -50
  55. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_sac_tabs.py +49 -46
  56. siliconcompiler/report/dashboard/web/state.py +141 -14
  57. siliconcompiler/report/dashboard/web/utils/__init__.py +79 -16
  58. siliconcompiler/report/dashboard/web/utils/file_utils.py +74 -11
  59. siliconcompiler/report/dashboard/web/viewer.py +25 -1
  60. siliconcompiler/report/report.py +5 -2
  61. siliconcompiler/report/summary_image.py +29 -11
  62. siliconcompiler/scheduler/__init__.py +9 -1
  63. siliconcompiler/scheduler/docker.py +79 -1
  64. siliconcompiler/scheduler/run_node.py +35 -19
  65. siliconcompiler/scheduler/scheduler.py +208 -24
  66. siliconcompiler/scheduler/schedulernode.py +372 -46
  67. siliconcompiler/scheduler/send_messages.py +77 -29
  68. siliconcompiler/scheduler/slurm.py +76 -12
  69. siliconcompiler/scheduler/taskscheduler.py +140 -20
  70. siliconcompiler/schema/__init__.py +0 -2
  71. siliconcompiler/schema/baseschema.py +194 -38
  72. siliconcompiler/schema/journal.py +7 -4
  73. siliconcompiler/schema/namedschema.py +16 -10
  74. siliconcompiler/schema/parameter.py +55 -9
  75. siliconcompiler/schema/parametervalue.py +60 -0
  76. siliconcompiler/schema/safeschema.py +25 -2
  77. siliconcompiler/schema/schema_cfg.py +5 -5
  78. siliconcompiler/schema/utils.py +2 -2
  79. siliconcompiler/schema_obj.py +20 -3
  80. siliconcompiler/tool.py +979 -302
  81. siliconcompiler/tools/bambu/__init__.py +41 -0
  82. siliconcompiler/tools/builtin/concatenate.py +2 -2
  83. siliconcompiler/tools/builtin/minimum.py +2 -1
  84. siliconcompiler/tools/builtin/mux.py +2 -1
  85. siliconcompiler/tools/builtin/nop.py +2 -1
  86. siliconcompiler/tools/builtin/verify.py +2 -1
  87. siliconcompiler/tools/klayout/__init__.py +95 -0
  88. siliconcompiler/tools/openroad/__init__.py +289 -0
  89. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +3 -0
  90. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +7 -2
  91. siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +8 -4
  92. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +9 -5
  93. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +5 -1
  94. siliconcompiler/tools/slang/__init__.py +1 -1
  95. siliconcompiler/tools/slang/elaborate.py +2 -1
  96. siliconcompiler/tools/vivado/scripts/sc_run.tcl +1 -1
  97. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +8 -1
  98. siliconcompiler/tools/vivado/syn_fpga.py +6 -0
  99. siliconcompiler/tools/vivado/vivado.py +35 -2
  100. siliconcompiler/tools/vpr/__init__.py +150 -0
  101. siliconcompiler/tools/yosys/__init__.py +369 -1
  102. siliconcompiler/tools/yosys/scripts/procs.tcl +0 -1
  103. siliconcompiler/toolscripts/_tools.json +5 -10
  104. siliconcompiler/utils/__init__.py +66 -0
  105. siliconcompiler/utils/flowgraph.py +2 -2
  106. siliconcompiler/utils/issue.py +2 -1
  107. siliconcompiler/utils/logging.py +14 -0
  108. siliconcompiler/utils/multiprocessing.py +256 -0
  109. siliconcompiler/utils/showtools.py +10 -0
  110. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +5 -5
  111. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +115 -118
  112. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
  113. siliconcompiler/schema/cmdlineschema.py +0 -250
  114. siliconcompiler/toolscripts/rhel8/install-slang.sh +0 -40
  115. siliconcompiler/toolscripts/rhel9/install-slang.sh +0 -40
  116. siliconcompiler/toolscripts/ubuntu20/install-slang.sh +0 -47
  117. siliconcompiler/toolscripts/ubuntu22/install-slang.sh +0 -37
  118. siliconcompiler/toolscripts/ubuntu24/install-slang.sh +0 -37
  119. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
  120. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
  121. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
- import os
2
- import threading
3
1
  import logging
2
+ import os
3
+ import math
4
4
  import queue
5
5
  import time
6
- import multiprocessing
6
+ import threading
7
7
 
8
8
  from collections import deque
9
9
  from dataclasses import dataclass, field
@@ -22,27 +22,116 @@ from siliconcompiler import SiliconCompilerError, NodeStatus
22
22
  from siliconcompiler.utils.logging import SCColorLoggerFormatter
23
23
  from siliconcompiler.flowgraph import RuntimeFlowgraph
24
24
 
25
- import atexit
25
+
26
+ class LogBuffer:
27
+ """
28
+ A buffer for storing log messages, designed to be thread-safe and to work
29
+ in conjunction with a `LogBufferHandler` for dashboard or UI display.
30
+ It uses a `collections.deque` to maintain a fixed-size history of log lines.
31
+ """
32
+ def __init__(self, queue: queue.Queue, n: int = 15, event: threading.Event = None):
33
+ """
34
+ Initializes the LogBuffer.
35
+
36
+ Args:
37
+ queue (queue.Queue): A thread-safe queue to push new log lines to.
38
+ n (int): The maximum number of log lines to retain in the buffer's history.
39
+ Defaults to 15.
40
+ event (threading.Event, optional): An optional `threading.Event` object.
41
+ If provided, this event is set whenever
42
+ a new log line is added, signaling
43
+ consumers that new data is available.
44
+ Defaults to None.
45
+ """
46
+ self.queue = queue
47
+ self.buffer = deque(maxlen=n)
48
+ self.lock = threading.Lock()
49
+ if not event:
50
+ # Create dummy event
51
+ event = threading.Event()
52
+ self.event = event
53
+
54
+ def make_handler(self) -> logging.Handler:
55
+ """
56
+ Creates and returns a `LogBufferHandler` instance associated with this `LogBuffer`.
57
+
58
+ This handler can then be added to a Python logger to direct log messages
59
+ to this buffer.
60
+
61
+ Returns:
62
+ logging.Handler: An instance of `LogBufferHandler`.
63
+ """
64
+ return LogBufferHandler(self)
65
+
66
+ def add_line(self, line: str):
67
+ """
68
+ Adds a new log line to the internal queue and signals the event.
69
+
70
+ This method is called by the `LogBufferHandler` (or directly) to
71
+ append a processed log line to the buffer. It also sets the internal
72
+ threading event to notify any waiting consumers.
73
+
74
+ Args:
75
+ line (str): The log line (string) to add.
76
+ """
77
+ self.queue.put(line)
78
+ self.event.set()
79
+
80
+ def get_lines(self, lines: int = None) -> List[str]:
81
+ """
82
+ Retrieves the last logged lines from the buffer.
83
+
84
+ New lines are first moved from the internal queue to the buffer,
85
+ and then the requested number of lines are returned from the buffer's history.
86
+
87
+ Args:
88
+ lines (int, optional): The maximum number of recent lines to retrieve.
89
+ If None, all lines currently in the buffer are returned.
90
+ Defaults to None.
91
+
92
+ Returns:
93
+ list[str]: A list of the last logged lines.
94
+ """
95
+ new_lines = []
96
+ try:
97
+ for _ in range(self.queue.qsize()):
98
+ new_lines.append(self.queue.get_nowait())
99
+ except queue.Empty:
100
+ pass
101
+ if not self.queue.empty():
102
+ # Set event since queue is not empty
103
+ self.event.set()
104
+
105
+ with self.lock:
106
+ self.buffer.extend(new_lines)
107
+ buffer_list = list(self.buffer)
108
+
109
+ if lines is None or lines > len(buffer_list):
110
+ return buffer_list
111
+ return buffer_list[-lines:]
26
112
 
27
113
 
28
114
  class LogBufferHandler(logging.Handler):
29
- def __init__(self, sync_queue, n=50, event=None):
115
+ """
116
+ A custom logging handler that buffers log records and processes them
117
+ for display in a dashboard or other UI, replacing console color codes
118
+ with a simplified markdown-like format.
119
+ """
120
+ def __init__(self, parent: LogBuffer):
30
121
  """
31
- Initializes the handler.
122
+ Initializes the LogBufferHandler.
32
123
 
33
124
  Args:
34
- n (int): Maximum number of lines to keep.
35
- event (threading.Event): Optional event to trigger on every log line.
125
+ parent (LogBuffer): The parent `LogBuffer` instance to which processed
126
+ log lines will be added.
36
127
  """
37
128
  super().__init__()
38
- self.queue = sync_queue
39
- self.buffer = deque(maxlen=n)
40
- self.event = event
41
- self._lock = threading.Lock()
129
+ self._parent = parent
42
130
 
43
131
  def emit(self, record):
44
132
  """
45
- Processes a log record.
133
+ Processes a log record, formats it, replaces console color codes,
134
+ and adds the transformed line to the parent `LogBuffer`.
46
135
 
47
136
  Args:
48
137
  record (logging.LogRecord): The log record to process.
@@ -58,31 +147,27 @@ class LogBufferHandler(logging.Handler):
58
147
  (SCColorLoggerFormatter.red.replace("[", "\\["), "[red]"),
59
148
  (SCColorLoggerFormatter.bold_red.replace("[", "\\["), "[bold red]")):
60
149
  log_entry = log_entry.replace(color, replacement)
61
- self.queue.put(log_entry)
62
- if self.event:
63
- self.event.set()
64
-
65
- def get_lines(self, lines=None):
66
- """
67
- Retrieves the last logged lines.
68
-
69
- Returns:
70
- list: A list of the last logged lines.
71
- """
72
- with self._lock:
73
- while not self.queue.empty():
74
- try:
75
- self.buffer.append(self.queue.get_nowait())
76
- except queue.Empty:
77
- break
78
- buffer_list = list(self.buffer)
79
- if lines is None or lines > len(buffer_list):
80
- return buffer_list
81
- return buffer_list[-lines:]
150
+ self._parent.add_line(log_entry)
82
151
 
83
152
 
84
153
  @dataclass
85
154
  class JobData:
155
+ """
156
+ A data class to hold information about a single job's progress and status.
157
+
158
+ Attributes:
159
+ total (int): The total number of nodes in the job.
160
+ success (int): The number of successfully completed nodes.
161
+ error (int): The number of nodes that resulted in an error.
162
+ skipped (int): The number of skipped nodes.
163
+ finished (int): The total number of finished nodes (success or error).
164
+ jobname (str): The name of the job.
165
+ design (str): The name of the design associated with the job.
166
+ runtime (float): The total runtime of the job so far.
167
+ complete (bool): A flag indicating if the job has completed.
168
+ nodes (List[dict]): A list of dictionaries, each containing detailed
169
+ information about a single node in the flowgraph.
170
+ """
86
171
  total: int = 0
87
172
  success: int = 0
88
173
  error: int = 0
@@ -97,6 +182,20 @@ class JobData:
97
182
 
98
183
  @dataclass
99
184
  class SessionData:
185
+ """
186
+ A data class to hold aggregated information about the entire run session,
187
+ which may include multiple jobs.
188
+
189
+ Attributes:
190
+ total (int): The total number of nodes across all jobs.
191
+ success (int): The total number of successfully completed nodes across all jobs.
192
+ error (int): The total number of nodes that resulted in an error across all jobs.
193
+ skipped (int): The total number of skipped nodes across all jobs.
194
+ finished (int): The total number of finished nodes across all jobs.
195
+ runtime (float): The maximum runtime among all jobs.
196
+ jobs (Dict[str, JobData]): A dictionary mapping job identifiers to their
197
+ corresponding JobData objects.
198
+ """
100
199
  total: int = 0
101
200
  success: int = 0
102
201
  error: int = 0
@@ -109,69 +208,85 @@ class SessionData:
109
208
  @dataclass
110
209
  class Layout:
111
210
  """
112
- Layout class represents the configuration for a dashboard layout.
211
+ Manages the dynamic layout of the dashboard, calculating the height
212
+ of different sections based on terminal size and content.
113
213
 
114
214
  Attributes:
115
- height (int): The total height of the layout.
116
- width (int): The total width of the layout.
117
- job_board_min (int): The minimum height allocated for the job board.
118
- job_board_max (int): The maximum height allocated for the job board.
119
- log_max (int): The maximum height allocated for the log section.
120
- log_min (int): The minimum height allocated for the log section.
121
- progress_bar_min (int): The minimum height allocated for the progress bar.
122
- progress_bar_max (int): The maximum height allocated for the progress bar.
123
- job_board_show_log (bool): A flag indicating whether to show the log in the job board.
124
-
125
- __reserved (int): Reserved space for table headings and extra padding.
126
-
127
- Methods:
128
- available_height():
129
- Calculates and returns the available height for other components in the layout
130
- after accounting for reserved space, job board, and log sections.
131
- Returns 0 if the total height is not set.
215
+ height (int): The total height of the terminal.
216
+ width (int): The total width of the terminal.
217
+ log_height (int): The calculated height for the log display area.
218
+ job_board_height (int): The calculated height for the job status board.
219
+ progress_bar_height (int): The calculated height for the progress bar section.
220
+ job_board_show_log (bool): Flag to determine if the log file column is shown.
221
+ job_board_v_limit (int): Width threshold to switch to a more compact view.
132
222
  """
133
223
 
134
224
  height: int = 0
135
225
  width: int = 0
136
226
 
137
- log_height = 0
138
- job_board_height = 0
139
- progress_bar_height = 0
227
+ log_height: int = 0
228
+ job_board_height: int = 0
229
+ progress_bar_height: int = 0
140
230
 
141
231
  job_board_show_log: bool = True
142
232
  job_board_v_limit: int = 120
143
233
 
144
234
  __progress_bar_height_default = 1
145
235
  padding_log = 2
146
- padding_progress_bar = 1
147
- padding_job_board = 1
148
- padding_job_board_header = 1
236
+ padding_progress_bar: int = 1
237
+ padding_job_board: int = 1
238
+ padding_job_board_header: int = 1
149
239
 
150
- def update(self, height, width, visible_jobs, visible_bars):
240
+ def update(self, height: int, width: int, visible_jobs: int, visible_bars: int):
241
+ """
242
+ Recalculates the layout dimensions based on the current terminal size and content.
243
+
244
+ This method implements the logic to intelligently allocate vertical space
245
+ to the progress bars, job board, and log view.
246
+
247
+ Args:
248
+ height (int): The current terminal height.
249
+ width (int): The current terminal width.
250
+ visible_jobs (int): The number of job nodes to be displayed.
251
+ visible_bars (int): The number of progress bars to be displayed.
252
+ """
151
253
  self.height = height
152
254
  self.width = width
153
255
 
154
- min_required = (
155
- max(visible_bars, self.__progress_bar_height_default)
156
- + self.padding_progress_bar
157
- )
158
- if self.height < min_required:
159
- self.progress_bar_height = 0
256
+ if self.height < 3:
257
+ self.progress_bar_height = self.height - self.padding_progress_bar - 1
160
258
  self.job_board_height = 0
161
259
  self.log_height = 0
162
- return
260
+
261
+ # target sizes
262
+ target_jobs = 0.25 * self.height
263
+ target_bars = 0.50 * self.height
264
+ # 25 % for log
265
+
266
+ # Adjust targets based on progress bars
267
+ if visible_bars < target_bars:
268
+ remainder = target_bars - visible_bars
269
+ target_bars = visible_bars
270
+ target_jobs += 0.75 * remainder
271
+ target_bars = int(math.ceil(target_bars))
272
+
273
+ # Adjust targets based on jobs
274
+ if visible_jobs < target_jobs:
275
+ target_jobs = visible_jobs
276
+ target_jobs = int(math.ceil(target_jobs))
163
277
 
164
278
  remaining_height = self.height
165
279
 
166
280
  # Allocate progress bar space (highest priority)
167
- self.progress_bar_height = max(visible_bars, self.__progress_bar_height_default)
281
+ self.progress_bar_height = max(min(target_bars, visible_bars),
282
+ self.__progress_bar_height_default)
168
283
  if self.progress_bar_height > 0:
169
284
  remaining_height -= self.progress_bar_height + self.padding_progress_bar
170
285
 
171
286
  # Calculate job board requirements
172
287
  job_board_min_space = self.padding_job_board_header + self.padding_job_board
173
288
  job_board_max_nodes = remaining_height // 2
174
- visible_jobs = min(visible_jobs, job_board_max_nodes)
289
+ visible_jobs = min(min(target_jobs, visible_jobs), job_board_max_nodes)
175
290
  if visible_jobs > 0:
176
291
  job_board_full_space = visible_jobs + job_board_min_space
177
292
  else:
@@ -190,24 +305,21 @@ class Layout:
190
305
  else:
191
306
  self.job_board_height = visible_jobs
192
307
  self.log_height = remaining_height - job_board_full_space - self.padding_log
308
+ if self.log_height < 0:
309
+ self.log_height = 0
193
310
 
194
311
  if self.width < self.job_board_v_limit:
195
312
  self.job_board_show_log = False
196
313
 
197
314
 
198
- class BoardSingleton(type):
199
- _instances = {}
200
- _lock = multiprocessing.Lock()
201
-
202
- def __call__(cls, *args, **kwargs):
203
- with cls._lock:
204
- if cls not in cls._instances:
205
- cls._instances[cls] = super(BoardSingleton, cls).__call__(*args, **kwargs)
206
- cls._instances[cls]._init_singleton()
207
- return cls._instances[cls]
208
-
315
+ class Board:
316
+ """
317
+ The main class for rendering the live dashboard UI.
209
318
 
210
- class Board(metaclass=BoardSingleton):
319
+ This class orchestrates the display of job progress, logs, and status updates
320
+ in the terminal using the `rich` library. It runs in a separate thread to
321
+ provide a non-blocking UI.
322
+ """
211
323
  __status_color_map = {
212
324
  NodeStatus.PENDING: "blue",
213
325
  NodeStatus.QUEUED: "blue",
@@ -243,20 +355,22 @@ class Board(metaclass=BoardSingleton):
243
355
 
244
356
  __JOB_BOARD_BOX = box.SIMPLE_HEAD
245
357
 
246
- def __init__(self):
247
- pass
358
+ def __init__(self, manager):
359
+ """
360
+ Initializes the Board.
248
361
 
249
- def _init_singleton(self):
362
+ Args:
363
+ manager: A multiprocessing.Manager object to create shared state
364
+ (events, dicts, locks) between processes.
365
+ """
250
366
  self._console = Console(theme=Board.__theme)
251
367
 
252
368
  self.live = Live(
253
369
  console=self._console,
254
- screen=False,
255
- auto_refresh=True,
370
+ screen=True,
371
+ auto_refresh=True
256
372
  )
257
373
 
258
- atexit.register(self._stop_on_exit)
259
-
260
374
  self._active = self._console.is_terminal
261
375
  if not self._active:
262
376
  self._console = None
@@ -264,34 +378,39 @@ class Board(metaclass=BoardSingleton):
264
378
 
265
379
  self._layout = Layout()
266
380
 
267
- # Manager to thread data
268
- self._manager = multiprocessing.Manager()
269
-
270
- self._render_event = self._manager.Event()
271
- self._render_stop_event = self._manager.Event()
381
+ self._render_event = manager.Event()
382
+ self._render_stop_event = manager.Event()
272
383
  self._render_thread = None
273
384
 
274
385
  # Holds thread job data
275
- self._board_info = self._manager.Namespace()
386
+ self._board_info = manager.Namespace()
276
387
  self._board_info.data_modified = False
277
- self._job_data = self._manager.dict()
278
- self._job_data_lock = self._manager.Lock()
388
+ self._job_data = manager.dict()
389
+ self._job_data_lock = manager.Lock()
279
390
 
280
391
  self._render_data = SessionData()
281
392
  self._render_data_lock = threading.Lock()
282
393
 
283
- self._log_handler_queue = self._manager.Queue()
394
+ self._log_handler_queue = manager.Queue()
395
+
396
+ self._log_handler = LogBuffer(self._log_handler_queue, n=120, event=self._render_event)
284
397
 
285
- self._log_handler = LogBufferHandler(
286
- self._log_handler_queue, n=120, event=self._render_event)
398
+ # Sleep time for the dashboard
399
+ self._dwell = 0.1
287
400
 
288
401
  if not self.__JOB_BOARD_HEADER:
289
402
  self._layout.padding_job_board_header = 0
290
403
 
291
404
  self._metrics = ("warnings", "errors")
292
405
 
293
- def _stop_on_exit(self):
294
- self.stop()
406
+ def make_log_hander(self) -> logging.Handler:
407
+ """
408
+ Creates and returns a logging handler that directs logs to this board.
409
+
410
+ Returns:
411
+ logging.Handler: The log handler instance.
412
+ """
413
+ return self._log_handler.make_handler()
295
414
 
296
415
  def open_dashboard(self):
297
416
  """Starts the dashboard rendering thread if it is not already running."""
@@ -312,8 +431,12 @@ class Board(metaclass=BoardSingleton):
312
431
 
313
432
  def update_manifest(self, chip, starttimes=None):
314
433
  """
315
- Updates the manifest file with the latest data from the chip object.
316
- This ensures that the dashboard reflects the current state of the chip.
434
+ Updates the dashboard with the latest data from a chip object's manifest.
435
+
436
+ Args:
437
+ chip: The SiliconCompiler chip object.
438
+ starttimes (dict, optional): A dictionary mapping (step, index) tuples
439
+ to their start times. Defaults to None.
317
440
  """
318
441
 
319
442
  if not self._active:
@@ -321,13 +444,24 @@ class Board(metaclass=BoardSingleton):
321
444
 
322
445
  self._update_render_data(chip, starttimes=starttimes)
323
446
 
324
- def is_running(self):
325
- """Returns True to indicate that the dashboard is running."""
447
+ def is_running(self) -> bool:
448
+ """
449
+ Checks if the dashboard rendering thread is currently active.
450
+
451
+ Returns:
452
+ bool: True if the dashboard is running, False otherwise.
453
+ """
326
454
 
327
455
  if not self._active:
328
456
  return False
329
457
 
330
- with self._job_data_lock:
458
+ try:
459
+ with self._job_data_lock:
460
+ if not self._render_thread:
461
+ return False
462
+
463
+ return self._render_thread.is_alive()
464
+ except BrokenPipeError:
331
465
  if not self._render_thread:
332
466
  return False
333
467
 
@@ -335,7 +469,10 @@ class Board(metaclass=BoardSingleton):
335
469
 
336
470
  def end_of_run(self, chip):
337
471
  """
338
- Stops the dashboard rendering thread and ensures all rendering operations are completed.
472
+ Signals that the run has completed, performing a final update.
473
+
474
+ Args:
475
+ chip: The SiliconCompiler chip object at the end of the run.
339
476
  """
340
477
 
341
478
  if not self._active:
@@ -345,7 +482,7 @@ class Board(metaclass=BoardSingleton):
345
482
 
346
483
  def stop(self):
347
484
  """
348
- Stops the dashboard rendering thread and ensures all rendering operations are completed.
485
+ Stops the dashboard rendering thread and cleans up the terminal display.
349
486
  """
350
487
  if not self.is_running():
351
488
  return
@@ -362,6 +499,10 @@ class Board(metaclass=BoardSingleton):
362
499
 
363
500
  # Restore terminal
364
501
  self.live.stop()
502
+
503
+ # Print final render to avoid losing it
504
+ if self.live._screen:
505
+ self._console.print(self._get_rendable())
365
506
  self._console.show_cursor()
366
507
 
367
508
  def wait(self):
@@ -372,12 +513,12 @@ class Board(metaclass=BoardSingleton):
372
513
  self._render_thread.join()
373
514
 
374
515
  @staticmethod
375
- def format_status(status: str):
516
+ def format_status(status: str) -> str:
376
517
  """
377
- Formats the status of a node for display in the dashboard.
518
+ Formats a node status string with rich-compatible color markup.
378
519
 
379
520
  Args:
380
- status (str): The status of the node (e.g., 'running', 'success', 'error').
521
+ status (str): The status of the node (e.g., 'RUNNING', 'SUCCESS').
381
522
 
382
523
  Returns:
383
524
  str: A formatted string with the status styled for display.
@@ -385,25 +526,37 @@ class Board(metaclass=BoardSingleton):
385
526
  return f"[node.{status.lower()}]{status.upper()}[/]"
386
527
 
387
528
  @staticmethod
388
- def format_node(design, jobname, step, index, multi_job) -> str:
529
+ def format_node(design: str, jobname: str, step: str, index: int, multi_job: bool) -> str:
389
530
  """
390
- Formats a node's information for display in the dashboard.
531
+ Formats a node's identifier for display in the dashboard.
391
532
 
392
533
  Args:
393
534
  design (str): The design name.
394
535
  jobname (str): The job name.
395
536
  step (str): The step name.
396
537
  index (int): The step index.
538
+ multi_job (bool): Flag indicating if multiple jobs are running, which
539
+ determines the format of the identifier.
397
540
 
398
541
  Returns:
399
- str: A formatted string with the node's information styled for display.
542
+ str: A formatted string representing the node.
400
543
  """
401
544
  if multi_job:
402
545
  return f"{design}/{jobname}/{step}/{index}"
403
546
  else:
404
547
  return f"{step}/{index}"
405
548
 
406
- def _render_log(self, layout):
549
+ def _render_log(self, layout: Layout):
550
+ """
551
+ Renders the log message area of the dashboard.
552
+
553
+ Args:
554
+ layout (Layout): The current layout object containing dimensions.
555
+
556
+ Returns:
557
+ rich.group.Group or None: A renderable Group object for the log
558
+ area, or None if there's no space.
559
+ """
407
560
  if layout.log_height == 0:
408
561
  return None
409
562
 
@@ -420,12 +573,16 @@ class Board(metaclass=BoardSingleton):
420
573
 
421
574
  return Group(table, Padding("", (0, 0)))
422
575
 
423
- def _render_job_dashboard(self, layout):
576
+ def _render_job_dashboard(self, layout: Layout):
424
577
  """
425
- Creates a table of jobs and their statuses for display in the dashboard.
578
+ Renders the main job status board.
579
+
580
+ Args:
581
+ layout (Layout): The current layout object containing dimensions.
426
582
 
427
583
  Returns:
428
- Group: A Rich Group object containing tables for each job.
584
+ rich.group.Group or None: A renderable Group containing the job table,
585
+ or None if there is no space or no data.
429
586
  """
430
587
  # Don't render anything if there is not enough space
431
588
  if layout.job_board_height == 0:
@@ -477,13 +634,12 @@ class Board(metaclass=BoardSingleton):
477
634
  job = job_data[chipid]
478
635
  node = job.nodes[node_idx]
479
636
 
480
- if (
481
- layout.job_board_show_log
482
- and os.path.exists(node["log"])
483
- ):
484
- log_file = "[bright_black]{}[/]".format(node['log'])
485
- else:
486
- log_file = None
637
+ log_file = None
638
+ if layout.job_board_show_log:
639
+ for log in node["log"]:
640
+ if os.path.exists(log):
641
+ log_file = "[bright_black]{}[/]".format(log)
642
+ break
487
643
 
488
644
  if node["time"]["duration"] is not None:
489
645
  duration = f'{node["time"]["duration"]:.1f}s'
@@ -513,21 +669,19 @@ class Board(metaclass=BoardSingleton):
513
669
  return Group(table, Padding("", (0, 0)))
514
670
  return Group(Padding("", (0, 0)), table, Padding("", (0, 0)))
515
671
 
516
- def _render_progress_bar(self, layout):
672
+ def _render_progress_bar(self, layout: Layout):
517
673
  """
518
- Creates progress bars showing job completion for display in the dashboard.
674
+ Renders the progress bar section of the dashboard.
675
+
676
+ Args:
677
+ layout (Layout): The current layout object containing dimensions.
519
678
 
520
679
  Returns:
521
- Group: A Rich Group object containing progress bars for each job.
680
+ rich.group.Group or rich.padding.Padding: A renderable object for the
681
+ progress bars.
522
682
  """
523
683
  with self._render_data_lock:
524
684
  job_data = self._render_data.jobs.copy()
525
- done = self._render_data.finished > 0 \
526
- and self._render_data.total == self._render_data.finished \
527
- and self._render_data.success == self._render_data.total
528
-
529
- if done:
530
- return None
531
685
 
532
686
  ref_time = time.time()
533
687
  runtimes = {}
@@ -545,6 +699,24 @@ class Board(metaclass=BoardSingleton):
545
699
 
546
700
  runtime_width = len(f"{max([0, *runtimes.values()]):.1f}")
547
701
 
702
+ job_info = []
703
+ for name, job in job_data.items():
704
+ done = job.finished == job.total
705
+ job_info.append(
706
+ (done, f"{job.design}/{job.jobname}", job.total, job.success, runtimes[name]))
707
+
708
+ while len(job_info) > layout.progress_bar_height:
709
+ for job in job_info:
710
+ if job[0]:
711
+ # complete complete and can be removed
712
+ job_info.remove(job)
713
+ break
714
+ # remove first job
715
+ del job_info[0]
716
+
717
+ if not job_info:
718
+ return Padding("", (0, 0))
719
+
548
720
  progress = Progress(
549
721
  TextColumn("[progress.description]{task.description}"),
550
722
  MofNCompleteColumn(),
@@ -552,48 +724,23 @@ class Board(metaclass=BoardSingleton):
552
724
  TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
553
725
  TextColumn(f" {{task.fields[runtime]:>{runtime_width}.1f}}s")
554
726
  )
555
- nodes = 0
556
- for name, job in job_data.items():
557
- nodes += len(job.nodes)
727
+ for _, name, total, success, runtime in job_info:
558
728
  progress.add_task(
559
- f"[text.primary]Progress ({job.design}/{job.jobname}):",
560
- total=job.total,
561
- completed=job.success,
562
- runtime=runtimes[name]
729
+ f"[text.primary]Progress ({name}):",
730
+ total=total,
731
+ completed=success,
732
+ runtime=runtime
563
733
  )
564
734
 
565
- if nodes == 0:
566
- return Padding("", (0, 0))
567
-
568
735
  return Group(progress, Padding("", (0, 0)))
569
736
 
570
- def _render_final(self, layout):
571
- """
572
- Creates a summary of the final results, including runtime, passed, and failed jobs.
573
-
574
- Returns:
575
- Padding: A Rich Padding object containing the summary text.
576
- """
577
- with self._render_data_lock:
578
- success = self._render_data.success
579
- error = self._render_data.error
580
- total = self._render_data.total
581
- finished = self._render_data.finished
582
- runtime = self._render_data.runtime
583
-
584
- if finished != 0 and finished == total:
585
- return Padding(
586
- f"[text.primary]Results {runtime:.2f}s\n"
587
- f" [success]{success} passed[/]\n"
588
- f" [error]{error} failed[/]\n"
589
- )
590
-
591
- return self._render_log(layout)
592
-
593
737
  def _render(self):
594
738
  """
595
- Main rendering method for the TUI. Continuously updates the dashboard
596
- with the latest data until the stop event is set.
739
+ Main rendering loop for the dashboard.
740
+
741
+ This method runs in a separate thread. It waits for update events,
742
+ fetches the latest data, re-renders the components, and updates the
743
+ live display.
597
744
  """
598
745
 
599
746
  def update_data():
@@ -618,7 +765,7 @@ class Board(metaclass=BoardSingleton):
618
765
 
619
766
  while not check_stop_event():
620
767
  try:
621
- if self._render_event.wait(timeout=0.2):
768
+ if self._render_event.wait(timeout=self._dwell):
622
769
  self._render_event.clear()
623
770
  except: # noqa E722
624
771
  # Catch any multiprocessing errors
@@ -629,6 +776,7 @@ class Board(metaclass=BoardSingleton):
629
776
 
630
777
  update_data()
631
778
  self.live.update(self._get_rendable(), refresh=True)
779
+ time.sleep(self._dwell)
632
780
 
633
781
  finally:
634
782
  update_data()
@@ -637,7 +785,13 @@ class Board(metaclass=BoardSingleton):
637
785
  else:
638
786
  self._console.print(self._get_rendable())
639
787
 
640
- def _update_layout(self):
788
+ def _update_layout(self) -> Layout:
789
+ """
790
+ Updates the layout dimensions based on the current data and console size.
791
+
792
+ Returns:
793
+ Layout: The updated layout object.
794
+ """
641
795
  with self._render_data_lock:
642
796
  visible_progress_bars = len(self._render_data.jobs)
643
797
  visible_jobs_count = self._render_data.total - self._render_data.skipped
@@ -652,6 +806,10 @@ class Board(metaclass=BoardSingleton):
652
806
  return self._layout
653
807
 
654
808
  def _update_rendable_data(self):
809
+ """
810
+ Transfers job data from the shared multiprocessing dictionary to the
811
+ local render data object, aggregating session-wide statistics.
812
+ """
655
813
  jobs = {}
656
814
  with self._job_data_lock:
657
815
  if self._board_info.data_modified:
@@ -688,18 +846,20 @@ class Board(metaclass=BoardSingleton):
688
846
 
689
847
  def _get_rendable(self):
690
848
  """
691
- Combines all dashboard components (job table, progress bars, final summary)
692
- into a single renderable group.
849
+ Assembles the final renderable object for the `rich.live` display.
850
+
851
+ It gets the latest layout, renders each component (job board, progress bars, log),
852
+ and combines them into a single `rich.group.Group`.
693
853
 
694
854
  Returns:
695
- Group: A Rich Group object containing all dashboard components.
855
+ rich.group.Group: The complete, renderable dashboard layout.
696
856
  """
697
857
 
698
858
  layout = self._update_layout()
699
859
 
700
860
  new_table = self._render_job_dashboard(layout)
701
861
  new_bar = self._render_progress_bar(layout)
702
- footer = self._render_final(layout)
862
+ footer = self._render_log(layout)
703
863
 
704
864
  items = []
705
865
  if new_table:
@@ -715,8 +875,13 @@ class Board(metaclass=BoardSingleton):
715
875
 
716
876
  def _update_render_data(self, chip, starttimes=None, complete=False):
717
877
  """
718
- Updates the render data with the latest job and node information from the chip object.
719
- This data is used to populate the dashboard.
878
+ Extracts job and node information from a chip object and updates the
879
+ shared job data dictionary, triggering a render event.
880
+
881
+ Args:
882
+ chip: The SiliconCompiler chip object.
883
+ starttimes (dict, optional): Dictionary of node start times. Defaults to None.
884
+ complete (bool, optional): Flag indicating if the job is complete. Defaults to False.
720
885
  """
721
886
 
722
887
  if not chip:
@@ -735,6 +900,21 @@ class Board(metaclass=BoardSingleton):
735
900
  self._render_event.set()
736
901
 
737
902
  def _get_job(self, chip, starttimes=None) -> JobData:
903
+ """
904
+ Parses a chip object to extract detailed information about the flowgraph,
905
+ node statuses, timings, and metrics.
906
+
907
+ This method calculates node display priority based on run status and
908
+ dependencies.
909
+
910
+ Args:
911
+ chip: The SiliconCompiler chip object to parse.
912
+ starttimes (dict, optional): A dictionary of node start times.
913
+ Defaults to None.
914
+
915
+ Returns:
916
+ JobData: A data object populated with the extracted information.
917
+ """
738
918
  if not starttimes:
739
919
  starttimes = {}
740
920
 
@@ -881,13 +1061,18 @@ class Board(metaclass=BoardSingleton):
881
1061
  "duration": duration
882
1062
  },
883
1063
  "metrics": node_metrics,
884
- "log": os.path.join(
1064
+ "log": [os.path.join(
885
1065
  os.path.relpath(
886
1066
  chip.getworkdir(step=step, index=index),
887
1067
  chip.cwd,
888
1068
  ),
889
- f"{step}.log",
890
- ),
1069
+ f"{step}.log"),
1070
+ os.path.join(
1071
+ os.path.relpath(
1072
+ chip.getworkdir(step=step, index=index),
1073
+ chip.cwd,
1074
+ ),
1075
+ f"sc_{step}_{index}.log")],
891
1076
  "print": {
892
1077
  "order": nodeorder[(step, index)],
893
1078
  "priority": node_priority[(step, index)]