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,3 +1,11 @@
1
+ """
2
+ A collection of functions that create and manage UI components for the
3
+ SiliconCompiler web dashboard using Streamlit.
4
+
5
+ This module contains functions for rendering various parts of the dashboard,
6
+ such as headers, file viewers, metric tables, and the interactive flowgraph.
7
+ These components are composed by the layout functions to build the complete UI.
8
+ """
1
9
  import base64
2
10
  import json
3
11
  import os
@@ -19,6 +27,7 @@ from siliconcompiler.report.dashboard.web.utils import file_utils
19
27
  from siliconcompiler.report.dashboard.web.components import flowgraph
20
28
 
21
29
 
30
+ # --- Constants for Page Configuration ---
22
31
  SC_ABOUT = [
23
32
  f"SiliconCompiler {sc_version}",
24
33
  '''A compiler framework that automates translation from source code to
@@ -38,14 +47,13 @@ SC_FONT_PATH = os.path.join(SC_DATA_ROOT, 'RobotoMono', 'RobotoMono-Regular.ttf'
38
47
 
39
48
  def page_header(title_col_width=0.7):
40
49
  """
41
- Displays the title and a selectbox that allows you to select a given run
42
- to inspect.
50
+ Displays the main page header, including the design title, a job selector,
51
+ and a settings popover for developers.
43
52
 
44
53
  Args:
45
- title_col_width (float) : A number between 0 and 1 which is the percentage of the
46
- width of the screen given to the title and logo. The rest is given to selectbox.
54
+ title_col_width (float): The percentage of the page width to allocate
55
+ to the title and logo. The rest is divided among other components.
47
56
  """
48
-
49
57
  if state.DEVELOPER:
50
58
  col_width = (1 - title_col_width) / 2
51
59
  title_col, job_select_col, settings_col = \
@@ -62,6 +70,7 @@ def page_header(title_col_width=0.7):
62
70
  if state.DEVELOPER:
63
71
  with settings_col:
64
72
  with streamlit.popover("Settings", use_container_width=True):
73
+ # Layout selection
65
74
  all_layouts = layouts.get_all_layouts()
66
75
  layout_index = all_layouts.index(state.get_key(state.APP_LAYOUT))
67
76
  if state.set_key(
@@ -69,98 +78,68 @@ def page_header(title_col_width=0.7):
69
78
  streamlit.selectbox("Layout", all_layouts, index=layout_index)):
70
79
  state.set_key(state.APP_RERUN, "Layout")
71
80
 
81
+ # Debugging and refresh rate settings
72
82
  state._DEBUG = streamlit.checkbox("Debug", state._DEBUG)
73
-
74
83
  state.set_key(
75
84
  state.APP_RUNNING_REFRESH,
76
85
  streamlit.slider(
77
86
  "Running refresh rate (ms)",
78
- min_value=1000,
79
- max_value=10000,
80
- step=500,
87
+ min_value=1000, max_value=10000, step=500,
81
88
  value=state.get_key(state.APP_RUNNING_REFRESH)))
82
-
83
89
  state.set_key(
84
90
  state.APP_STOPPED_REFRESH,
85
91
  streamlit.slider(
86
92
  "Stopped refresh rate (ms)",
87
- min_value=1000,
88
- max_value=100000,
89
- step=1000,
93
+ min_value=1000, max_value=100000, step=1000,
90
94
  value=state.get_key(state.APP_STOPPED_REFRESH)))
91
95
 
96
+ # Content display settings
92
97
  state.set_key(
93
98
  state.MAX_DICT_ITEMS_TO_SHOW,
94
99
  streamlit.number_input(
95
100
  "Maximum dict item to show",
96
- min_value=1,
97
- max_value=10000,
101
+ min_value=1, max_value=10000,
98
102
  value=state.get_key(state.MAX_DICT_ITEMS_TO_SHOW)))
99
-
100
103
  state.set_key(
101
104
  state.MAX_FILE_LINES_TO_SHOW,
102
105
  streamlit.number_input(
103
106
  "Maximum file lines to show",
104
- min_value=100,
105
- max_value=1000,
106
- step=100,
107
+ min_value=100, max_value=1000, step=100,
107
108
  value=state.get_key(state.MAX_FILE_LINES_TO_SHOW)))
108
109
 
109
110
 
110
111
  def design_title(design=""):
112
+ """
113
+ Renders the SiliconCompiler logo and design name using custom HTML and CSS.
114
+
115
+ Args:
116
+ design (str): The name of the design to display.
117
+ """
118
+ # Inject custom font and CSS for styling the title
111
119
  font = base64.b64encode(open(SC_FONT_PATH, "rb").read()).decode()
112
- streamlit.html(
113
- f'''
120
+ streamlit.html(f'''
114
121
  <head>
115
122
  <style>
116
- /* Define the @font-face rule */
117
123
  @font-face {{
118
- font-family: 'Roboto Mono';
119
- src: url(data:font/truetype;charset=utf-8;base64,{font}) format('truetype');
120
- font-weight: normal;
121
- font-style: normal;
124
+ font-family: 'Roboto Mono';
125
+ src: url(data:font/truetype;charset=utf-8;base64,{font}) format('truetype');
122
126
  }}
123
-
124
- /* Styles for the logo and text */
125
- .logo-container {{
126
- display: flex;
127
- align-items: flex-start;
127
+ .logo-container {{ display: flex; align-items: flex-start; }}
128
+ .logo-image {{ margin-right: 10px; margin-top: -10px; }}
129
+ .logo-text {{ display: flex; flex-direction: column; margin-top: -20px; }}
130
+ .design-text, .dashboard-text {{
131
+ font-family: 'Roboto Mono', sans-serif;
132
+ font-weight: 700 !important;
133
+ font-size: 30px !important;
128
134
  }}
129
-
130
- .logo-image {{
131
- margin-right: 10px;
132
- margin-top: -10px;
133
- }}
134
-
135
- .logo-text {{
136
- display: flex;
137
- flex-direction: column;
138
- margin-top: -20px;
139
- }}
140
-
141
- .design-text {{
142
- color: #F1C437; /* Yellow color */
143
- font-family: 'Roboto Mono', sans-serif;
144
- font-weight: 700 !important;
145
- font-size: 30px !important;
146
- margin-bottom: -16px;
147
- }}
148
-
149
- .dashboard-text {{
150
- color: #1D4482; /* Blue color */
151
- font-family: 'Roboto Mono', sans-serif;
152
- font-weight: 700 !important;
153
- font-size: 30px !important;
154
- }}
155
-
135
+ .design-text {{ color: #F1C437; margin-bottom: -16px; }}
136
+ .dashboard-text {{ color: #1D4482; }}
156
137
  </style>
157
- </head>
158
- '''
159
- )
138
+ </head>''')
160
139
 
140
+ # Render the logo and title HTML
161
141
  logo = base64.b64encode(open(SC_LOGO_PATH, "rb").read()).decode()
162
- streamlit.html(
163
- f'''
142
+ streamlit.html(f'''
164
143
  <body>
165
144
  <div class="logo-container">
166
145
  <img src="data:image/png;base64,{logo}" alt="SiliconCompiler logo"
@@ -170,23 +149,33 @@ def design_title(design=""):
170
149
  <p class="dashboard-text">dashboard</p>
171
150
  </div>
172
151
  </div>
173
- </body>
174
- '''
175
- )
152
+ </body>''')
176
153
 
177
154
 
178
155
  def job_selector():
156
+ """
157
+ Displays a selectbox for choosing a job/run to inspect.
158
+
159
+ The list of jobs includes the current run ('default') and any historical
160
+ runs found in the manifest.
161
+ """
179
162
  job = streamlit.selectbox(
180
163
  'pick a job',
181
164
  state.get_chips(),
182
165
  label_visibility='collapsed')
183
166
 
184
167
  if state.set_key(state.SELECTED_JOB, job):
185
- # Job changed, so need to run
168
+ # If the job changes, trigger a rerun to update the entire dashboard.
186
169
  state.set_key(state.APP_RERUN, "Job")
187
170
 
188
171
 
189
172
  def setup_page():
173
+ """
174
+ Configures the global Streamlit page settings.
175
+
176
+ This should be one of the first Streamlit commands called. It sets the page
177
+ title, icon, layout, and custom menu items.
178
+ """
190
179
  streamlit.set_page_config(
191
180
  page_title=f'{state.get_chip().design} dashboard',
192
181
  page_icon=Image.open(SC_LOGO_PATH),
@@ -195,6 +184,19 @@ def setup_page():
195
184
 
196
185
 
197
186
  def file_viewer(chip, path, page_key=None, header_col_width=0.89):
187
+ """
188
+ Renders a viewer for a specified file.
189
+
190
+ Supports images, JSON, and plain text files with syntax highlighting
191
+ and pagination.
192
+
193
+ Args:
194
+ chip (Chip): The chip object, used for context (e.g., workdir).
195
+ path (str): The absolute path to the file to display.
196
+ page_key (str, optional): The state key to use for storing the current
197
+ page number for paginated text files.
198
+ header_col_width (float): The percentage of width for the file header.
199
+ """
198
200
  if not path:
199
201
  streamlit.error('Select a file')
200
202
  return
@@ -203,90 +205,74 @@ def file_viewer(chip, path, page_key=None, header_col_width=0.89):
203
205
  streamlit.error(f'{path} is not a file')
204
206
  return
205
207
 
206
- # Detect file type
208
+ # --- File Header and Download Button ---
207
209
  relative_path = os.path.relpath(path, chip.getworkdir())
208
210
  filename = os.path.basename(path)
209
211
  file_extension = utils.get_file_ext(path)
210
212
 
211
- # Build streamlit module
212
213
  header_col, download_col = \
213
214
  streamlit.columns([header_col_width, 1 - header_col_width], gap='small')
214
-
215
215
  with header_col:
216
216
  streamlit.header(relative_path)
217
-
218
217
  with download_col:
219
- streamlit.markdown(' ') # aligns download button with title
220
- streamlit.download_button(
221
- label="Download",
222
- data=path,
223
- file_name=filename,
224
- use_container_width=True)
225
-
218
+ streamlit.markdown(' ') # Aligns download button with title
219
+ with open(path, "rb") as fp:
220
+ streamlit.download_button(
221
+ label="Download",
222
+ data=fp,
223
+ file_name=filename,
224
+ use_container_width=True)
225
+
226
+ # --- File Content Viewer ---
226
227
  try:
227
228
  if file_extension in ('jpg', 'jpeg', 'png'):
228
- # Data is an image
229
229
  streamlit.image(path)
230
230
  elif file_extension == 'json':
231
- # Data is a json file
232
231
  data = json.loads(file_utils.read_file(path, None))
232
+ # Expand JSON if it's not too large
233
233
  expand_keys = report.get_total_manifest_key_count(data) < \
234
234
  state.get_key(state.MAX_DICT_ITEMS_TO_SHOW)
235
235
  if not expand_keys:
236
- # Open two levels
237
- expand_keys = 2
236
+ expand_keys = 2 # Default to expanding two levels
238
237
  streamlit.json(data, expanded=expand_keys)
239
238
  else:
239
+ # For text files, implement pagination
240
240
  file_data = file_utils.read_file(path, None).splitlines()
241
- max_pages = len(file_data)
242
241
  page_size = state.get_key(state.MAX_FILE_LINES_TO_SHOW)
242
+ max_pages = (len(file_data) + page_size - 1) // page_size
243
243
 
244
- file_section = streamlit.container()
245
-
246
- if page_key:
247
- if state.get_key(page_key) is None:
248
- state.set_key(page_key, 1)
249
- index = state.get_key(page_key)
250
- else:
251
- index = 1
244
+ if page_key and state.get_key(page_key) is None:
245
+ state.set_key(page_key, 1)
246
+ index = state.get_key(page_key) if page_key else 1
252
247
 
253
248
  page = sac.pagination(
254
- align='center',
255
- index=index,
256
- jump=True,
257
- show_total=True,
258
- page_size=page_size,
259
- total=max_pages,
260
- disabled=max_pages < state.get_key(state.MAX_FILE_LINES_TO_SHOW))
249
+ align='center', index=index, total=max_pages,
250
+ disabled=max_pages <= 1)
261
251
 
262
252
  if page_key:
263
253
  state.set_key(page_key, page)
264
254
 
265
- start_idx = (page - 1) * state.get_key(state.MAX_FILE_LINES_TO_SHOW)
266
- end_idx = start_idx + state.get_key(state.MAX_FILE_LINES_TO_SHOW)
255
+ start_idx = (page - 1) * page_size
256
+ end_idx = start_idx + page_size
267
257
  file_show = file_data[start_idx:end_idx]
268
- with file_section:
269
- # Assume file is text
270
- streamlit.code(
271
- "\n".join(file_show),
272
- language=file_utils.get_file_type(file_extension),
273
- line_numbers=True)
258
+
259
+ streamlit.code(
260
+ "\n".join(file_show),
261
+ language=file_utils.get_file_type(file_extension),
262
+ line_numbers=True)
274
263
  except Exception as e:
275
- streamlit.markdown(f'Error occurred reading file: {e}')
264
+ streamlit.error(f'Error occurred reading file: {e}')
276
265
 
277
266
 
278
- def manifest_viewer(
279
- chip,
280
- header_col_width=0.70):
267
+ def manifest_viewer(chip, header_col_width=0.70):
281
268
  """
282
- Displays the manifest and a way to search through the manifest.
269
+ Displays the chip's manifest with search and filtering options.
283
270
 
284
271
  Args:
285
- chip (Chip) : Chip object
286
- header_col_width (float) : A number between 0 and 1 which is the maximum
287
- percentage of the width of the screen given to the header. The rest
288
- is given to the settings and download buttons.
272
+ chip (Chip): The chip object whose manifest will be displayed.
273
+ header_col_width (float): The percentage of width for the header.
289
274
  """
275
+ # --- Header and Settings ---
290
276
  end_column_widths = (1 - header_col_width) / 2
291
277
  header_col, settings_col, download_col = \
292
278
  streamlit.columns(
@@ -294,102 +280,92 @@ def manifest_viewer(
294
280
  gap='small')
295
281
  with header_col:
296
282
  streamlit.header('Manifest')
297
-
298
283
  with settings_col:
299
- streamlit.markdown(' ') # aligns with title
284
+ streamlit.markdown(' ') # Aligns with title
300
285
  with streamlit.popover("Settings", use_container_width=True):
301
- if streamlit.checkbox(
302
- 'Raw manifest',
303
- help='Click here to see the manifest before it was made more readable'):
286
+ # Filtering options
287
+ if streamlit.checkbox('Raw manifest', help='View raw, unprocessed manifest'):
304
288
  manifest_to_show = chip.getdict()
305
289
  else:
306
290
  manifest_to_show = report.make_manifest(chip)
307
291
 
308
- if streamlit.checkbox(
309
- 'Hide empty values',
310
- help='Hide empty keypaths',
311
- value=True):
312
- manifest_to_show = report.search_manifest(
313
- manifest_to_show,
314
- value_search='*')
315
-
316
- search_key = streamlit.text_input('Search Keys', '', placeholder="Keys")
317
- search_value = streamlit.text_input('Search Values', '', placeholder="Values")
292
+ if streamlit.checkbox('Hide empty values', value=True):
293
+ manifest_to_show = report.search_manifest(manifest_to_show, value_search='*')
318
294
 
295
+ # Search functionality
296
+ search_key = streamlit.text_input('Search Keys', placeholder="Keys")
297
+ search_value = streamlit.text_input('Search Values', placeholder="Values")
319
298
  manifest_to_show = report.search_manifest(
320
- manifest_to_show,
321
- key_search=search_key,
322
- value_search=search_value)
323
-
299
+ manifest_to_show, key_search=search_key, value_search=search_value)
324
300
  with download_col:
325
- streamlit.markdown(' ') # aligns with title
301
+ streamlit.markdown(' ') # Aligns with title
326
302
  streamlit.download_button(
327
- label='Download',
328
- file_name='manifest.json',
303
+ label='Download', file_name='manifest.json',
329
304
  data=json.dumps(chip.getdict(), indent=2),
330
- mime="application/json",
331
- use_container_width=True)
305
+ mime="application/json", use_container_width=True)
332
306
 
307
+ # --- Manifest Display ---
333
308
  expand_keys = report.get_total_manifest_key_count(manifest_to_show) < \
334
309
  state.get_key(state.MAX_DICT_ITEMS_TO_SHOW)
335
310
  if not expand_keys:
336
- # Open two levels
337
- expand_keys = 2
311
+ expand_keys = 2 # Default to expanding two levels
338
312
  streamlit.json(manifest_to_show, expanded=expand_keys)
339
313
 
340
314
 
341
315
  def metrics_viewer(metric_dataframe, metric_to_metric_unit_map, header_col_width=0.7, height=None):
342
316
  """
343
- Displays multi-select check box to the users which allows them to select
344
- which nodes and metrics to view in the dataframe.
317
+ Displays a filterable and transposable table of metrics.
345
318
 
346
319
  Args:
347
- metric_dataframe (Pandas.DataFrame) : Contains the metrics of all nodes.
348
- metric_to_metric_unit_map (dict) : Maps the metric to the associated metric unit.
320
+ metric_dataframe (pandas.DataFrame): The dataframe containing all metrics.
321
+ metric_to_metric_unit_map (dict): A mapping from formatted metric names
322
+ (with units) to raw metric names.
323
+ header_col_width (float): The percentage of width for the header.
324
+ height (int, optional): The height of the dataframe in pixels.
349
325
  """
350
-
351
326
  all_nodes = metric_dataframe.columns.tolist()
352
327
  all_metrics = list(metric_to_metric_unit_map.values())
353
328
 
329
+ # --- Header and Settings ---
354
330
  header_col, settings_col = streamlit.columns(
355
- [header_col_width, 1 - header_col_width],
356
- gap="large")
331
+ [header_col_width, 1 - header_col_width], gap="large")
357
332
  with header_col:
358
333
  streamlit.header('Metrics')
359
334
  with settings_col:
360
- # Align to header
361
- streamlit.markdown('')
362
-
335
+ streamlit.markdown('') # Align to header
363
336
  with streamlit.popover("Settings", use_container_width=True):
364
- transpose = streamlit.checkbox(
365
- 'Transpose',
366
- help='Transpose the metrics table')
367
-
337
+ transpose = streamlit.checkbox('Transpose', help='Transpose the metrics table')
368
338
  display_nodes = streamlit.multiselect('Pick nodes to include', all_nodes, [])
369
339
  display_metrics = streamlit.multiselect('Pick metrics to include?', all_metrics, [])
370
340
 
371
- # Filter data
341
+ # --- Filter and Display Dataframe ---
372
342
  if not display_nodes:
373
343
  display_nodes = all_nodes
374
-
375
344
  if not display_metrics:
376
345
  display_metrics = all_metrics
377
346
 
378
347
  dataframe_nodes = list(display_nodes)
379
- dataframe_metrics = []
380
- for metric in metric_dataframe.index.tolist():
381
- if metric_to_metric_unit_map[metric] in display_metrics:
382
- dataframe_metrics.append(metric)
348
+ dataframe_metrics = [
349
+ metric for metric in metric_dataframe.index.tolist()
350
+ if metric_to_metric_unit_map.get(metric) in display_metrics
351
+ ]
383
352
 
384
- metric_dataframe = metric_dataframe.loc[dataframe_metrics, dataframe_nodes]
353
+ filtered_df = metric_dataframe.loc[dataframe_metrics, dataframe_nodes]
385
354
  if transpose:
386
- metric_dataframe = metric_dataframe.transpose()
355
+ filtered_df = filtered_df.transpose()
387
356
 
388
- # TODO By July 2024, Streamlit will let catch click events on the dataframe
389
- streamlit.dataframe(metric_dataframe, use_container_width=True, height=height)
357
+ streamlit.dataframe(filtered_df, use_container_width=True, height=height)
390
358
 
391
359
 
392
360
  def node_image_viewer(chip, step, index):
361
+ """
362
+ Displays a gallery of all image files associated with a given node.
363
+
364
+ Args:
365
+ chip (Chip): The chip object.
366
+ step (str): The step of the node.
367
+ index (str): The index of the node.
368
+ """
393
369
  exts = ('png', 'jpg', 'jpeg')
394
370
  images = []
395
371
  for path, _, files in report.get_files(chip, step, index):
@@ -397,46 +373,53 @@ def node_image_viewer(chip, step, index):
397
373
 
398
374
  if not images:
399
375
  streamlit.markdown("No images to show")
376
+ return
400
377
 
401
378
  work_dir = chip.getworkdir(step=step, index=index)
402
-
403
379
  columns = streamlit.slider(
404
380
  "Image columns",
405
- min_value=1,
406
- max_value=min(len(images), 10),
407
- value=min(len(images), 4),
408
- disabled=len(images) < 2)
409
-
410
- column = 0
411
- for image in sorted(images):
412
- if column == 0:
413
- cols = streamlit.columns(columns)
381
+ min_value=1, max_value=min(len(images), 10),
382
+ value=min(len(images), 4), disabled=len(images) < 2)
414
383
 
415
- with cols[column]:
384
+ # Display images in a grid
385
+ for i, image in enumerate(sorted(images)):
386
+ if i % columns == 0:
387
+ cols = streamlit.columns(columns)
388
+ with cols[i % columns]:
416
389
  streamlit.image(
417
390
  image,
418
391
  caption=os.path.relpath(image, work_dir),
419
392
  use_column_width=True)
420
393
 
421
- column += 1
422
- if column == columns:
423
- column = 0
424
-
425
394
 
426
395
  def node_file_tree_viewer(chip, step, index):
396
+ """
397
+ Displays an interactive file tree for a given node.
398
+
399
+ Files are decorated with tags indicating which metrics they are a source for.
400
+ Selecting a file in the tree updates the application state to display it
401
+ in the file viewer.
402
+
403
+ Args:
404
+ chip (Chip): The chip object.
405
+ step (str): The step of the node.
406
+ index (str): The index of the node.
407
+ """
427
408
  logs_and_reports = file_utils.convert_filepaths_to_select_tree(
428
409
  report.get_files(chip, step, index))
429
410
 
430
411
  if not logs_and_reports:
431
412
  streamlit.markdown("No files to show")
413
+ return
432
414
 
415
+ # --- Prepare data for the tree component ---
433
416
  lookup = {}
434
417
  tree_items = []
435
-
436
418
  metrics_source, file_metrics = report.get_metrics_source(chip, step, index)
437
419
  work_dir = chip.getworkdir(step=step, index=index)
438
420
 
439
421
  def make_item(file):
422
+ """Recursively builds a tree item for the antd component."""
440
423
  lookup[file['value']] = file['label']
441
424
  item = sac.TreeItem(
442
425
  file['value'],
@@ -444,56 +427,55 @@ def node_file_tree_viewer(chip, step, index):
444
427
  tag=[],
445
428
  children=[])
446
429
 
430
+ # Add metric source tags
447
431
  check_file = os.path.relpath(file['value'], work_dir)
448
432
  if check_file in file_metrics:
449
433
  metrics = set(file_metrics[check_file])
450
- primary_source = set()
451
- if check_file in metrics_source:
452
- primary_source = set(metrics_source[check_file])
453
- metrics = metrics - primary_source
454
-
455
- for color, metric_set in (('blue', primary_source), ('green', metrics)):
456
- if len(item.tag) >= 5:
457
- break
458
-
459
- for metric in metric_set:
460
- if len(item.tag) < 5:
461
- item.tag.append(sac.Tag(metric, color=color))
462
- else:
463
- item.tag.append(sac.Tag('metrics...', color='geekblue'))
464
- break
434
+ primary_source = set(metrics_source.get(check_file, []))
435
+ other_metrics = metrics - primary_source
436
+
437
+ tags = [sac.Tag(m, color='blue') for m in primary_source]
438
+ tags += [sac.Tag(m, color='green') for m in other_metrics]
439
+
440
+ item.tag = tags[:5]
441
+ if len(tags) > 5:
442
+ item.tag.append(sac.Tag('...', color='geekblue'))
465
443
  item.tooltip = "metrics: " + ", ".join(file_metrics[check_file])
466
444
 
445
+ # Recursively add children for folders
467
446
  if 'children' in file:
468
447
  item.icon = 'folder'
469
- for child in file['children']:
470
- item.children.append(make_item(child))
448
+ item.children = [make_item(child) for child in file['children']]
471
449
 
472
450
  return item
473
451
 
474
- for file in logs_and_reports:
475
- tree_items.append(make_item(file))
476
-
477
- def format_label(value):
478
- return lookup[value]
452
+ tree_items = [make_item(file) for file in logs_and_reports]
479
453
 
454
+ # --- Render the tree ---
480
455
  selected = sac.tree(
481
456
  items=tree_items,
482
- format_func=format_label,
483
- size='md',
484
- icon='table',
485
- open_all=True)
457
+ format_func=lambda v: lookup.get(v, v),
458
+ size='md', icon='table', open_all=True)
486
459
 
487
460
  if selected and os.path.isfile(selected):
488
461
  state.set_key(state.SELECTED_FILE, selected)
489
- state.set_key(state.SELECTED_FILE_PAGE, None)
462
+ state.set_key(state.SELECTED_FILE_PAGE, 1)
490
463
 
491
464
 
492
465
  def node_viewer(chip, step, index, metric_dataframe, height=None):
466
+ """
467
+ Displays a summary view for a single node, including metrics, records, and files.
468
+
469
+ Args:
470
+ chip (Chip): The chip object.
471
+ step (str): The step of the node.
472
+ index (str): The index of the node.
473
+ metric_dataframe (pandas.DataFrame): The dataframe of all metrics.
474
+ height (int, optional): The height for the dataframe components.
475
+ """
493
476
  from pandas import DataFrame
494
477
 
495
478
  metrics_col, records_col, logs_and_reports_col = streamlit.columns(3, gap='small')
496
-
497
479
  node_name = f'{step}/{index}'
498
480
 
499
481
  with metrics_col:
@@ -501,58 +483,55 @@ def node_viewer(chip, step, index, metric_dataframe, height=None):
501
483
  if node_name in metric_dataframe:
502
484
  streamlit.dataframe(
503
485
  metric_dataframe[node_name].dropna(),
504
- use_container_width=True,
505
- height=height)
486
+ use_container_width=True, height=height)
506
487
  with records_col:
507
- streamlit.subheader(f'{step}/{index} details')
508
- nodes = {}
509
- nodes[step + index] = report.get_flowgraph_nodes(chip, step, index)
488
+ streamlit.subheader(f'{node_name} details')
489
+ nodes = {step + index: report.get_flowgraph_nodes(chip, step, index)}
510
490
  streamlit.dataframe(
511
491
  DataFrame.from_dict(nodes),
512
- use_container_width=True,
513
- height=height)
492
+ use_container_width=True, height=height)
514
493
  with logs_and_reports_col:
515
- streamlit.subheader(f'{step}/{index} files')
494
+ streamlit.subheader(f'{node_name} files')
516
495
  node_file_tree_viewer(chip, step, index)
517
496
 
518
497
 
519
498
  def flowgraph_viewer(chip):
520
- '''
521
- This function creates, displays, and returns the selected node of the flowgraph.
499
+ """
500
+ Displays the interactive flowgraph for the current job.
522
501
 
523
- Args:
524
- chip (Chip) : The chip object that contains the schema read from.
525
- '''
502
+ Selecting a node in the graph updates the application state.
526
503
 
504
+ Args:
505
+ chip (Chip): The chip object containing the flowgraph to display.
506
+ """
527
507
  nodes, edges = flowgraph.get_nodes_and_edges(chip)
528
- if state.set_key(state.SELECTED_FLOWGRAPH_NODE, agraph(
529
- nodes=nodes,
530
- edges=edges,
531
- config=flowgraph.get_graph_config())):
508
+ selected_node = agraph(
509
+ nodes=nodes,
510
+ edges=edges,
511
+ config=flowgraph.get_graph_config())
512
+
513
+ if state.set_key(state.SELECTED_FLOWGRAPH_NODE, selected_node):
532
514
  if state.get_key(state.SELECTED_FLOWGRAPH_NODE):
533
515
  state.set_key(state.NODE_SOURCE, "flowgraph")
534
516
 
535
517
 
536
518
  def node_selector(nodes):
537
519
  """
538
- Displays selectbox for nodes to show in the node information panel. Since
539
- both the flowgraph and selectbox show which node's information is
540
- displayed, the one clicked more recently will be displayed.
520
+ Displays a dropdown for selecting a node.
521
+
522
+ This provides an alternative to selecting a node by clicking on the flowgraph.
541
523
 
542
524
  Args:
543
- nodes (list) : Contains the metrics of all nodes.
525
+ nodes (list): A list of node name strings (e.g., ['import/0', 'syn/0']).
544
526
  """
545
527
  prev_node = state.get_selected_node()
546
528
 
547
529
  with streamlit.popover("Select Node", use_container_width=True):
548
- # Preselect node
549
- idx = 0
550
- if prev_node:
551
- idx = nodes.index(prev_node)
552
- if state.set_key(
553
- state.SELECTED_SELECTOR_NODE,
554
- streamlit.selectbox(
555
- 'Pick a node to inspect',
556
- nodes,
557
- index=idx)):
530
+ idx = nodes.index(prev_node) if prev_node in nodes else 0
531
+ selected_node = streamlit.selectbox(
532
+ 'Pick a node to inspect',
533
+ nodes,
534
+ index=idx)
535
+
536
+ if state.set_key(state.SELECTED_SELECTOR_NODE, selected_node):
558
537
  state.set_key(state.NODE_SOURCE, "selector")