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.
- siliconcompiler/__init__.py +12 -5
- siliconcompiler/__main__.py +1 -7
- siliconcompiler/_metadata.py +1 -1
- siliconcompiler/apps/_common.py +104 -23
- siliconcompiler/apps/sc.py +4 -8
- siliconcompiler/apps/sc_dashboard.py +6 -4
- siliconcompiler/apps/sc_install.py +10 -6
- siliconcompiler/apps/sc_issue.py +7 -5
- siliconcompiler/apps/sc_remote.py +1 -1
- siliconcompiler/apps/sc_server.py +9 -14
- siliconcompiler/apps/sc_show.py +6 -5
- siliconcompiler/apps/smake.py +130 -94
- siliconcompiler/apps/utils/replay.py +4 -7
- siliconcompiler/apps/utils/summarize.py +3 -5
- siliconcompiler/asic.py +420 -0
- siliconcompiler/checklist.py +25 -2
- siliconcompiler/cmdlineschema.py +534 -0
- siliconcompiler/constraints/asic_component.py +2 -2
- siliconcompiler/constraints/asic_pins.py +2 -2
- siliconcompiler/constraints/asic_timing.py +3 -3
- siliconcompiler/core.py +7 -32
- siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
- siliconcompiler/dependencyschema.py +89 -31
- siliconcompiler/design.py +176 -207
- siliconcompiler/filesetschema.py +250 -0
- siliconcompiler/flowgraph.py +274 -95
- siliconcompiler/fpga.py +124 -1
- siliconcompiler/library.py +218 -20
- siliconcompiler/metric.py +233 -20
- siliconcompiler/package/__init__.py +271 -50
- siliconcompiler/package/git.py +92 -16
- siliconcompiler/package/github.py +108 -12
- siliconcompiler/package/https.py +79 -16
- siliconcompiler/packageschema.py +88 -7
- siliconcompiler/pathschema.py +31 -2
- siliconcompiler/pdk.py +566 -1
- siliconcompiler/project.py +1095 -94
- siliconcompiler/record.py +38 -1
- siliconcompiler/remote/__init__.py +5 -2
- siliconcompiler/remote/client.py +11 -6
- siliconcompiler/remote/schema.py +5 -23
- siliconcompiler/remote/server.py +41 -54
- siliconcompiler/report/__init__.py +3 -3
- siliconcompiler/report/dashboard/__init__.py +48 -14
- siliconcompiler/report/dashboard/cli/__init__.py +99 -21
- siliconcompiler/report/dashboard/cli/board.py +364 -179
- siliconcompiler/report/dashboard/web/__init__.py +90 -12
- siliconcompiler/report/dashboard/web/components/__init__.py +219 -240
- siliconcompiler/report/dashboard/web/components/flowgraph.py +49 -26
- siliconcompiler/report/dashboard/web/components/graph.py +139 -100
- siliconcompiler/report/dashboard/web/layouts/__init__.py +29 -1
- siliconcompiler/report/dashboard/web/layouts/_common.py +38 -2
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph.py +39 -26
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_node_tab.py +50 -50
- siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_sac_tabs.py +49 -46
- siliconcompiler/report/dashboard/web/state.py +141 -14
- siliconcompiler/report/dashboard/web/utils/__init__.py +79 -16
- siliconcompiler/report/dashboard/web/utils/file_utils.py +74 -11
- siliconcompiler/report/dashboard/web/viewer.py +25 -1
- siliconcompiler/report/report.py +5 -2
- siliconcompiler/report/summary_image.py +29 -11
- siliconcompiler/scheduler/__init__.py +9 -1
- siliconcompiler/scheduler/docker.py +79 -1
- siliconcompiler/scheduler/run_node.py +35 -19
- siliconcompiler/scheduler/scheduler.py +208 -24
- siliconcompiler/scheduler/schedulernode.py +372 -46
- siliconcompiler/scheduler/send_messages.py +77 -29
- siliconcompiler/scheduler/slurm.py +76 -12
- siliconcompiler/scheduler/taskscheduler.py +140 -20
- siliconcompiler/schema/__init__.py +0 -2
- siliconcompiler/schema/baseschema.py +194 -38
- siliconcompiler/schema/journal.py +7 -4
- siliconcompiler/schema/namedschema.py +16 -10
- siliconcompiler/schema/parameter.py +55 -9
- siliconcompiler/schema/parametervalue.py +60 -0
- siliconcompiler/schema/safeschema.py +25 -2
- siliconcompiler/schema/schema_cfg.py +5 -5
- siliconcompiler/schema/utils.py +2 -2
- siliconcompiler/schema_obj.py +20 -3
- siliconcompiler/tool.py +979 -302
- siliconcompiler/tools/bambu/__init__.py +41 -0
- siliconcompiler/tools/builtin/concatenate.py +2 -2
- siliconcompiler/tools/builtin/minimum.py +2 -1
- siliconcompiler/tools/builtin/mux.py +2 -1
- siliconcompiler/tools/builtin/nop.py +2 -1
- siliconcompiler/tools/builtin/verify.py +2 -1
- siliconcompiler/tools/klayout/__init__.py +95 -0
- siliconcompiler/tools/openroad/__init__.py +289 -0
- siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +3 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +7 -2
- siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +8 -4
- siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +9 -5
- siliconcompiler/tools/openroad/scripts/common/write_images.tcl +5 -1
- siliconcompiler/tools/slang/__init__.py +1 -1
- siliconcompiler/tools/slang/elaborate.py +2 -1
- siliconcompiler/tools/vivado/scripts/sc_run.tcl +1 -1
- siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +8 -1
- siliconcompiler/tools/vivado/syn_fpga.py +6 -0
- siliconcompiler/tools/vivado/vivado.py +35 -2
- siliconcompiler/tools/vpr/__init__.py +150 -0
- siliconcompiler/tools/yosys/__init__.py +369 -1
- siliconcompiler/tools/yosys/scripts/procs.tcl +0 -1
- siliconcompiler/toolscripts/_tools.json +5 -10
- siliconcompiler/utils/__init__.py +66 -0
- siliconcompiler/utils/flowgraph.py +2 -2
- siliconcompiler/utils/issue.py +2 -1
- siliconcompiler/utils/logging.py +14 -0
- siliconcompiler/utils/multiprocessing.py +256 -0
- siliconcompiler/utils/showtools.py +10 -0
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +5 -5
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +115 -118
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
- siliconcompiler/schema/cmdlineschema.py +0 -250
- siliconcompiler/toolscripts/rhel8/install-slang.sh +0 -40
- siliconcompiler/toolscripts/rhel9/install-slang.sh +0 -40
- siliconcompiler/toolscripts/ubuntu20/install-slang.sh +0 -47
- siliconcompiler/toolscripts/ubuntu22/install-slang.sh +0 -37
- siliconcompiler/toolscripts/ubuntu24/install-slang.sh +0 -37
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
- {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
siliconcompiler/metric.py
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from typing import List, Tuple, TextIO
|
|
5
|
+
|
|
1
6
|
from siliconcompiler.schema import BaseSchema
|
|
2
7
|
from siliconcompiler.schema import EditableSchema, Parameter, PerNode, Scope
|
|
3
8
|
from siliconcompiler.schema.utils import trim
|
|
4
9
|
|
|
5
|
-
from siliconcompiler.utils
|
|
10
|
+
from siliconcompiler.utils import truncate_text, units
|
|
6
11
|
from siliconcompiler.record import RecordTime
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
class MetricSchema(BaseSchema):
|
|
15
|
+
'''
|
|
16
|
+
Schema for storing and accessing metrics collected during a run.
|
|
17
|
+
|
|
18
|
+
This class provides a structured way to define, record, and report
|
|
19
|
+
various metrics such as runtime, memory usage, and design quality
|
|
20
|
+
indicators for each step of a compilation flow.
|
|
21
|
+
'''
|
|
22
|
+
|
|
10
23
|
def __init__(self):
|
|
24
|
+
'''Initializes the MetricSchema.'''
|
|
11
25
|
super().__init__()
|
|
12
26
|
|
|
13
27
|
schema = EditableSchema(self)
|
|
@@ -98,25 +112,33 @@ class MetricSchema(BaseSchema):
|
|
|
98
112
|
|
|
99
113
|
def clear(self, step, index):
|
|
100
114
|
'''
|
|
101
|
-
|
|
115
|
+
Clears all saved metrics for a given step and index.
|
|
102
116
|
|
|
103
117
|
Args:
|
|
104
|
-
step (str):
|
|
105
|
-
index (str
|
|
118
|
+
step (str): The step name to clear metrics for.
|
|
119
|
+
index (str or int): The index to clear metrics for.
|
|
106
120
|
'''
|
|
107
121
|
for metric in self.getkeys():
|
|
108
122
|
self.unset(metric, step=step, index=str(index))
|
|
109
123
|
|
|
110
124
|
def record(self, step, index, metric, value, unit=None):
|
|
111
125
|
"""
|
|
112
|
-
|
|
126
|
+
Records a metric value for a specific step and index.
|
|
127
|
+
|
|
128
|
+
This method handles unit conversion if the metric is defined with a
|
|
129
|
+
unit in the schema.
|
|
113
130
|
|
|
114
131
|
Args:
|
|
115
|
-
step (str): step to record
|
|
116
|
-
index (str
|
|
117
|
-
metric (str): name of metric
|
|
118
|
-
value (int
|
|
119
|
-
unit (str): unit
|
|
132
|
+
step (str): The step to record the metric for.
|
|
133
|
+
index (str or int): The index to record the metric for.
|
|
134
|
+
metric (str): The name of the metric to record.
|
|
135
|
+
value (int or float): The value of the metric.
|
|
136
|
+
unit (str, optional): The unit of the provided value. If the schema
|
|
137
|
+
defines a unit for this metric, the value will be converted.
|
|
138
|
+
Defaults to None.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The recorded value after any unit conversion.
|
|
120
142
|
"""
|
|
121
143
|
metric_unit = self.get(metric, field='unit')
|
|
122
144
|
|
|
@@ -124,18 +146,21 @@ class MetricSchema(BaseSchema):
|
|
|
124
146
|
raise ValueError(f"{metric} does not have a unit, but {unit} was supplied")
|
|
125
147
|
|
|
126
148
|
if metric_unit:
|
|
127
|
-
value = convert(value, from_unit=unit, to_unit=metric_unit)
|
|
149
|
+
value = units.convert(value, from_unit=unit, to_unit=metric_unit)
|
|
128
150
|
|
|
129
151
|
return self.set(metric, value, step=step, index=str(index))
|
|
130
152
|
|
|
131
153
|
def record_tasktime(self, step, index, record):
|
|
132
154
|
"""
|
|
133
|
-
|
|
155
|
+
Records the task time for a given node based on start and end times.
|
|
134
156
|
|
|
135
157
|
Args:
|
|
136
|
-
step (str): step
|
|
137
|
-
index (str
|
|
138
|
-
record (
|
|
158
|
+
step (str): The step of the node.
|
|
159
|
+
index (str or int): The index of the node.
|
|
160
|
+
record (RecordSchema): The record schema containing timing data.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
bool: True if the time was successfully recorded, False otherwise.
|
|
139
164
|
"""
|
|
140
165
|
start_time, end_time = [
|
|
141
166
|
record.get_recorded_time(step, index, RecordTime.START),
|
|
@@ -149,13 +174,19 @@ class MetricSchema(BaseSchema):
|
|
|
149
174
|
|
|
150
175
|
def record_totaltime(self, step, index, flow, record):
|
|
151
176
|
"""
|
|
152
|
-
|
|
177
|
+
Records the cumulative total time up to the end of a given node.
|
|
178
|
+
|
|
179
|
+
This method calculates the total wall-clock time by summing the
|
|
180
|
+
durations of all previously executed parallel tasks.
|
|
153
181
|
|
|
154
182
|
Args:
|
|
155
|
-
step (str): step
|
|
156
|
-
index (str
|
|
157
|
-
flow (
|
|
158
|
-
record (
|
|
183
|
+
step (str): The step of the node.
|
|
184
|
+
index (str or int): The index of the node.
|
|
185
|
+
flow (FlowgraphSchema): The flowgraph containing the nodes.
|
|
186
|
+
record (RecordSchema): The record schema containing timing data.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
bool: True if the time was successfully recorded, False otherwise.
|
|
159
190
|
"""
|
|
160
191
|
all_nodes = flow.get_nodes()
|
|
161
192
|
node_times = [
|
|
@@ -205,9 +236,182 @@ class MetricSchema(BaseSchema):
|
|
|
205
236
|
|
|
206
237
|
return self.record(step, index, "totaltime", total_time, unit="s")
|
|
207
238
|
|
|
239
|
+
def get_formatted_metric(self, metric: str, step: str, index: str) -> str:
|
|
240
|
+
'''
|
|
241
|
+
Retrieves and formats a metric for display.
|
|
242
|
+
|
|
243
|
+
Handles special formatting for memory (binary units), time, and adds
|
|
244
|
+
SI suffixes for other float values.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
metric (str): The name of the metric to format.
|
|
248
|
+
step (str): The step of the metric.
|
|
249
|
+
index (str): The index of the metric.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
str: The formatted, human-readable metric value as a string.
|
|
253
|
+
'''
|
|
254
|
+
if metric == 'memory':
|
|
255
|
+
return units.format_binary(self.get(metric, step=step, index=index),
|
|
256
|
+
self.get(metric, field="unit"))
|
|
257
|
+
elif metric in ['exetime', 'tasktime', 'totaltime']:
|
|
258
|
+
return units.format_time(self.get(metric, step=step, index=index))
|
|
259
|
+
elif self.get(metric, field="type") == 'int':
|
|
260
|
+
return str(self.get(metric, step=step, index=index))
|
|
261
|
+
else:
|
|
262
|
+
return units.format_si(self.get(metric, step=step, index=index),
|
|
263
|
+
self.get(metric, field="unit"))
|
|
264
|
+
|
|
265
|
+
def summary_table(self,
|
|
266
|
+
nodes: List[Tuple[str, str]] = None,
|
|
267
|
+
column_width: int = 15,
|
|
268
|
+
formatted: bool = True,
|
|
269
|
+
trim_empty_metrics: bool = True):
|
|
270
|
+
'''
|
|
271
|
+
Generates a summary of metrics as a pandas DataFrame.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
nodes (List[Tuple[str, str]], optional): A list of (step, index)
|
|
275
|
+
tuples to include in the summary. If None, all nodes with
|
|
276
|
+
metrics are included. Defaults to None.
|
|
277
|
+
column_width (int, optional): The width for each column.
|
|
278
|
+
Defaults to 15.
|
|
279
|
+
formatted (bool, optional): If True, metric values are formatted
|
|
280
|
+
for human readability. Defaults to True.
|
|
281
|
+
trim_empty_metrics (bool, optional): If True, metrics that have no
|
|
282
|
+
value for any of the specified nodes are excluded.
|
|
283
|
+
Defaults to True.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
pandas.DataFrame: A DataFrame containing the metric summary.
|
|
287
|
+
'''
|
|
288
|
+
from pandas import DataFrame
|
|
289
|
+
|
|
290
|
+
if not nodes:
|
|
291
|
+
nodes = set()
|
|
292
|
+
for metric in self.getkeys():
|
|
293
|
+
for value, step, index in self.get(metric, field=None).getvalues():
|
|
294
|
+
nodes.add((step, index))
|
|
295
|
+
nodes = list(sorted(nodes))
|
|
296
|
+
|
|
297
|
+
row_labels = list(self.getkeys())
|
|
298
|
+
sort_map = {metric: 0 for metric in row_labels}
|
|
299
|
+
sort_map["errors"] = -2
|
|
300
|
+
sort_map["warnings"] = -1
|
|
301
|
+
sort_map["memory"] = 1
|
|
302
|
+
sort_map["exetime"] = 2
|
|
303
|
+
sort_map["tasktime"] = 3
|
|
304
|
+
sort_map["totaltime"] = 4
|
|
305
|
+
row_labels = sorted(row_labels, key=lambda row: sort_map[row])
|
|
306
|
+
|
|
307
|
+
if trim_empty_metrics:
|
|
308
|
+
for metric in self.getkeys():
|
|
309
|
+
data = []
|
|
310
|
+
for step, index in nodes:
|
|
311
|
+
data.append(self.get(metric, step=step, index=index))
|
|
312
|
+
if all([dat is None for dat in data]):
|
|
313
|
+
row_labels.remove(metric)
|
|
314
|
+
|
|
315
|
+
if 'totaltime' in row_labels:
|
|
316
|
+
if not any([self.get('totaltime', step=step, index=index) is None
|
|
317
|
+
for step, index in nodes]):
|
|
318
|
+
nodes.sort(
|
|
319
|
+
key=lambda node: self.get('totaltime', step=node[0], index=node[1]))
|
|
320
|
+
|
|
321
|
+
# trim labels to column width
|
|
322
|
+
column_labels = ["unit"]
|
|
323
|
+
labels = [f'{step}/{index}' for step, index in nodes]
|
|
324
|
+
if labels:
|
|
325
|
+
column_width = min([column_width, max([len(label) for label in labels])])
|
|
326
|
+
|
|
327
|
+
for label in labels:
|
|
328
|
+
column_labels.append(truncate_text(label, column_width).center(column_width))
|
|
329
|
+
|
|
330
|
+
if formatted:
|
|
331
|
+
none_value = "---".center(column_width)
|
|
332
|
+
else:
|
|
333
|
+
none_value = None
|
|
334
|
+
|
|
335
|
+
data = []
|
|
336
|
+
for metric in row_labels:
|
|
337
|
+
row = [self.get(metric, field="unit") or ""]
|
|
338
|
+
for step, index in nodes:
|
|
339
|
+
value = self.get(metric, step=step, index=index)
|
|
340
|
+
if value is None:
|
|
341
|
+
value = none_value
|
|
342
|
+
else:
|
|
343
|
+
if formatted:
|
|
344
|
+
value = self.get_formatted_metric(metric, step=step, index=index)
|
|
345
|
+
value = value.center(column_width)
|
|
346
|
+
else:
|
|
347
|
+
value = self.get(metric, step=step, index=index)
|
|
348
|
+
row.append(value)
|
|
349
|
+
data.append(row)
|
|
350
|
+
|
|
351
|
+
return DataFrame(data, row_labels, column_labels)
|
|
352
|
+
|
|
353
|
+
def summary(self,
|
|
354
|
+
headers: List[Tuple[str, str]],
|
|
355
|
+
nodes: List[Tuple[str, str]] = None,
|
|
356
|
+
column_width: int = 15,
|
|
357
|
+
fd: TextIO = None) -> None:
|
|
358
|
+
'''
|
|
359
|
+
Prints a formatted summary of metrics to a file descriptor.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
headers (List[Tuple[str, str]]): A list of (title, value) tuples
|
|
363
|
+
to print in the header section of the summary.
|
|
364
|
+
nodes (List[Tuple[str, str]], optional): A list of (step, index)
|
|
365
|
+
tuples to include. Defaults to all nodes.
|
|
366
|
+
column_width (int, optional): The width for each column in the table.
|
|
367
|
+
Defaults to 15.
|
|
368
|
+
fd (TextIO, optional): The file descriptor to write to.
|
|
369
|
+
Defaults to `sys.stdout`.
|
|
370
|
+
'''
|
|
371
|
+
header = []
|
|
372
|
+
headers.insert(0, ("SUMMARY", None))
|
|
373
|
+
if headers:
|
|
374
|
+
max_header = max([len(title) for title, _ in headers])
|
|
375
|
+
for title, value in headers:
|
|
376
|
+
if value is None:
|
|
377
|
+
header.append(f"{title:<{max_header}} :")
|
|
378
|
+
else:
|
|
379
|
+
header.append(f"{title:<{max_header}} : {value}")
|
|
380
|
+
|
|
381
|
+
max_line_width = max(4 * column_width, int(0.95*shutil.get_terminal_size().columns))
|
|
382
|
+
data = self.summary_table(nodes=nodes, column_width=column_width)
|
|
383
|
+
|
|
384
|
+
if not fd:
|
|
385
|
+
fd = sys.stdout
|
|
386
|
+
|
|
387
|
+
print("-" * max_line_width, file=fd)
|
|
388
|
+
print("\n".join(header), file=fd)
|
|
389
|
+
print(file=fd)
|
|
390
|
+
if data.empty:
|
|
391
|
+
print(" No metrics to display!", file=fd)
|
|
392
|
+
else:
|
|
393
|
+
print(data.to_string(line_width=max_line_width, col_space=3), file=fd)
|
|
394
|
+
print("-" * max_line_width, file=fd)
|
|
395
|
+
|
|
396
|
+
@classmethod
|
|
397
|
+
def _getdict_type(cls) -> str:
|
|
398
|
+
"""
|
|
399
|
+
Returns the metadata type for `getdict` serialization.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
return MetricSchema.__name__
|
|
403
|
+
|
|
208
404
|
|
|
209
405
|
class MetricSchemaTmp(MetricSchema):
|
|
406
|
+
'''
|
|
407
|
+
A temporary schema containing a wide range of common ASIC and FPGA metrics.
|
|
408
|
+
|
|
409
|
+
This class extends the base `MetricSchema` with numerous metrics related to
|
|
410
|
+
design rules, area, timing, power, and physical implementation details.
|
|
411
|
+
'''
|
|
412
|
+
|
|
210
413
|
def __init__(self):
|
|
414
|
+
'''Initializes the MetricSchemaTmp.'''
|
|
211
415
|
super().__init__()
|
|
212
416
|
|
|
213
417
|
schema_metric_tmp(self)
|
|
@@ -217,6 +421,15 @@ class MetricSchemaTmp(MetricSchema):
|
|
|
217
421
|
# Metrics to Track
|
|
218
422
|
###########################################################################
|
|
219
423
|
def schema_metric_tmp(schema):
|
|
424
|
+
'''
|
|
425
|
+
Defines a temporary, extended set of metrics in the provided schema.
|
|
426
|
+
|
|
427
|
+
This function populates a schema with a comprehensive list of metrics
|
|
428
|
+
commonly used in ASIC and FPGA design flows.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
schema (Schema): The schema object to configure.
|
|
432
|
+
'''
|
|
220
433
|
schema = EditableSchema(schema)
|
|
221
434
|
|
|
222
435
|
metrics = {'drvs': 'design rule violations',
|