librelane 2.4.0.dev0__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.

Potentially problematic release.


This version of librelane might be problematic. Click here for more details.

Files changed (166) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +470 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +61 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +245 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +402 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +117 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +158 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +722 -0
  27. librelane/container.py +264 -0
  28. librelane/env_info.py +306 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +330 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +985 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/logging/__init__.py +40 -0
  52. librelane/logging/logger.py +323 -0
  53. librelane/open_pdks_rev +1 -0
  54. librelane/plugins.py +21 -0
  55. librelane/py.typed +0 -0
  56. librelane/scripts/base.sdc +80 -0
  57. librelane/scripts/klayout/Readme.md +2 -0
  58. librelane/scripts/klayout/open_design.py +63 -0
  59. librelane/scripts/klayout/render.py +121 -0
  60. librelane/scripts/klayout/stream_out.py +176 -0
  61. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  62. librelane/scripts/klayout/xor.drc +120 -0
  63. librelane/scripts/magic/Readme.md +1 -0
  64. librelane/scripts/magic/common/read.tcl +114 -0
  65. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  66. librelane/scripts/magic/def/mag.tcl +19 -0
  67. librelane/scripts/magic/def/mag_gds.tcl +81 -0
  68. librelane/scripts/magic/drc.tcl +79 -0
  69. librelane/scripts/magic/extract_spice.tcl +98 -0
  70. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  71. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  72. librelane/scripts/magic/gds/extras_mag.tcl +47 -0
  73. librelane/scripts/magic/gds/mag_with_pointers.tcl +32 -0
  74. librelane/scripts/magic/get_bbox.tcl +11 -0
  75. librelane/scripts/magic/lef/extras_maglef.tcl +63 -0
  76. librelane/scripts/magic/lef/maglef.tcl +27 -0
  77. librelane/scripts/magic/lef.tcl +57 -0
  78. librelane/scripts/magic/open.tcl +28 -0
  79. librelane/scripts/magic/wrapper.tcl +19 -0
  80. librelane/scripts/netgen/setup.tcl +28 -0
  81. librelane/scripts/odbpy/apply_def_template.py +49 -0
  82. librelane/scripts/odbpy/cell_frequency.py +107 -0
  83. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  84. librelane/scripts/odbpy/contextualize.py +109 -0
  85. librelane/scripts/odbpy/defutil.py +574 -0
  86. librelane/scripts/odbpy/diodes.py +373 -0
  87. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  88. librelane/scripts/odbpy/exception_codes.py +17 -0
  89. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  90. librelane/scripts/odbpy/io_place.py +482 -0
  91. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  92. librelane/scripts/odbpy/lefutil.py +97 -0
  93. librelane/scripts/odbpy/placers.py +162 -0
  94. librelane/scripts/odbpy/power_utils.py +395 -0
  95. librelane/scripts/odbpy/random_place.py +57 -0
  96. librelane/scripts/odbpy/reader.py +246 -0
  97. librelane/scripts/odbpy/remove_buffers.py +173 -0
  98. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  99. librelane/scripts/odbpy/wire_lengths.py +93 -0
  100. librelane/scripts/openroad/antenna_check.tcl +20 -0
  101. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  102. librelane/scripts/openroad/basic_mp.tcl +24 -0
  103. librelane/scripts/openroad/buffer_list.tcl +10 -0
  104. librelane/scripts/openroad/common/dpl.tcl +24 -0
  105. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  106. librelane/scripts/openroad/common/grt.tcl +32 -0
  107. librelane/scripts/openroad/common/io.tcl +476 -0
  108. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  109. librelane/scripts/openroad/common/resizer.tcl +103 -0
  110. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  111. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  112. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  113. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  114. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  115. librelane/scripts/openroad/cts.tcl +80 -0
  116. librelane/scripts/openroad/cut_rows.tcl +24 -0
  117. librelane/scripts/openroad/dpl.tcl +24 -0
  118. librelane/scripts/openroad/drt.tcl +37 -0
  119. librelane/scripts/openroad/fill.tcl +30 -0
  120. librelane/scripts/openroad/floorplan.tcl +145 -0
  121. librelane/scripts/openroad/gpl.tcl +88 -0
  122. librelane/scripts/openroad/grt.tcl +30 -0
  123. librelane/scripts/openroad/gui.tcl +15 -0
  124. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  125. librelane/scripts/openroad/ioplacer.tcl +67 -0
  126. librelane/scripts/openroad/irdrop.tcl +51 -0
  127. librelane/scripts/openroad/pdn.tcl +52 -0
  128. librelane/scripts/openroad/rcx.tcl +32 -0
  129. librelane/scripts/openroad/repair_design.tcl +70 -0
  130. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  131. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  132. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  133. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  134. librelane/scripts/openroad/sta/corner.tcl +393 -0
  135. librelane/scripts/openroad/tapcell.tcl +25 -0
  136. librelane/scripts/openroad/write_views.tcl +27 -0
  137. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  138. librelane/scripts/pyosys/json_header.py +84 -0
  139. librelane/scripts/pyosys/synthesize.py +493 -0
  140. librelane/scripts/pyosys/ys_common.py +153 -0
  141. librelane/scripts/tclsh/hello.tcl +1 -0
  142. librelane/state/__init__.py +24 -0
  143. librelane/state/__main__.py +61 -0
  144. librelane/state/design_format.py +180 -0
  145. librelane/state/state.py +351 -0
  146. librelane/steps/__init__.py +61 -0
  147. librelane/steps/__main__.py +511 -0
  148. librelane/steps/checker.py +637 -0
  149. librelane/steps/common_variables.py +340 -0
  150. librelane/steps/cvc_rv.py +169 -0
  151. librelane/steps/klayout.py +509 -0
  152. librelane/steps/magic.py +566 -0
  153. librelane/steps/misc.py +160 -0
  154. librelane/steps/netgen.py +253 -0
  155. librelane/steps/odb.py +955 -0
  156. librelane/steps/openroad.py +2433 -0
  157. librelane/steps/openroad_alerts.py +102 -0
  158. librelane/steps/pyosys.py +629 -0
  159. librelane/steps/step.py +1547 -0
  160. librelane/steps/tclstep.py +288 -0
  161. librelane/steps/verilator.py +222 -0
  162. librelane/steps/yosys.py +371 -0
  163. librelane-2.4.0.dev0.dist-info/METADATA +151 -0
  164. librelane-2.4.0.dev0.dist-info/RECORD +166 -0
  165. librelane-2.4.0.dev0.dist-info/WHEEL +4 -0
  166. librelane-2.4.0.dev0.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,2433 @@
1
+ # Copyright 2023 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import io
15
+ import os
16
+ import re
17
+ import json
18
+ import tempfile
19
+ import functools
20
+ import subprocess
21
+ from enum import Enum
22
+ from math import inf
23
+ from glob import glob
24
+ from decimal import Decimal
25
+ from base64 import b64encode
26
+ from abc import abstractmethod
27
+ from dataclasses import dataclass
28
+ from concurrent.futures import Future, ThreadPoolExecutor
29
+ from typing import (
30
+ Any,
31
+ List,
32
+ Dict,
33
+ Literal,
34
+ Set,
35
+ Tuple,
36
+ Optional,
37
+ Union,
38
+ )
39
+
40
+
41
+ import yaml
42
+ import rich
43
+ import rich.table
44
+
45
+ from .step import (
46
+ CompositeStep,
47
+ DefaultOutputProcessor,
48
+ StepError,
49
+ ViewsUpdate,
50
+ MetricsUpdate,
51
+ Step,
52
+ StepException,
53
+ )
54
+ from .openroad_alerts import (
55
+ OpenROADAlert,
56
+ OpenROADOutputProcessor,
57
+ )
58
+ from .tclstep import TclStep
59
+ from .common_variables import (
60
+ io_layer_variables,
61
+ pdn_variables,
62
+ rsz_variables,
63
+ dpl_variables,
64
+ grt_variables,
65
+ routing_layer_variables,
66
+ )
67
+
68
+ from ..config import Variable, Macro
69
+ from ..config.flow import option_variables
70
+ from ..state import State, DesignFormat
71
+ from ..logging import debug, info, verbose, console, options
72
+ from ..common import (
73
+ Path,
74
+ TclUtils,
75
+ get_script_dir,
76
+ mkdirp,
77
+ aggregate_metrics,
78
+ process_list_file,
79
+ _get_process_limit,
80
+ )
81
+
82
+ EXAMPLE_INPUT = """
83
+ li1 X 0.23 0.46
84
+ li1 Y 0.17 0.34
85
+ met1 X 0.17 0.34
86
+ met1 Y 0.17 0.34
87
+ met2 X 0.23 0.46
88
+ met2 Y 0.23 0.46
89
+ met3 X 0.34 0.68
90
+ met3 Y 0.34 0.68
91
+ met4 X 0.46 0.92
92
+ met4 Y 0.46 0.92
93
+ met5 X 1.70 3.40
94
+ met5 Y 1.70 3.40
95
+ """
96
+
97
+
98
+ def old_to_new_tracks(old_tracks: str) -> str:
99
+ """
100
+ >>> old_to_new_tracks(EXAMPLE_INPUT)
101
+ 'make_tracks li1 -x_offset 0.23 -x_pitch 0.46 -y_offset 0.17 -y_pitch 0.34\\nmake_tracks met1 -x_offset 0.17 -x_pitch 0.34 -y_offset 0.17 -y_pitch 0.34\\nmake_tracks met2 -x_offset 0.23 -x_pitch 0.46 -y_offset 0.23 -y_pitch 0.46\\nmake_tracks met3 -x_offset 0.34 -x_pitch 0.68 -y_offset 0.34 -y_pitch 0.68\\nmake_tracks met4 -x_offset 0.46 -x_pitch 0.92 -y_offset 0.46 -y_pitch 0.92\\nmake_tracks met5 -x_offset 1.70 -x_pitch 3.40 -y_offset 1.70 -y_pitch 3.40\\n'
102
+ """
103
+ layers: Dict[str, Dict[str, Tuple[str, str]]] = {}
104
+
105
+ for line in old_tracks.splitlines():
106
+ if line.strip() == "":
107
+ continue
108
+ layer, cardinal, offset, pitch = line.split()
109
+ layers[layer] = layers.get(layer) or {}
110
+ layers[layer][cardinal] = (offset, pitch)
111
+
112
+ final_str = ""
113
+ for layer, data in layers.items():
114
+ x_offset, x_pitch = data["X"]
115
+ y_offset, y_pitch = data["Y"]
116
+ final_str += f"make_tracks {layer} -x_offset {x_offset} -x_pitch {x_pitch} -y_offset {y_offset} -y_pitch {y_pitch}\n"
117
+
118
+ return final_str
119
+
120
+
121
+ def pdn_macro_migrator(x):
122
+ if not isinstance(x, str):
123
+ return x
124
+ if "," in x:
125
+ return [el.strip() for el in x.split(",")]
126
+ else:
127
+ return [x.strip()]
128
+
129
+
130
+ @Step.factory.register()
131
+ class CheckSDCFiles(Step):
132
+ """
133
+ Checks that the two variables used for SDC files by OpenROAD steps,
134
+ namely, ``PNR_SDC_FILE`` and ``SIGNOFF_SDC_FILE``, are explicitly set to
135
+ valid paths by the users, and emits a warning that the fallback will be
136
+ utilized otherwise.
137
+ """
138
+
139
+ id = "OpenROAD.CheckSDCFiles"
140
+ name = "Check SDC Files"
141
+ inputs = []
142
+ outputs = []
143
+
144
+ config_vars = [
145
+ Variable(
146
+ "PNR_SDC_FILE",
147
+ Optional[Path],
148
+ "Specifies the SDC file used during all implementation (PnR) steps",
149
+ ),
150
+ Variable(
151
+ "SIGNOFF_SDC_FILE",
152
+ Optional[Path],
153
+ "Specifies the SDC file for STA during signoff",
154
+ ),
155
+ ]
156
+
157
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
158
+ default_sdc_file = [
159
+ var for var in option_variables if var.name == "FALLBACK_SDC_FILE"
160
+ ][0]
161
+ assert default_sdc_file is not None
162
+
163
+ is_generic_fallback = default_sdc_file.default
164
+ fallback_descriptor = "generic" if is_generic_fallback else "user-defined"
165
+ if self.config["PNR_SDC_FILE"] is None:
166
+ self.warn(
167
+ f"'PNR_SDC_FILE' is not defined. Using {fallback_descriptor} fallback SDC for OpenROAD PnR steps."
168
+ )
169
+ if self.config["SIGNOFF_SDC_FILE"] is None:
170
+ self.warn(
171
+ f"'SIGNOFF_SDC_FILE' is not defined. Using {fallback_descriptor} fallback SDC for OpenROAD PnR steps."
172
+ )
173
+ return {}, {}
174
+
175
+
176
+ class OpenROADStep(TclStep):
177
+ inputs = [DesignFormat.ODB]
178
+ outputs = [
179
+ DesignFormat.ODB,
180
+ DesignFormat.DEF,
181
+ DesignFormat.SDC,
182
+ DesignFormat.NETLIST,
183
+ DesignFormat.POWERED_NETLIST,
184
+ ]
185
+
186
+ output_processors = [OpenROADOutputProcessor, DefaultOutputProcessor]
187
+
188
+ config_vars = [
189
+ Variable(
190
+ "PDN_CONNECT_MACROS_TO_GRID",
191
+ bool,
192
+ "Enables the connection of macros to the top level power grid.",
193
+ default=True,
194
+ deprecated_names=["FP_PDN_ENABLE_MACROS_GRID"],
195
+ ),
196
+ Variable(
197
+ "PDN_MACRO_CONNECTIONS",
198
+ Optional[List[str]],
199
+ "Specifies explicit power connections of internal macros to the top level power grid, in the format: regex matching macro instance names, power domain vdd and ground net names, and macro vdd and ground pin names `<instance_name_rx> <vdd_net> <gnd_net> <vdd_pin> <gnd_pin>`.",
200
+ deprecated_names=[("FP_PDN_MACRO_HOOKS", pdn_macro_migrator)],
201
+ ),
202
+ Variable(
203
+ "PDN_ENABLE_GLOBAL_CONNECTIONS",
204
+ bool,
205
+ "Enables the creation of global connections in PDN generation.",
206
+ default=True,
207
+ deprecated_names=["FP_PDN_ENABLE_GLOBAL_CONNECTIONS"],
208
+ ),
209
+ Variable(
210
+ "PNR_SDC_FILE",
211
+ Optional[Path],
212
+ "Specifies the SDC file used during all implementation (PnR) steps",
213
+ ),
214
+ Variable(
215
+ "FP_DEF_TEMPLATE",
216
+ Optional[Path],
217
+ "Points to the DEF file to be used as a template.",
218
+ ),
219
+ ]
220
+
221
+ @abstractmethod
222
+ def get_script_path(self) -> str:
223
+ pass
224
+
225
+ def on_alert(self, alert: OpenROADAlert) -> OpenROADAlert:
226
+ if alert.code in [
227
+ "ORD-0039", # .openroad ignored with -python
228
+ "ODB-0220", # lef parsing/NOWIREEXTENSIONATPIN statement is obsolete in version 5.6 or later.
229
+ "STA-1256", # table template \w+ not found
230
+ ]:
231
+ return alert
232
+ if alert.cls == "error":
233
+ self.err(str(alert), extra={"key": alert.code})
234
+ elif alert.cls == "warning":
235
+ self.warn(str(alert), extra={"key": alert.code})
236
+ return alert
237
+
238
+ def prepare_env(self, env: dict, state: State) -> dict:
239
+ env = super().prepare_env(env, state)
240
+
241
+ lib_list = self.toolbox.filter_views(self.config, self.config["LIB"])
242
+ lib_list += self.toolbox.get_macro_views(self.config, DesignFormat.LIB)
243
+
244
+ env["_SDC_IN"] = self.config["PNR_SDC_FILE"] or self.config["FALLBACK_SDC_FILE"]
245
+ env["_PNR_LIBS"] = TclStep.value_to_tcl(lib_list)
246
+ env["_MACRO_LIBS"] = TclStep.value_to_tcl(
247
+ self.toolbox.get_macro_views(self.config, DesignFormat.LIB)
248
+ )
249
+
250
+ excluded_cells: Set[str] = set(self.config["EXTRA_EXCLUDED_CELLS"] or [])
251
+ excluded_cells.update(process_list_file(self.config["PNR_EXCLUDED_CELL_FILE"]))
252
+ env["_PNR_EXCLUDED_CELLS"] = TclUtils.join(excluded_cells)
253
+
254
+ return env
255
+
256
+ def run(self, state_in, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
257
+ """
258
+ The `run()` override for the OpenROADStep class handles two things:
259
+
260
+ 1. Before the `super()` call: It creates a version of the lib file
261
+ minus cells that are known bad (i.e. those that fail DRC) and pass it on
262
+ in the environment variable `_PNR_LIBS`.
263
+
264
+ 2. After the `super()` call: Processes the `or_metrics_out.json` file and
265
+ updates the State's `metrics` property with any new metrics in that object.
266
+ """
267
+ kwargs, env = self.extract_env(kwargs)
268
+ env = self.prepare_env(env, state_in)
269
+
270
+ check = False
271
+ if "check" in kwargs:
272
+ check = kwargs.pop("check")
273
+
274
+ command = self.get_command()
275
+
276
+ subprocess_result = self.run_subprocess(
277
+ command,
278
+ env=env,
279
+ check=check,
280
+ **kwargs,
281
+ )
282
+
283
+ generated_metrics = subprocess_result["generated_metrics"]
284
+
285
+ views_updates: ViewsUpdate = {}
286
+ for output in self.outputs:
287
+ if output.value.multiple:
288
+ # Too step-specific.
289
+ continue
290
+ path = Path(env[f"SAVE_{output.name}"])
291
+ if not path.exists():
292
+ continue
293
+ views_updates[output] = path
294
+
295
+ # 1. Parse warnings and errors
296
+ alerts = subprocess_result["openroad_alerts"]
297
+ if subprocess_result["returncode"] != 0:
298
+ error_strings = [str(alert) for alert in alerts if alert.cls == "error"]
299
+ if len(error_strings):
300
+ error_string = "\n".join(error_strings)
301
+ raise StepError(
302
+ f"{self.id} failed with the following errors:\n{error_string}"
303
+ )
304
+ else:
305
+ raise StepException(
306
+ f"{self.id} failed unexpectedly. Please check the logs and file an issue."
307
+ )
308
+ # 2. Metrics
309
+ metrics_path = os.path.join(self.step_dir, "or_metrics_out.json")
310
+ if os.path.exists(metrics_path):
311
+ or_metrics_out = json.loads(open(metrics_path).read(), parse_float=Decimal)
312
+ for key, value in or_metrics_out.items():
313
+ if value == "Infinity":
314
+ or_metrics_out[key] = inf
315
+ elif value == "-Infinity":
316
+ or_metrics_out[key] = -inf
317
+ generated_metrics.update(or_metrics_out)
318
+
319
+ metric_updates_with_aggregates = aggregate_metrics(generated_metrics)
320
+
321
+ return views_updates, metric_updates_with_aggregates
322
+
323
+ def get_command(self) -> List[str]:
324
+ metrics_path = os.path.join(self.step_dir, "or_metrics_out.json")
325
+ return [
326
+ "openroad",
327
+ "-exit",
328
+ "-no_splash",
329
+ "-metrics",
330
+ metrics_path,
331
+ self.get_script_path(),
332
+ ]
333
+
334
+ def layout_preview(self) -> Optional[str]:
335
+ if self.state_out is None:
336
+ return None
337
+
338
+ state_in = self.state_in.result()
339
+ if self.state_out.get("def") == state_in.get("def"):
340
+ return None
341
+
342
+ if image := self.toolbox.render_png(self.config, self.state_out):
343
+ image_encoded = b64encode(image).decode("utf8")
344
+ return f'<img src="data:image/png;base64,{image_encoded}" />'
345
+
346
+ return None
347
+
348
+
349
+ @Step.factory.register()
350
+ class STAMidPNR(OpenROADStep):
351
+ """
352
+ Performs `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
353
+ using OpenROAD on an OpenROAD database, mid-PnR, with estimated values for
354
+ parasitics.
355
+ """
356
+
357
+ id = "OpenROAD.STAMidPNR"
358
+ name = "STA (Mid-PnR)"
359
+ long_name = "Static Timing Analysis (Mid-PnR)"
360
+
361
+ inputs = [DesignFormat.ODB]
362
+ outputs = []
363
+
364
+ def get_script_path(self):
365
+ return os.path.join(get_script_dir(), "openroad", "sta", "corner.tcl")
366
+
367
+
368
+ class OpenSTAStep(OpenROADStep):
369
+ @dataclass(frozen=True)
370
+ class CornerFileList:
371
+ libs: Tuple[str, ...]
372
+ netlists: Tuple[str, ...]
373
+ spefs: Tuple[Tuple[str, str], ...]
374
+ extra_spefs_backcompat: Optional[Tuple[Tuple[str, str], ...]] = None
375
+ current_corner_spef: Optional[str] = None
376
+
377
+ def set_env(self, env: Dict[str, Any]):
378
+ env["_CURRENT_CORNER_LIBS"] = TclStep.value_to_tcl(self.libs)
379
+ env["_CURRENT_CORNER_NETLISTS"] = TclStep.value_to_tcl(self.netlists)
380
+ env["_CURRENT_CORNER_SPEFS"] = TclStep.value_to_tcl(self.spefs)
381
+ if self.extra_spefs_backcompat is not None:
382
+ env["_CURRENT_CORNER_EXTRA_SPEFS_BACKCOMPAT"] = TclStep.value_to_tcl(
383
+ self.extra_spefs_backcompat
384
+ )
385
+ if self.current_corner_spef is not None:
386
+ env["_CURRENT_SPEF_BY_CORNER"] = self.current_corner_spef
387
+
388
+ inputs = [DesignFormat.NETLIST]
389
+
390
+ def get_command(self) -> List[str]:
391
+ return ["sta", "-no_splash", "-exit", self.get_script_path()]
392
+
393
+ def layout_preview(self) -> Optional[str]:
394
+ return None
395
+
396
+ def _get_corner_files(
397
+ self: Step,
398
+ timing_corner: Optional[str] = None,
399
+ prioritize_nl: bool = False,
400
+ ) -> Tuple[str, CornerFileList]:
401
+ (
402
+ timing_corner,
403
+ libs,
404
+ netlists,
405
+ spefs,
406
+ ) = self.toolbox.get_timing_files_categorized(
407
+ self.config,
408
+ prioritize_nl=prioritize_nl,
409
+ timing_corner=timing_corner,
410
+ )
411
+ state_in = self.state_in.result()
412
+
413
+ name = timing_corner
414
+ current_corner_spef = None
415
+ input_spef_dict = state_in[DesignFormat.SPEF]
416
+ if input_spef_dict is not None:
417
+ if not isinstance(input_spef_dict, dict):
418
+ raise StepException(
419
+ "Malformed input state: value for 'spef' is not a dictionary"
420
+ )
421
+
422
+ current_corner_spefs = self.toolbox.filter_views(
423
+ self.config, input_spef_dict, timing_corner
424
+ )
425
+ if len(current_corner_spefs) < 1:
426
+ raise StepException(
427
+ f"No SPEF file compatible with corner '{timing_corner}' found."
428
+ )
429
+ elif len(current_corner_spefs) > 1:
430
+ self.warn(
431
+ f"Multiple SPEF files compatible with corner '{timing_corner}' found. The first one encountered will be used."
432
+ )
433
+ current_corner_spef = str(current_corner_spefs[0])
434
+
435
+ extra_spefs_backcompat_raw = None
436
+ if extra_spef_list := self.config.get("EXTRA_SPEFS"):
437
+ extra_spefs_backcompat_raw = []
438
+ self.warn(
439
+ "The configuration variable 'EXTRA_SPEFS' is deprecated. It is recommended to use the new 'MACROS' configuration variable."
440
+ )
441
+ if len(extra_spef_list) % 4 != 0:
442
+ raise StepException(
443
+ "Invalid value for 'EXTRA_SPEFS': Element count not divisible by four. It is recommended that you migrate your configuration to use the new 'MACROS' configuration variable."
444
+ )
445
+ for i in range(len(extra_spef_list) // 4):
446
+ start = i * 4
447
+ module, min, nom, max = (
448
+ extra_spef_list[start],
449
+ extra_spef_list[start + 1],
450
+ extra_spef_list[start + 2],
451
+ extra_spef_list[start + 3],
452
+ )
453
+ mapping = {
454
+ "min_*": [min],
455
+ "nom_*": [nom],
456
+ "max_*": [max],
457
+ }
458
+ spef = str(
459
+ self.toolbox.filter_views(
460
+ self.config, mapping, timing_corner=timing_corner
461
+ )[0]
462
+ )
463
+ extra_spefs_backcompat_raw.append((module, spef))
464
+
465
+ extra_spefs_backcompat = None
466
+ if extra_spefs_backcompat_raw is not None:
467
+ extra_spefs_backcompat = tuple(extra_spefs_backcompat_raw)
468
+
469
+ return (
470
+ name,
471
+ OpenSTAStep.CornerFileList(
472
+ libs=tuple([str(lib) for lib in libs]),
473
+ netlists=tuple([str(netlist) for netlist in netlists]),
474
+ spefs=tuple([(pair[0], str(pair[1])) for pair in spefs]),
475
+ extra_spefs_backcompat=extra_spefs_backcompat,
476
+ current_corner_spef=current_corner_spef,
477
+ ),
478
+ )
479
+
480
+
481
+ @Step.factory.register()
482
+ class CheckMacroInstances(OpenSTAStep):
483
+ """
484
+ Checks if all macro instances declared in the configuration are, in fact,
485
+ in the design, emitting an error otherwise.
486
+
487
+ Nested macros (macros within macros) are supported provided netlist views
488
+ are available for the macro.
489
+ """
490
+
491
+ id = "OpenROAD.CheckMacroInstances"
492
+ name = "Check Macro Instances"
493
+ outputs = []
494
+
495
+ config_vars = OpenROADStep.config_vars
496
+
497
+ def get_script_path(self):
498
+ return os.path.join(
499
+ get_script_dir(), "openroad", "sta", "check_macro_instances.tcl"
500
+ )
501
+
502
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
503
+ kwargs, env = self.extract_env(kwargs)
504
+ macros: Optional[Dict[str, Macro]] = self.config["MACROS"]
505
+ if macros is None:
506
+ info("No macros found, skipping instance check…")
507
+ return {}, {}
508
+
509
+ macro_instance_pairs = []
510
+ for macro_name, data in macros.items():
511
+ for instance_name in data.instances:
512
+ macro_instance_pairs.append(instance_name)
513
+ macro_instance_pairs.append(macro_name)
514
+
515
+ env["_check_macro_instances"] = TclUtils.join(macro_instance_pairs)
516
+
517
+ corner_name, file_list = self._get_corner_files(prioritize_nl=True)
518
+ file_list.set_env(env)
519
+ env["_CURRENT_CORNER_NAME"] = corner_name
520
+
521
+ return super().run(state_in, env=env, **kwargs)
522
+
523
+
524
+ class MultiCornerSTA(OpenSTAStep):
525
+ outputs = [DesignFormat.SDF, DesignFormat.SDC]
526
+
527
+ config_vars = OpenSTAStep.config_vars + [
528
+ Variable(
529
+ "STA_MACRO_PRIORITIZE_NL",
530
+ bool,
531
+ "Prioritize the use of Netlists + SPEF files over LIB files if available for Macros. Useful if extraction was done using OpenROAD, where SPEF files are far more accurate.",
532
+ default=True,
533
+ ),
534
+ Variable(
535
+ "STA_MAX_VIOLATOR_COUNT",
536
+ Optional[int],
537
+ "Maximum number of violators to list in violator_list.rpt",
538
+ ),
539
+ Variable(
540
+ "EXTRA_SPEFS",
541
+ Optional[List[Union[str, Path]]],
542
+ "A variable that only exists for backwards compatibility with LibreLane <2.0.0 and should not be used by new designs.",
543
+ ),
544
+ Variable(
545
+ "STA_THREADS",
546
+ Optional[int],
547
+ "The maximum number of STA corners to run in parallel. If unset, this will be equal to your machine's thread count.",
548
+ ),
549
+ ]
550
+
551
+ def get_script_path(self):
552
+ return os.path.join(get_script_dir(), "openroad", "sta", "corner.tcl")
553
+
554
+ def run_corner(
555
+ self,
556
+ state_in: State,
557
+ current_env: Dict[str, Any],
558
+ corner: str,
559
+ corner_dir: str,
560
+ ) -> Dict[str, Any]:
561
+ info(f"Starting STA for the {corner} timing corner…")
562
+ current_env["_CURRENT_CORNER_NAME"] = corner
563
+ log_path = os.path.join(corner_dir, "sta.log")
564
+
565
+ try:
566
+ subprocess_result = self.run_subprocess(
567
+ self.get_command(),
568
+ log_to=log_path,
569
+ env=current_env,
570
+ silent=True,
571
+ report_dir=corner_dir,
572
+ )
573
+
574
+ generated_metrics = subprocess_result["generated_metrics"]
575
+
576
+ info(f"Finished STA for the {corner} timing corner.")
577
+ except subprocess.CalledProcessError as e:
578
+ self.err(f"Failed STA for the {corner} timing corner:")
579
+ raise e
580
+
581
+ return generated_metrics
582
+
583
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
584
+ kwargs, env = self.extract_env(kwargs)
585
+ env = self.prepare_env(env, state_in)
586
+
587
+ tpe = ThreadPoolExecutor(
588
+ max_workers=self.config["STA_THREADS"] or _get_process_limit()
589
+ )
590
+
591
+ futures: Dict[str, Future[MetricsUpdate]] = {}
592
+ files_so_far: Dict[OpenSTAStep.CornerFileList, str] = {}
593
+ corners_used: Set[str] = set()
594
+ for corner in self.config["STA_CORNERS"]:
595
+ _, file_list = self._get_corner_files(
596
+ corner, prioritize_nl=self.config["STA_MACRO_PRIORITIZE_NL"]
597
+ )
598
+ if previous := files_so_far.get(file_list):
599
+ info(
600
+ f"Skipping corner {corner} for STA (identical to {previous} at this stage)…"
601
+ )
602
+ continue
603
+ files_so_far[file_list] = corner
604
+ corners_used.add(corner)
605
+
606
+ current_env = env.copy()
607
+ file_list.set_env(current_env)
608
+
609
+ corner_dir = os.path.join(self.step_dir, corner)
610
+ mkdirp(corner_dir)
611
+
612
+ futures[corner] = tpe.submit(
613
+ self.run_corner,
614
+ state_in,
615
+ current_env,
616
+ corner,
617
+ corner_dir,
618
+ )
619
+
620
+ metrics_updates: MetricsUpdate = {}
621
+ for corner, updates_future in futures.items():
622
+ metrics_updates.update(updates_future.result())
623
+
624
+ metric_updates_with_aggregates = aggregate_metrics(metrics_updates)
625
+
626
+ def format_count(count: Optional[Union[int, float, Decimal]]) -> str:
627
+ if count is None:
628
+ return "[gray]?"
629
+ count = int(count)
630
+ if count == 0:
631
+ return f"[green]{count}"
632
+ else:
633
+ return f"[red]{count}"
634
+
635
+ def format_slack(slack: Optional[Union[int, float, Decimal]]) -> str:
636
+ if slack is None:
637
+ return "[gray]?"
638
+ if slack == float(inf):
639
+ return "[gray]N/A"
640
+ slack = round(float(slack), 4)
641
+ formatted_slack = f"{slack:.4f}"
642
+ if slack < 0:
643
+ return f"[red]{formatted_slack}"
644
+ else:
645
+ return f"[green]{formatted_slack}"
646
+
647
+ table = rich.table.Table()
648
+ table.add_column("Corner/Group", width=20)
649
+ table.add_column("Hold Worst Slack")
650
+ table.add_column("Reg to Reg Paths")
651
+ table.add_column("Hold TNS")
652
+ table.add_column("Hold Vio Count")
653
+ table.add_column("of which reg to reg")
654
+ table.add_column("Setup Worst Slack")
655
+ table.add_column("Reg to Reg Paths")
656
+ table.add_column("Setup TNS")
657
+ table.add_column("Setup Vio Count")
658
+ table.add_column("of which reg to reg")
659
+ table.add_column("Max Cap Violations")
660
+ table.add_column("Max Slew Violations")
661
+ for corner in ["Overall"] + self.config["STA_CORNERS"]:
662
+ modifier = ""
663
+ if corner != "Overall":
664
+ if corner not in corners_used:
665
+ continue
666
+ modifier = f"__corner:{corner}"
667
+ row = [corner]
668
+ for metric in [
669
+ "timing__hold__ws",
670
+ "timing__hold_r2r__ws",
671
+ "timing__hold__tns",
672
+ "timing__hold_vio__count",
673
+ "timing__hold_r2r_vio__count",
674
+ "timing__setup__ws",
675
+ "timing__setup_r2r__ws",
676
+ "timing__setup__tns",
677
+ "timing__setup_vio__count",
678
+ "timing__setup_r2r_vio__count",
679
+ "design__max_cap_violation__count",
680
+ "design__max_slew_violation__count",
681
+ ]:
682
+ formatter = format_count if metric.endswith("count") else format_slack
683
+ row.append(
684
+ formatter(metric_updates_with_aggregates.get(f"{metric}{modifier}"))
685
+ )
686
+ table.add_row(*row)
687
+
688
+ if not options.get_condensed_mode():
689
+ console.print(table)
690
+ file_console = rich.console.Console(
691
+ file=open(os.path.join(self.step_dir, "summary.rpt"), "w", encoding="utf8"),
692
+ width=160,
693
+ )
694
+ file_console.print(table)
695
+
696
+ return {}, metric_updates_with_aggregates
697
+
698
+
699
+ @Step.factory.register()
700
+ class STAPrePNR(MultiCornerSTA):
701
+ """
702
+ Performs hierarchical `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
703
+ using OpenSTA on the pre-PnR Verilog netlist, with all available timing information
704
+ for standard cells and macros for multiple corners.
705
+
706
+ If timing information is not available for a Macro, the macro in question
707
+ will be black-boxed.
708
+
709
+ During this step, the special variable `OPENLANE_SDC_IDEAL_CLOCKS` is
710
+ exposed to SDC files with a value of `1`. We encourage PNR SDC files to use
711
+ ideal clocks at this stage based on this variable's existence and value.
712
+ """
713
+
714
+ id = "OpenROAD.STAPrePNR"
715
+ name = "STA (Pre-PnR)"
716
+ long_name = "Static Timing Analysis (Pre-PnR)"
717
+
718
+ def prepare_env(self, env: Dict, state: State) -> Dict:
719
+ env = super().prepare_env(env, state)
720
+ env["OPENLANE_SDC_IDEAL_CLOCKS"] = "1"
721
+ return env
722
+
723
+ def run_corner(
724
+ self, state_in: State, current_env: Dict[str, Any], corner: str, corner_dir: str
725
+ ) -> Dict[str, Any]:
726
+ current_env["_SDF_SAVE_DIR"] = corner_dir
727
+ return super().run_corner(state_in, current_env, corner, corner_dir)
728
+
729
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
730
+ views_updates, metrics_updates = super().run(state_in, **kwargs)
731
+
732
+ sdf_dict = state_in[DesignFormat.SDF] or {}
733
+ if not isinstance(sdf_dict, dict):
734
+ raise StepException(
735
+ "Malformed input state: incoming value for SDF is not a dictionary."
736
+ )
737
+
738
+ sdf_dict = sdf_dict.copy()
739
+
740
+ for corner in self.config["STA_CORNERS"]:
741
+ sdf = os.path.join(
742
+ self.step_dir, corner, f"{self.config['DESIGN_NAME']}__{corner}.sdf"
743
+ )
744
+ if os.path.isfile(sdf):
745
+ sdf_dict[corner] = Path(sdf)
746
+
747
+ views_updates[DesignFormat.SDF] = sdf_dict
748
+
749
+ return views_updates, metrics_updates
750
+
751
+
752
+ @Step.factory.register()
753
+ class STAPostPNR(STAPrePNR):
754
+ """
755
+ Performs multi-corner `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
756
+ using OpenSTA on the post-PnR Verilog netlist, with extracted parasitics for
757
+ both the top-level module and any associated macros.
758
+
759
+ During this step, the special variable `OPENLANE_SDC_IDEAL_CLOCKS` is
760
+ exposed to SDC files with a value of `0`. We encourage PNR SDC files to use
761
+ propagated clocks at this stage based on this variable's existence and value.
762
+ """
763
+
764
+ id = "OpenROAD.STAPostPNR"
765
+ name = "STA (Post-PnR)"
766
+ long_name = "Static Timing Analysis (Post-PnR)"
767
+
768
+ config_vars = STAPrePNR.config_vars + [
769
+ Variable(
770
+ "SIGNOFF_SDC_FILE",
771
+ Optional[Path],
772
+ "Specifies the SDC file for STA during signoff",
773
+ ),
774
+ ]
775
+
776
+ inputs = STAPrePNR.inputs + [DesignFormat.SPEF, DesignFormat.ODB]
777
+ outputs = STAPrePNR.outputs + [DesignFormat.LIB]
778
+
779
+ def prepare_env(self, env: dict, state: State) -> dict:
780
+ env = super().prepare_env(env, state)
781
+ if signoff_sdc_file := self.config["SIGNOFF_SDC_FILE"]:
782
+ env["_SDC_IN"] = signoff_sdc_file
783
+ env["OPENLANE_SDC_IDEAL_CLOCKS"] = "0"
784
+ return env
785
+
786
+ def filter_unannotated_report(
787
+ self,
788
+ corner: str,
789
+ corner_dir: str,
790
+ env: Dict,
791
+ checks_report: str,
792
+ odb_design: str,
793
+ ):
794
+ tech_lefs = self.toolbox.filter_views(self.config, self.config["TECH_LEFS"])
795
+ if len(tech_lefs) != 1:
796
+ raise StepException(
797
+ "Misconfigured SCL: 'TECH_LEFS' must return exactly one Tech LEF for its default timing corner."
798
+ )
799
+
800
+ lefs = ["--input-lef", tech_lefs[0]]
801
+ for lef in self.config["CELL_LEFS"]:
802
+ lefs.append("--input-lef")
803
+ lefs.append(lef)
804
+ if extra_lefs := self.config["EXTRA_LEFS"]:
805
+ for lef in extra_lefs:
806
+ lefs.append("--input-lef")
807
+ lefs.append(lef)
808
+ metrics_path = os.path.join(corner_dir, "filter_unannotated_metrics.json")
809
+ filter_unannotated_cmd = [
810
+ "openroad",
811
+ "-exit",
812
+ "-no_splash",
813
+ "-metrics",
814
+ metrics_path,
815
+ "-python",
816
+ os.path.join(get_script_dir(), "odbpy", "filter_unannotated.py"),
817
+ "--corner",
818
+ corner,
819
+ "--checks-report",
820
+ checks_report,
821
+ odb_design,
822
+ ] + lefs
823
+
824
+ subprocess_result = self.run_subprocess(
825
+ filter_unannotated_cmd,
826
+ log_to=os.path.join(corner_dir, "filter_unannotated.log"),
827
+ env=env,
828
+ silent=True,
829
+ report_dir=corner_dir,
830
+ )
831
+
832
+ generated_metrics = subprocess_result["generated_metrics"]
833
+
834
+ if os.path.exists(metrics_path):
835
+ or_metrics_out = json.loads(open(metrics_path).read())
836
+ generated_metrics.update(or_metrics_out)
837
+
838
+ return generated_metrics
839
+
840
+ def run_corner(
841
+ self,
842
+ state_in: State,
843
+ current_env: Dict[str, Any],
844
+ corner: str,
845
+ corner_dir: str,
846
+ ) -> MetricsUpdate:
847
+ current_env["_LIB_SAVE_DIR"] = corner_dir
848
+ metrics_updates = super().run_corner(state_in, current_env, corner, corner_dir)
849
+ try:
850
+ filter_unannotated_metrics = self.filter_unannotated_report(
851
+ corner=corner,
852
+ checks_report=os.path.join(corner_dir, "checks.rpt"),
853
+ corner_dir=corner_dir,
854
+ env=current_env,
855
+ odb_design=str(state_in[DesignFormat.ODB]),
856
+ )
857
+ except subprocess.CalledProcessError as e:
858
+ self.err(
859
+ f"Failed filtering unannotated nets for the {corner} timing corner."
860
+ )
861
+ raise e
862
+ return {**metrics_updates, **filter_unannotated_metrics}
863
+
864
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
865
+ views_updates, metrics_updates = super().run(state_in, **kwargs)
866
+ lib_dict = state_in[DesignFormat.LIB] or {}
867
+ if not isinstance(lib_dict, dict):
868
+ raise StepException(
869
+ "Malformed input state: value for LIB is not a dictionary."
870
+ )
871
+
872
+ lib_dict.copy()
873
+
874
+ for corner in self.config["STA_CORNERS"]:
875
+ lib = os.path.join(
876
+ self.step_dir, corner, f"{self.config['DESIGN_NAME']}__{corner}.lib"
877
+ )
878
+ lib_dict[corner] = Path(lib)
879
+
880
+ views_updates[DesignFormat.LIB] = lib_dict
881
+ return views_updates, metrics_updates
882
+
883
+
884
+ @Step.factory.register()
885
+ class Floorplan(OpenROADStep):
886
+ """
887
+ Creates DEF and ODB files with the initial floorplan based on the Yosys netlist.
888
+ """
889
+
890
+ id = "OpenROAD.Floorplan"
891
+ name = "Floorplan Init"
892
+ long_name = "Floorplan Initialization"
893
+
894
+ inputs = [DesignFormat.NETLIST]
895
+
896
+ config_vars = OpenROADStep.config_vars + [
897
+ Variable(
898
+ "FP_SIZING",
899
+ Literal["absolute", "relative"],
900
+ "Sizing mode for floorplanning",
901
+ default="relative",
902
+ ),
903
+ Variable(
904
+ "FP_ASPECT_RATIO",
905
+ Decimal,
906
+ "The core's aspect ratio (height / width).",
907
+ default=1,
908
+ ),
909
+ Variable(
910
+ "FP_CORE_UTIL",
911
+ Decimal,
912
+ "The core utilization percentage.",
913
+ default=50,
914
+ units="%",
915
+ ),
916
+ Variable(
917
+ "FP_OBSTRUCTIONS",
918
+ Optional[List[Tuple[Decimal, Decimal, Decimal, Decimal]]],
919
+ "Obstructions applied at floorplanning stage. Placement sites are never generated at these locations, which guarantees that it will remain empty throughout the entire flow.",
920
+ units="µm",
921
+ ),
922
+ Variable(
923
+ "PL_SOFT_OBSTRUCTIONS",
924
+ Optional[List[Tuple[Decimal, Decimal, Decimal, Decimal]]],
925
+ "Soft placement blockages applied at the floorplanning stage. Areas that are soft-blocked will not be used by the initial placer, however, later phases such as buffer insertion or clock tree synthesis are still allowed to place cells in this area.",
926
+ units="µm",
927
+ ),
928
+ Variable(
929
+ "CORE_AREA",
930
+ Optional[Tuple[Decimal, Decimal, Decimal, Decimal]],
931
+ "Specifies a core area (i.e. die area minus margins) to be used in floorplanning."
932
+ + " It must be paired with `DIE_AREA`.",
933
+ units="µm",
934
+ ),
935
+ Variable(
936
+ "BOTTOM_MARGIN_MULT",
937
+ Decimal,
938
+ "The core margin, in multiples of site heights, from the bottom boundary."
939
+ + " If `DIEA_AREA` and `CORE_AREA` are set, this variable has no effect.",
940
+ default=4,
941
+ ),
942
+ Variable(
943
+ "TOP_MARGIN_MULT",
944
+ Decimal,
945
+ "The core margin, in multiples of site heights, from the top boundary."
946
+ + " If `DIE_AREA` and `CORE_AREA` are set, this variable has no effect.",
947
+ default=4,
948
+ ),
949
+ Variable(
950
+ "LEFT_MARGIN_MULT",
951
+ Decimal,
952
+ "The core margin, in multiples of site widths, from the left boundary."
953
+ + " If `DIE_AREA` are `CORE_AREA` are set, this variable has no effect.",
954
+ default=12,
955
+ ),
956
+ Variable(
957
+ "RIGHT_MARGIN_MULT",
958
+ Decimal,
959
+ "The core margin, in multiples of site widths, from the right boundary."
960
+ + " If `DIE_AREA` are `CORE_AREA` are set, this variable has no effect.",
961
+ default=12,
962
+ ),
963
+ Variable(
964
+ "EXTRA_SITES",
965
+ Optional[List[str]],
966
+ "Explicitly specify sites other than `PLACE_SITE` to create rows for. If the alternate-site standard cells properly declare the `SITE` property, you do not need to provide this explicitly.",
967
+ pdk=True,
968
+ ),
969
+ ]
970
+
971
+ class Mode(str, Enum):
972
+ TEMPLATE = "template"
973
+ ABSOULTE = "absolute"
974
+ RELATIVE = "relative"
975
+
976
+ def get_script_path(self):
977
+ return os.path.join(get_script_dir(), "openroad", "floorplan.tcl")
978
+
979
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
980
+ path = self.config["FP_TRACKS_INFO"]
981
+ tracks_info_str = open(path).read()
982
+ tracks_commands = old_to_new_tracks(tracks_info_str)
983
+ new_tracks_info = os.path.join(self.step_dir, "config.tracks")
984
+ with open(new_tracks_info, "w") as f:
985
+ f.write(tracks_commands)
986
+
987
+ kwargs, env = self.extract_env(kwargs)
988
+ env["TRACKS_INFO_FILE_PROCESSED"] = new_tracks_info
989
+ return super().run(state_in, env=env, **kwargs)
990
+
991
+
992
+ def _migrate_ppl_mode(migrated):
993
+ as_int = None
994
+ try:
995
+ as_int = int(migrated)
996
+ except ValueError:
997
+ pass
998
+ if as_int is not None:
999
+ if as_int < 0 or as_int > 2:
1000
+ raise ValueError(
1001
+ f"Legacy variable FP_IO_MODE can only either be 0 for matching or 1 for random_equidistant-- '{as_int}' is invalid.\nPlease see the documentation for the usage of the replacement variable, 'FP_PIN_MODE'."
1002
+ )
1003
+ return ["matching", "random_equidistant"][as_int]
1004
+ return migrated
1005
+
1006
+
1007
+ @Step.factory.register()
1008
+ class IOPlacement(OpenROADStep):
1009
+ """
1010
+ Places I/O pins on a floor-planned ODB file using OpenROAD's built-in placer.
1011
+
1012
+ If ``FP_PIN_ORDER_CFG`` is not ``None``, this step is skipped (for
1013
+ compatibility with LibreLane 1.)
1014
+ """
1015
+
1016
+ id = "OpenROAD.IOPlacement"
1017
+ name = "I/O Placement"
1018
+
1019
+ config_vars = (
1020
+ OpenROADStep.config_vars
1021
+ + io_layer_variables
1022
+ + [
1023
+ Variable(
1024
+ "FP_PPL_MODE",
1025
+ Literal["matching", "random_equidistant", "annealing"],
1026
+ "Decides the mode of the random IO placement option.",
1027
+ default="matching",
1028
+ deprecated_names=[("FP_IO_MODE", _migrate_ppl_mode)],
1029
+ ),
1030
+ Variable(
1031
+ "FP_IO_MIN_DISTANCE",
1032
+ Optional[Decimal],
1033
+ "The minimum distance between two pins. If unspecified by a PDK, OpenROAD will use the length of two routing tracks.",
1034
+ units="µm",
1035
+ pdk=True,
1036
+ ),
1037
+ Variable(
1038
+ "FP_PIN_ORDER_CFG",
1039
+ Optional[Path],
1040
+ "Path to a custom pin configuration file.",
1041
+ ),
1042
+ Variable(
1043
+ "FP_DEF_TEMPLATE",
1044
+ Optional[Path],
1045
+ "Points to the DEF file to be used as a template.",
1046
+ ),
1047
+ Variable(
1048
+ "FP_IO_VLENGTH",
1049
+ Optional[Decimal],
1050
+ """
1051
+ The length of the pins with a north or south orientation. If unspecified by a PDK, OpenROAD will use whichever is higher of the following two values:
1052
+ * The pin width
1053
+ * The minimum value satisfying the minimum area constraint given the pin width
1054
+ """,
1055
+ units="µm",
1056
+ pdk=True,
1057
+ ),
1058
+ Variable(
1059
+ "FP_IO_HLENGTH",
1060
+ Optional[Decimal],
1061
+ """
1062
+ The length of the pins with an east or west orientation. If unspecified by a PDK, OpenROAD will use whichever is higher of the following two values:
1063
+ * The pin width
1064
+ * The minimum value satisfying the minimum area constraint given the pin width
1065
+ """,
1066
+ units="µm",
1067
+ pdk=True,
1068
+ ),
1069
+ ]
1070
+ )
1071
+
1072
+ def get_script_path(self):
1073
+ return os.path.join(get_script_dir(), "openroad", "ioplacer.tcl")
1074
+
1075
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1076
+ if self.config["FP_PIN_ORDER_CFG"] is not None:
1077
+ info(f"FP_PIN_ORDER_CFG is set. Skipping '{self.id}'…")
1078
+ return {}, {}
1079
+ if self.config["FP_DEF_TEMPLATE"] is not None:
1080
+ info(
1081
+ f"I/O pins were loaded from {self.config['FP_DEF_TEMPLATE']}. Skipping {self.id}…"
1082
+ )
1083
+ return {}, {}
1084
+
1085
+ return super().run(state_in, **kwargs)
1086
+
1087
+
1088
+ @Step.factory.register()
1089
+ class TapEndcapInsertion(OpenROADStep):
1090
+ """
1091
+ Places well TAP cells across a floorplan, as well as end-cap cells at the
1092
+ edges of the floorplan.
1093
+ """
1094
+
1095
+ id = "OpenROAD.TapEndcapInsertion"
1096
+ name = "Tap/Decap Insertion"
1097
+
1098
+ config_vars = OpenROADStep.config_vars + [
1099
+ Variable(
1100
+ "FP_MACRO_HORIZONTAL_HALO",
1101
+ Decimal,
1102
+ "Specify the horizontal halo size around macros while cutting rows.",
1103
+ default=10,
1104
+ units="µm",
1105
+ deprecated_names=["FP_TAP_HORIZONTAL_HALO"],
1106
+ ),
1107
+ Variable(
1108
+ "FP_MACRO_VERTICAL_HALO",
1109
+ Decimal,
1110
+ "Specify the vertical halo size around macros while cutting rows.",
1111
+ default=10,
1112
+ units="µm",
1113
+ deprecated_names=["FP_TAP_VERTICAL_HALO"],
1114
+ ),
1115
+ ]
1116
+
1117
+ def get_script_path(self):
1118
+ return os.path.join(get_script_dir(), "openroad", "tapcell.tcl")
1119
+
1120
+
1121
+ def get_psm_error_count(rpt: io.TextIOWrapper) -> int:
1122
+ sio = io.StringIO()
1123
+
1124
+ # Turn almost-YAML into YAML
1125
+ VIO_TYPE_PFX = "violation type: "
1126
+ for line in rpt:
1127
+ if line.startswith(VIO_TYPE_PFX):
1128
+ vio_type = line[len(VIO_TYPE_PFX) :].strip()
1129
+ sio.write(f"- type: {vio_type}\n")
1130
+ elif "bbox = " in line:
1131
+ sio.write(line.replace("bbox = ", "- bbox ="))
1132
+ else:
1133
+ sio.write(line)
1134
+
1135
+ sio.seek(0)
1136
+ violations = yaml.load(sio, Loader=yaml.SafeLoader) or []
1137
+ return functools.reduce(
1138
+ lambda acc, current: acc + len(current["srcs"]), violations, 0
1139
+ )
1140
+
1141
+
1142
+ @Step.factory.register()
1143
+ class GeneratePDN(OpenROADStep):
1144
+ """
1145
+ Creates a power distribution network on a floorplanned ODB file.
1146
+ """
1147
+
1148
+ id = "OpenROAD.GeneratePDN"
1149
+ name = "Generate PDN"
1150
+ long_name = "Power Distribution Network Generation"
1151
+
1152
+ config_vars = (
1153
+ OpenROADStep.config_vars
1154
+ + pdn_variables
1155
+ + [
1156
+ Variable(
1157
+ "FP_PDN_CFG",
1158
+ Optional[Path],
1159
+ "A custom PDN configuration file. If not provided, the default PDN config will be used.",
1160
+ deprecated_names=["PDN_CFG"],
1161
+ )
1162
+ ]
1163
+ )
1164
+
1165
+ def get_script_path(self):
1166
+ return os.path.join(get_script_dir(), "openroad", "pdn.tcl")
1167
+
1168
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1169
+ kwargs, env = self.extract_env(kwargs)
1170
+ if self.config["FP_PDN_CFG"] is None:
1171
+ env["FP_PDN_CFG"] = os.path.join(
1172
+ get_script_dir(), "openroad", "common", "pdn_cfg.tcl"
1173
+ )
1174
+ info(f"'FP_PDN_CFG' not explicitly set, setting it to {env['FP_PDN_CFG']}…")
1175
+ views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
1176
+
1177
+ error_reports = glob(os.path.join(self.step_dir, "*-grid-errors.rpt"))
1178
+ for report in error_reports:
1179
+ net = os.path.basename(report).split("-", maxsplit=1)[0]
1180
+ count = get_psm_error_count(open(report, encoding="utf8"))
1181
+ metrics_updates[f"design__power_grid_violation__count__net:{net}"] = count
1182
+
1183
+ metric_updates_with_aggregates = aggregate_metrics(
1184
+ metrics_updates,
1185
+ {"design__power_grid_violation__count": (0, lambda x: sum(x))},
1186
+ )
1187
+
1188
+ return views_updates, metric_updates_with_aggregates
1189
+
1190
+
1191
+ class _GlobalPlacement(OpenROADStep):
1192
+ config_vars = (
1193
+ OpenROADStep.config_vars
1194
+ + routing_layer_variables
1195
+ + [
1196
+ Variable(
1197
+ "PL_TARGET_DENSITY_PCT",
1198
+ Optional[Decimal],
1199
+ "The desired placement density of cells. If not specified, the value will be equal to (`FP_CORE_UTIL` + 5 * `GPL_CELL_PADDING` + 10).",
1200
+ units="%",
1201
+ deprecated_names=[
1202
+ ("PL_TARGET_DENSITY", lambda d: Decimal(d) * Decimal(100.0))
1203
+ ],
1204
+ ),
1205
+ Variable(
1206
+ "PL_SKIP_INITIAL_PLACEMENT",
1207
+ bool,
1208
+ "Specifies whether the placer should run initial placement or not.",
1209
+ default=False,
1210
+ ),
1211
+ Variable(
1212
+ "PL_WIRE_LENGTH_COEF",
1213
+ Decimal,
1214
+ "Global placement initial wirelength coefficient."
1215
+ + " Decreasing the variable will modify the initial placement of the standard cells to reduce the wirelengths",
1216
+ default=0.25,
1217
+ deprecated_names=["PL_WIRELENGTH_COEF"],
1218
+ ),
1219
+ Variable(
1220
+ "PL_MIN_PHI_COEFFICIENT",
1221
+ Optional[Decimal],
1222
+ "Sets a lower bound on the µ_k variable in the GPL algorithm. Useful if global placement diverges. See https://openroad.readthedocs.io/en/latest/main/src/gpl/README.html",
1223
+ ),
1224
+ Variable(
1225
+ "PL_MAX_PHI_COEFFICIENT",
1226
+ Optional[Decimal],
1227
+ "Sets a upper bound on the µ_k variable in the GPL algorithm. Useful if global placement diverges.See https://openroad.readthedocs.io/en/latest/main/src/gpl/README.html",
1228
+ ),
1229
+ Variable(
1230
+ "FP_CORE_UTIL",
1231
+ Decimal,
1232
+ "The core utilization percentage.",
1233
+ default=50,
1234
+ units="%",
1235
+ ),
1236
+ Variable(
1237
+ "GPL_CELL_PADDING",
1238
+ Decimal,
1239
+ "Cell padding value (in sites) for global placement. The number will be integer divided by 2 and placed on both sides.",
1240
+ units="sites",
1241
+ pdk=True,
1242
+ ),
1243
+ ]
1244
+ )
1245
+
1246
+ def get_script_path(self):
1247
+ return os.path.join(get_script_dir(), "openroad", "gpl.tcl")
1248
+
1249
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1250
+ kwargs, env = self.extract_env(kwargs)
1251
+ if self.config["PL_TARGET_DENSITY_PCT"] is None:
1252
+ util = self.config["FP_CORE_UTIL"]
1253
+ metrics_util = state_in.metrics.get("design__instance__utilization")
1254
+ if metrics_util is not None:
1255
+ util = metrics_util * 100
1256
+
1257
+ expr = util + (5 * self.config["GPL_CELL_PADDING"]) + 10
1258
+ expr = min(expr, 100)
1259
+ env["PL_TARGET_DENSITY_PCT"] = f"{expr}"
1260
+ info(
1261
+ f"'PL_TARGET_DENSITY_PCT' not explicitly set, using dynamically calculated target density: {expr}…"
1262
+ )
1263
+ return super().run(state_in, env=env, **kwargs)
1264
+
1265
+
1266
+ @Step.factory.register()
1267
+ class GlobalPlacement(_GlobalPlacement):
1268
+ """
1269
+ Performs a somewhat nebulous initial placement for standard cells in a
1270
+ floorplan. While the placement is not concrete, it is enough to start
1271
+ accounting for issues such as fanout, transition time, et cetera.
1272
+ """
1273
+
1274
+ id = "OpenROAD.GlobalPlacement"
1275
+ name = "Global Placement"
1276
+
1277
+ config_vars = _GlobalPlacement.config_vars + [
1278
+ Variable(
1279
+ "PL_TIME_DRIVEN",
1280
+ bool,
1281
+ "Specifies whether the placer should use time driven placement.",
1282
+ default=True,
1283
+ ),
1284
+ Variable(
1285
+ "PL_ROUTABILITY_DRIVEN",
1286
+ bool,
1287
+ "Specifies whether the placer should use routability driven placement.",
1288
+ default=True,
1289
+ ),
1290
+ Variable(
1291
+ "PL_ROUTABILITY_OVERFLOW_THRESHOLD",
1292
+ Optional[Decimal],
1293
+ "Sets overflow threshold for routability mode.",
1294
+ ),
1295
+ ]
1296
+
1297
+
1298
+ @Step.factory.register()
1299
+ class GlobalPlacementSkipIO(_GlobalPlacement):
1300
+ """
1301
+ Performs global placement without taking I/O into consideration.
1302
+
1303
+ This is useful for flows where the:
1304
+ * Cells are placed
1305
+ * I/Os are placed to match the cells
1306
+ * Cells are then re-placed for an optimal placement
1307
+ """
1308
+
1309
+ id = "OpenROAD.GlobalPlacementSkipIO"
1310
+ name = "Global Placement Skip IO"
1311
+
1312
+ config_vars = _GlobalPlacement.config_vars + [
1313
+ Variable(
1314
+ "FP_PPL_MODE",
1315
+ Literal["matching", "random_equidistant", "annealing"],
1316
+ "Decides the mode of the random IO placement option.",
1317
+ default="matching",
1318
+ deprecated_names=[("FP_IO_MODE", _migrate_ppl_mode)],
1319
+ ),
1320
+ Variable(
1321
+ "FP_DEF_TEMPLATE",
1322
+ Optional[Path],
1323
+ "Points to the DEF file to be used as a template.",
1324
+ ),
1325
+ ]
1326
+
1327
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1328
+ kwargs, env = self.extract_env(kwargs)
1329
+ if self.config["FP_DEF_TEMPLATE"] is not None:
1330
+ info(
1331
+ f"I/O pins were loaded from {self.config['FP_DEF_TEMPLATE']}. Skipping the first global placement iteration…"
1332
+ )
1333
+ return {}, {}
1334
+ env["__PL_SKIP_IO"] = "1"
1335
+ return super().run(state_in, env=env, **kwargs)
1336
+
1337
+
1338
+ @Step.factory.register()
1339
+ class BasicMacroPlacement(OpenROADStep):
1340
+ id = "OpenROAD.BasicMacroPlacement"
1341
+ name = "Basic Macro Placement"
1342
+
1343
+ config_vars = OpenROADStep.config_vars + [
1344
+ Variable(
1345
+ "PL_MACRO_HALO",
1346
+ str,
1347
+ "Macro placement halo. Format: `{Horizontal} {Vertical}`.",
1348
+ default="0 0",
1349
+ units="µm",
1350
+ ),
1351
+ Variable(
1352
+ "PL_MACRO_CHANNEL",
1353
+ str,
1354
+ "Channel widths between macros. Format: `{Horizontal} {Vertical}`.",
1355
+ default="0 0",
1356
+ units="µm",
1357
+ ),
1358
+ ]
1359
+
1360
+ def get_script_path(self):
1361
+ raise NotImplementedError()
1362
+
1363
+
1364
+ @Step.factory.register()
1365
+ class DetailedPlacement(OpenROADStep):
1366
+ """
1367
+ Performs "detailed placement" on an ODB file with global placement. This results
1368
+ in a concrete and legal placement of all cells.
1369
+ """
1370
+
1371
+ id = "OpenROAD.DetailedPlacement"
1372
+ name = "Detailed Placement"
1373
+
1374
+ config_vars = OpenROADStep.config_vars + dpl_variables
1375
+
1376
+ def get_script_path(self):
1377
+ return os.path.join(get_script_dir(), "openroad", "dpl.tcl")
1378
+
1379
+
1380
+ @Step.factory.register()
1381
+ class CheckAntennas(OpenROADStep):
1382
+ """
1383
+ Runs OpenROAD to check if one or more long nets may constitute an
1384
+ `antenna risk <https://en.wikipedia.org/wiki/Antenna_effect>`_.
1385
+
1386
+ The metric ``route__antenna_violation__count`` will be updated with the number of violating nets.
1387
+ """
1388
+
1389
+ id = "OpenROAD.CheckAntennas"
1390
+ name = "Check Antennas"
1391
+
1392
+ # default inputs
1393
+ outputs = []
1394
+
1395
+ def get_script_path(self):
1396
+ return os.path.join(get_script_dir(), "openroad", "antenna_check.tcl")
1397
+
1398
+ def __summarize_antenna_report(self, report_file: str, output_file: str):
1399
+ """
1400
+ Extracts the list of violating nets from an ARC report file"
1401
+ """
1402
+
1403
+ class AntennaViolation:
1404
+ def __init__(self, net, pin, required_ratio, partial_ratio, layer):
1405
+ self.net = net
1406
+ self.pin = pin
1407
+ self.required_ratio = float(required_ratio)
1408
+ self.partial_ratio = float(partial_ratio)
1409
+ self.layer = layer
1410
+ self.partial_to_required = self.partial_ratio / self.required_ratio
1411
+
1412
+ def __lt__(self, other):
1413
+ return self.partial_to_required < other.partial_to_required
1414
+
1415
+ net_pattern = re.compile(r"\s*Net:\s*(\S+)")
1416
+ required_ratio_pattern = re.compile(r"\s*Required ratio:\s+([\d.]+)")
1417
+ partial_ratio_pattern = re.compile(r"\s*Partial area ratio:\s+([\d.]+)")
1418
+ layer_pattern = re.compile(r"\s*Layer:\s+(\S+)")
1419
+ pin_pattern = re.compile(r"\s*Pin:\s+(\S+)")
1420
+
1421
+ required_ratio = None
1422
+ layer = None
1423
+ partial_ratio = None
1424
+ required_ratio = None
1425
+ pin = None
1426
+ net = None
1427
+ violations: List[AntennaViolation] = []
1428
+
1429
+ net_pattern = re.compile(r"\s*Net:\s*(\S+)")
1430
+ required_ratio_pattern = re.compile(r"\s*Required ratio:\s+([\d.]+)")
1431
+ partial_ratio_pattern = re.compile(r"\s*Partial area ratio:\s+([\d.]+)")
1432
+ layer_pattern = re.compile(r"\s*Layer:\s+(\S+)")
1433
+ pin_pattern = re.compile(r"\s*Pin:\s+(\S+)")
1434
+
1435
+ with open(report_file, "r") as f:
1436
+ for line in f:
1437
+ pin_new = pin_pattern.match(line)
1438
+ required_ratio_new = required_ratio_pattern.match(line)
1439
+ partial_ratio_new = partial_ratio_pattern.match(line)
1440
+ layer_new = layer_pattern.match(line)
1441
+ net_new = net_pattern.match(line)
1442
+ required_ratio = (
1443
+ required_ratio_new.group(1)
1444
+ if required_ratio_new is not None
1445
+ else required_ratio
1446
+ )
1447
+ partial_ratio = (
1448
+ partial_ratio_new.group(1)
1449
+ if partial_ratio_new is not None
1450
+ else partial_ratio
1451
+ )
1452
+ layer = layer_new.group(1) if layer_new is not None else layer
1453
+ pin = pin_new.group(1) if pin_new is not None else pin
1454
+ net = net_new.group(1) if net_new is not None else net
1455
+
1456
+ if "VIOLATED" in line:
1457
+ violations.append(
1458
+ AntennaViolation(
1459
+ net=net,
1460
+ pin=pin,
1461
+ partial_ratio=partial_ratio,
1462
+ layer=layer,
1463
+ required_ratio=required_ratio,
1464
+ )
1465
+ )
1466
+
1467
+ violations.sort(reverse=True)
1468
+
1469
+ # Partial/Required: 2.36, Required: 3091.96, Partial: 7298.29,
1470
+ # Net: net384, Pin: _22354_/A, Layer: met5
1471
+ table = rich.table.Table()
1472
+ decimal_places = 2
1473
+ row = []
1474
+ table.add_column("P / R")
1475
+ table.add_column("Partial")
1476
+ table.add_column("Required")
1477
+ table.add_column("Net")
1478
+ table.add_column("Pin")
1479
+ table.add_column("Layer")
1480
+ for violation in violations:
1481
+ row = [
1482
+ f"{violation.partial_to_required:.{decimal_places}f}",
1483
+ f"{violation.partial_ratio:.{decimal_places}f}",
1484
+ f"{violation.required_ratio:.{decimal_places}f}",
1485
+ f"{violation.net}",
1486
+ f"{violation.pin}",
1487
+ f"{violation.layer}",
1488
+ ]
1489
+ table.add_row(*row)
1490
+
1491
+ if not options.get_condensed_mode() and len(violations):
1492
+ console.print(table)
1493
+ file_console = rich.console.Console(
1494
+ file=open(output_file, "w", encoding="utf8"), width=160
1495
+ )
1496
+ file_console.print(table)
1497
+
1498
+ def __get_antenna_nets(self, report: io.TextIOWrapper) -> int:
1499
+ pattern = re.compile(r"Net:\s*(\w+)")
1500
+ count = 0
1501
+
1502
+ for line in report:
1503
+ line = line.strip()
1504
+ m = pattern.match(line)
1505
+ if m is None:
1506
+ continue
1507
+ count += 1
1508
+
1509
+ return count
1510
+
1511
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1512
+ report_dir = os.path.join(self.step_dir, "reports")
1513
+ report_path = os.path.join(report_dir, "antenna.rpt")
1514
+ report_summary_path = os.path.join(report_dir, "antenna_summary.rpt")
1515
+ kwargs, env = self.extract_env(kwargs)
1516
+ env["_ANTENNA_REPORT"] = report_path
1517
+
1518
+ mkdirp(os.path.join(self.step_dir, "reports"))
1519
+
1520
+ views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
1521
+ metrics_updates["route__antenna_violation__count"] = self.__get_antenna_nets(
1522
+ open(report_path)
1523
+ )
1524
+ self.__summarize_antenna_report(report_path, report_summary_path)
1525
+
1526
+ return views_updates, metrics_updates
1527
+
1528
+
1529
+ @Step.factory.register()
1530
+ class GlobalRouting(OpenROADStep):
1531
+ """
1532
+ The initial phase of routing. Given a detailed-placed ODB file, this
1533
+ phase starts assigning coarse-grained routing "regions" for each net so they
1534
+ may be later connected to wires.
1535
+
1536
+ Estimated capacitance and resistance values are much more accurate for
1537
+ global routing.
1538
+ """
1539
+
1540
+ id = "OpenROAD.GlobalRouting"
1541
+ name = "Global Routing"
1542
+
1543
+ outputs = [DesignFormat.ODB, DesignFormat.DEF]
1544
+
1545
+ config_vars = OpenROADStep.config_vars + grt_variables + dpl_variables
1546
+
1547
+ def get_script_path(self):
1548
+ return os.path.join(get_script_dir(), "openroad", "grt.tcl")
1549
+
1550
+
1551
+ class _DiodeInsertion(GlobalRouting):
1552
+ id = "DiodeInsertion"
1553
+
1554
+ def get_script_path(self):
1555
+ return os.path.join(get_script_dir(), "openroad", "antenna_repair.tcl")
1556
+
1557
+
1558
+ @Step.factory.register()
1559
+ class RepairAntennas(CompositeStep):
1560
+ """
1561
+ Applies `antenna effect <https://en.wikipedia.org/wiki/Antenna_effect>`_
1562
+ mitigations using global-routing information, then re-runs detailed placement
1563
+ and global routing to legalize any inserted diodes.
1564
+
1565
+ An antenna check is once again performed, updating the
1566
+ ``route__antenna_violation__count`` metric.
1567
+ """
1568
+
1569
+ id = "OpenROAD.RepairAntennas"
1570
+ name = "Antenna Repair"
1571
+
1572
+ Steps = [_DiodeInsertion, CheckAntennas]
1573
+
1574
+
1575
+ @Step.factory.register()
1576
+ class DetailedRouting(OpenROADStep):
1577
+ """
1578
+ The latter phase of routing. This transforms the abstract nets from global
1579
+ routing into wires on the metal layers that respect all design rules, avoids
1580
+ creating accidental shorts, and ensures all wires are connected.
1581
+
1582
+ This is by far the longest part of a typical flow, taking hours, days or weeks
1583
+ on larger designs.
1584
+
1585
+ After this point, all cells connected to a net can no longer be moved or
1586
+ removed without a custom-written step of some kind that will also rip up
1587
+ wires.
1588
+ """
1589
+
1590
+ id = "OpenROAD.DetailedRouting"
1591
+ name = "Detailed Routing"
1592
+
1593
+ config_vars = OpenROADStep.config_vars + [
1594
+ Variable(
1595
+ "DRT_THREADS",
1596
+ Optional[int],
1597
+ "Specifies the number of threads to be used in OpenROAD Detailed Routing. If unset, this will be equal to your machine's thread count.",
1598
+ deprecated_names=["ROUTING_CORES"],
1599
+ ),
1600
+ Variable(
1601
+ "DRT_MIN_LAYER",
1602
+ Optional[str],
1603
+ "An optional override to the lowest layer used in detailed routing. For example, in sky130, you may want global routing to avoid li1, but let detailed routing use li1 if it has to.",
1604
+ ),
1605
+ Variable(
1606
+ "DRT_MAX_LAYER",
1607
+ Optional[str],
1608
+ "An optional override to the highest layer used in detailed routing.",
1609
+ ),
1610
+ Variable(
1611
+ "DRT_OPT_ITERS",
1612
+ int,
1613
+ "Specifies the maximum number of optimization iterations during Detailed Routing in TritonRoute.",
1614
+ default=64,
1615
+ ),
1616
+ ]
1617
+
1618
+ def get_script_path(self):
1619
+ return os.path.join(get_script_dir(), "openroad", "drt.tcl")
1620
+
1621
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1622
+ kwargs, env = self.extract_env(kwargs)
1623
+ env["DRT_THREADS"] = env.get("DRT_THREADS", str(_get_process_limit()))
1624
+ info(f"Running TritonRoute with {env['DRT_THREADS']} threads…")
1625
+ return super().run(state_in, env=env, **kwargs)
1626
+
1627
+
1628
+ @Step.factory.register()
1629
+ class LayoutSTA(OpenROADStep):
1630
+ """
1631
+ Performs `Static Timing Analysis <https://en.wikipedia.org/wiki/Static_timing_analysis>`_
1632
+ using OpenROAD on the ODB layout in its current state.
1633
+ """
1634
+
1635
+ id = "OpenROAD.LayoutSTA"
1636
+ name = "Layout STA"
1637
+ long_name = "Layout Static Timing Analysis"
1638
+
1639
+ inputs = [DesignFormat.ODB]
1640
+ outputs = []
1641
+
1642
+ def get_script_path(self):
1643
+ return os.path.join(get_script_dir(), "openroad", "sta.tcl")
1644
+
1645
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1646
+ kwargs, env = self.extract_env(kwargs)
1647
+ env["RUN_STANDALONE"] = "1"
1648
+ return super().run(state_in, env=env, **kwargs)
1649
+
1650
+
1651
+ @Step.factory.register()
1652
+ class FillInsertion(OpenROADStep):
1653
+ """
1654
+ Fills gaps in the floorplan with filler and decap cells.
1655
+
1656
+ This is run after detailed placement. After this point, the design is basically
1657
+ completely hardened.
1658
+ """
1659
+
1660
+ id = "OpenROAD.FillInsertion"
1661
+ name = "Fill Insertion"
1662
+
1663
+ def get_script_path(self):
1664
+ return os.path.join(get_script_dir(), "openroad", "fill.tcl")
1665
+
1666
+
1667
+ @Step.factory.register()
1668
+ class RCX(OpenROADStep):
1669
+ """
1670
+ This extracts `parasitic <https://en.wikipedia.org/wiki/Parasitic_element_(electrical_networks)>`_
1671
+ electrical values from a detailed-placed circuit. These can be used to create
1672
+ basically the highest accurate STA possible for a given design.
1673
+ """
1674
+
1675
+ id = "OpenROAD.RCX"
1676
+ name = "Parasitics (RC) Extraction"
1677
+ long_name = "Parasitic Resistance/Capacitance Extraction"
1678
+
1679
+ config_vars = OpenROADStep.config_vars + [
1680
+ Variable(
1681
+ "RCX_MERGE_VIA_WIRE_RES",
1682
+ bool,
1683
+ "If enabled, the via and wire resistances will be merged.",
1684
+ default=True,
1685
+ ),
1686
+ Variable(
1687
+ "RCX_SDC_FILE",
1688
+ Optional[Path],
1689
+ "Specifies SDC file to be used for RCX-based STA, which can be different from the one used for implementation.",
1690
+ ),
1691
+ Variable(
1692
+ "RCX_RULESETS",
1693
+ Dict[str, Path],
1694
+ "Map of corner patterns to OpenRCX extraction rules.",
1695
+ pdk=True,
1696
+ ),
1697
+ Variable(
1698
+ "STA_THREADS",
1699
+ Optional[int],
1700
+ "The maximum number of STA corners to run in parallel. If unset, this will be equal to your machine's thread count.",
1701
+ ),
1702
+ ]
1703
+
1704
+ inputs = [DesignFormat.DEF]
1705
+ outputs = [DesignFormat.SPEF]
1706
+
1707
+ def get_script_path(self):
1708
+ return os.path.join(get_script_dir(), "openroad", "rcx.tcl")
1709
+
1710
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1711
+ kwargs, env = self.extract_env(kwargs)
1712
+ env = self.prepare_env(env, state_in)
1713
+
1714
+ def run_corner(corner: str):
1715
+ nonlocal env
1716
+ current_env = env.copy()
1717
+
1718
+ rcx_ruleset = self.config["RCX_RULESETS"].get(corner)
1719
+ if rcx_ruleset is None:
1720
+ self.warn(
1721
+ f"RCX ruleset for corner {corner} not found. The corner may be ill-defined."
1722
+ )
1723
+ return None
1724
+
1725
+ corner_sanitized = corner.strip("*_")
1726
+ corner_dir = os.path.join(self.step_dir, corner_sanitized)
1727
+ mkdirp(corner_dir)
1728
+
1729
+ tech_lefs = self.toolbox.filter_views(
1730
+ self.config, self.config["TECH_LEFS"], corner
1731
+ )
1732
+ if len(tech_lefs) < 1:
1733
+ self.warn(f"No tech lef for timing corner {corner} found.")
1734
+ return None
1735
+ elif len(tech_lefs) > 1:
1736
+ self.warn(
1737
+ f"Multiple tech lefs found for timing corner {corner}. Only the first one matched will be used."
1738
+ )
1739
+
1740
+ current_env["RCX_LEF"] = tech_lefs[0]
1741
+ current_env["RCX_RULESET"] = rcx_ruleset
1742
+
1743
+ out = os.path.join(
1744
+ corner_dir, f"{self.config['DESIGN_NAME']}.{corner_sanitized}.spef"
1745
+ )
1746
+ current_env["SAVE_SPEF"] = out
1747
+
1748
+ corner_qualifier = f"the {corner} corner"
1749
+ if "*" in corner:
1750
+ corner_qualifier = f"corners matching {corner}"
1751
+
1752
+ log_path = os.path.join(corner_dir, "rcx.log")
1753
+ info(f"Running RCX for {corner_qualifier} ({log_path})…")
1754
+
1755
+ try:
1756
+ self.run_subprocess(
1757
+ self.get_command(),
1758
+ log_to=log_path,
1759
+ env=current_env,
1760
+ silent=True,
1761
+ )
1762
+ info(f"Finished RCX for {corner_qualifier}.")
1763
+ except subprocess.CalledProcessError as e:
1764
+ self.err(f"Failed RCX for the {corner_qualifier}:")
1765
+ raise e
1766
+
1767
+ return out
1768
+
1769
+ tpe = ThreadPoolExecutor(
1770
+ max_workers=self.config["STA_THREADS"] or _get_process_limit()
1771
+ )
1772
+
1773
+ futures: Dict[str, Future[str]] = {}
1774
+ for corner in self.config["RCX_RULESETS"]:
1775
+ futures[corner] = tpe.submit(
1776
+ run_corner,
1777
+ corner,
1778
+ )
1779
+
1780
+ views_updates: ViewsUpdate = {}
1781
+ metrics_updates: MetricsUpdate = {}
1782
+
1783
+ spef_dict = state_in[DesignFormat.SPEF] or {}
1784
+ if not isinstance(spef_dict, dict):
1785
+ raise StepException(
1786
+ "Malformed input state: value for SPEF is not a dictionary."
1787
+ )
1788
+
1789
+ for corner, future in futures.items():
1790
+ if result := future.result():
1791
+ spef_dict[corner] = Path(result)
1792
+
1793
+ views_updates[DesignFormat.SPEF] = spef_dict
1794
+
1795
+ return views_updates, metrics_updates
1796
+
1797
+
1798
+ @Step.factory.register()
1799
+ class IRDropReport(OpenROADStep):
1800
+ """
1801
+ Performs static IR-drop analysis on the power distribution network. For power
1802
+ nets, this constitutes a decrease in voltage, and for ground nets, it constitutes
1803
+ an increase in voltage.
1804
+ """
1805
+
1806
+ id = "OpenROAD.IRDropReport"
1807
+ name = "IR Drop Report"
1808
+ long_name = "Generate IR Drop Report"
1809
+
1810
+ inputs = [DesignFormat.ODB, DesignFormat.SPEF]
1811
+ outputs = []
1812
+
1813
+ config_vars = OpenROADStep.config_vars + [
1814
+ Variable(
1815
+ "VSRC_LOC_FILES",
1816
+ Optional[Dict[str, Path]],
1817
+ "Map of power and ground nets to OpenROAD PSM location files. See [this](https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/src/psm#commands) for more info.",
1818
+ )
1819
+ ]
1820
+
1821
+ def get_script_path(self):
1822
+ return os.path.join(get_script_dir(), "openroad", "irdrop.tcl")
1823
+
1824
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
1825
+ from decimal import Decimal
1826
+
1827
+ assert state_in[DesignFormat.SPEF] is not None
1828
+ if not isinstance(state_in[DesignFormat.SPEF], dict):
1829
+ raise StepException(
1830
+ "Malformed input state: value for SPEF is not a dictionary."
1831
+ )
1832
+
1833
+ kwargs, env = self.extract_env(kwargs)
1834
+
1835
+ input_spef_dict = state_in[DesignFormat.SPEF]
1836
+ assert input_spef_dict is not None # Checked by start
1837
+ if not isinstance(input_spef_dict, dict):
1838
+ raise StepException(
1839
+ "Malformed input state: value for 'spef' is not a dictionary"
1840
+ )
1841
+
1842
+ spefs_in = self.toolbox.filter_views(self.config, input_spef_dict)
1843
+ if len(spefs_in) > 1:
1844
+ raise StepException(
1845
+ "Found more than one input SPEF file for the default corner."
1846
+ )
1847
+ elif len(spefs_in) < 1:
1848
+ raise StepException("No SPEF file found for the default corner.")
1849
+
1850
+ libs_in = self.toolbox.filter_views(self.config, self.config["LIB"])
1851
+
1852
+ if self.config["VSRC_LOC_FILES"] is None:
1853
+ self.warn(
1854
+ "'VSRC_LOC_FILES' was not given a value, which may make the results of IR drop analysis inaccurate. If you are not integrating a top-level chip for manufacture, you may ignore this warning, otherwise, see the documentation for 'VSRC_LOC_FILES'."
1855
+ )
1856
+
1857
+ if voltage := self.toolbox.get_lib_voltage(str(libs_in[0])):
1858
+ env["LIB_VOLTAGE"] = str(voltage)
1859
+
1860
+ env["CURRENT_SPEF_DEFAULT_CORNER"] = str(spefs_in[0])
1861
+ views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
1862
+
1863
+ report = open(os.path.join(self.step_dir, "irdrop.rpt")).read()
1864
+
1865
+ verbose(report)
1866
+
1867
+ voltage_rx = re.compile(r"Worstcase voltage\s*:\s*([\d\.\+\-e]+)\s*V")
1868
+ avg_drop_rx = re.compile(r"Average IR drop\s*:\s*([\d\.\+\-e]+)\s*V")
1869
+ worst_drop_rx = re.compile(r"Worstcase IR drop\s*:\s*([\d\.\+\-e]+)\s*V")
1870
+
1871
+ if m := voltage_rx.search(report):
1872
+ value_float = float(m[1])
1873
+ value_dec = Decimal(value_float)
1874
+ metrics_updates["ir__voltage__worst"] = value_dec
1875
+ else:
1876
+ raise Exception(
1877
+ "OpenROAD IR Drop Log format has changed- please file an issue."
1878
+ )
1879
+
1880
+ if m := avg_drop_rx.search(report):
1881
+ value_float = float(m[1])
1882
+ value_dec = Decimal(value_float)
1883
+ metrics_updates["ir__drop__avg"] = value_dec
1884
+ else:
1885
+ raise Exception(
1886
+ "OpenROAD IR Drop Log format has changed- please file an issue."
1887
+ )
1888
+
1889
+ if m := worst_drop_rx.search(report):
1890
+ value_float = float(m[1])
1891
+ value_dec = Decimal(value_float)
1892
+ metrics_updates["ir__drop__worst"] = value_dec
1893
+ else:
1894
+ raise Exception(
1895
+ "OpenROAD IR Drop Log format has changed- please file an issue."
1896
+ )
1897
+
1898
+ return views_updates, metrics_updates
1899
+
1900
+
1901
+ @Step.factory.register()
1902
+ class CutRows(OpenROADStep):
1903
+ """
1904
+ Cut floorplan rows with respect to placed macros.
1905
+ """
1906
+
1907
+ id = "OpenROAD.CutRows"
1908
+ name = "Cut Rows"
1909
+
1910
+ inputs = [DesignFormat.ODB]
1911
+ outputs = [
1912
+ DesignFormat.ODB,
1913
+ DesignFormat.DEF,
1914
+ ]
1915
+
1916
+ config_vars = OpenROADStep.config_vars + [
1917
+ Variable(
1918
+ "FP_MACRO_HORIZONTAL_HALO",
1919
+ Decimal,
1920
+ "Specify the horizontal halo size around macros while cutting rows.",
1921
+ default=10,
1922
+ units="µm",
1923
+ deprecated_names=["FP_TAP_HORIZONTAL_HALO"],
1924
+ ),
1925
+ Variable(
1926
+ "FP_MACRO_VERTICAL_HALO",
1927
+ Decimal,
1928
+ "Specify the vertical halo size around macros while cutting rows.",
1929
+ default=10,
1930
+ units="µm",
1931
+ deprecated_names=["FP_TAP_VERTICAL_HALO"],
1932
+ ),
1933
+ ]
1934
+
1935
+ def get_script_path(self):
1936
+ return os.path.join(get_script_dir(), "openroad", "cut_rows.tcl")
1937
+
1938
+
1939
+ @Step.factory.register()
1940
+ class WriteViews(OpenROADStep):
1941
+ """
1942
+ Write various layout views of an ODB design
1943
+ """
1944
+
1945
+ id = "OpenROAD.WriteViews"
1946
+ name = "OpenROAD Write Views"
1947
+ outputs = OpenROADStep.outputs + [
1948
+ DesignFormat.POWERED_NETLIST_SDF_FRIENDLY,
1949
+ DesignFormat.POWERED_NETLIST_NO_PHYSICAL_CELLS,
1950
+ DesignFormat.OPENROAD_LEF,
1951
+ ]
1952
+
1953
+ config_vars = OpenROADStep.config_vars + [
1954
+ Variable(
1955
+ "OPENROAD_LEF_BLOAT_OCCUPIED_LAYERS",
1956
+ bool,
1957
+ description="Generates cover obstructions (obstructions over the entire layer) for each layer where shapes are present",
1958
+ default=True,
1959
+ )
1960
+ ]
1961
+
1962
+ def get_script_path(self):
1963
+ return os.path.join(get_script_dir(), "openroad", "write_views.tcl")
1964
+
1965
+
1966
+ # Resizer Steps
1967
+
1968
+
1969
+ ## ABC
1970
+ class ResizerStep(OpenROADStep):
1971
+ config_vars = OpenROADStep.config_vars + grt_variables + rsz_variables
1972
+
1973
+ def run(
1974
+ self,
1975
+ state_in,
1976
+ **kwargs,
1977
+ ) -> Tuple[ViewsUpdate, MetricsUpdate]:
1978
+ kwargs, env = self.extract_env(kwargs)
1979
+
1980
+ corners_key: str = "RSZ_CORNERS"
1981
+
1982
+ if "corners_key" in kwargs:
1983
+ corners_key = kwargs.pop("corners_key")
1984
+
1985
+ corners = self.config[corners_key] or self.config["STA_CORNERS"]
1986
+ lib_set_set = set()
1987
+ count = 0
1988
+ for corner in corners:
1989
+ _, libs, _, _ = self.toolbox.get_timing_files_categorized(
1990
+ self.config, corner
1991
+ )
1992
+ lib_set = frozenset(libs)
1993
+ if lib_set in lib_set_set:
1994
+ debug(f"Liberty files for '{corner}' already accounted for- skipped")
1995
+ continue
1996
+ lib_set_set.add(lib_set)
1997
+ env[f"RSZ_CORNER_{count}"] = TclStep.value_to_tcl([corner] + libs)
1998
+ debug(f"Liberty files for '{corner}' added: {libs}")
1999
+ count += 1
2000
+
2001
+ return super().run(state_in, env=env, **kwargs)
2002
+
2003
+
2004
+ @Step.factory.register()
2005
+ class CTS(ResizerStep):
2006
+ """
2007
+ Creates a `Clock tree <https://en.wikipedia.org/wiki/Clock_signal#Distribution>`_
2008
+ for an ODB file with detailed-placed cells, using reasonably accurate resistance
2009
+ and capacitance estimations. Detailed Placement is then re-performed to
2010
+ accommodate the new cells.
2011
+ """
2012
+
2013
+ id = "OpenROAD.CTS"
2014
+ name = "Clock Tree Synthesis"
2015
+
2016
+ config_vars = (
2017
+ OpenROADStep.config_vars
2018
+ + dpl_variables
2019
+ + [
2020
+ Variable(
2021
+ "CTS_SINK_CLUSTERING_SIZE",
2022
+ int,
2023
+ "Specifies the maximum number of sinks per cluster.",
2024
+ default=25,
2025
+ ),
2026
+ Variable(
2027
+ "CTS_SINK_CLUSTERING_MAX_DIAMETER",
2028
+ Decimal,
2029
+ "Specifies maximum diameter of the sink cluster.",
2030
+ default=50,
2031
+ units="µm",
2032
+ ),
2033
+ Variable(
2034
+ "CTS_CLK_MAX_WIRE_LENGTH",
2035
+ Decimal,
2036
+ "Specifies the maximum wire length on the clock net.",
2037
+ default=0,
2038
+ units="µm",
2039
+ ),
2040
+ Variable(
2041
+ "CTS_DISABLE_POST_PROCESSING",
2042
+ bool,
2043
+ "Specifies whether or not to disable post cts processing for outlier sinks.",
2044
+ default=False,
2045
+ ),
2046
+ Variable(
2047
+ "CTS_DISTANCE_BETWEEN_BUFFERS",
2048
+ Decimal,
2049
+ "Specifies the distance between buffers when creating the clock tree.",
2050
+ default=0,
2051
+ units="µm",
2052
+ ),
2053
+ Variable(
2054
+ "CTS_CORNERS",
2055
+ Optional[List[str]],
2056
+ "A list of fully-qualified IPVT corners to use during clock tree synthesis. If unspecified, the value for `STA_CORNERS` from the PDK will be used.",
2057
+ ),
2058
+ Variable(
2059
+ "CTS_ROOT_BUFFER",
2060
+ str,
2061
+ "Defines the cell inserted at the root of the clock tree. Used in CTS.",
2062
+ pdk=True,
2063
+ ),
2064
+ Variable(
2065
+ "CTS_CLK_BUFFERS",
2066
+ List[str],
2067
+ "Defines the list of clock buffers to be used in CTS.",
2068
+ deprecated_names=["CTS_CLK_BUFFER_LIST"],
2069
+ pdk=True,
2070
+ ),
2071
+ Variable(
2072
+ "CTS_MAX_CAP",
2073
+ Optional[Decimal],
2074
+ "Overrides the maximum capacitance CTS characterization will test. If omitted, the capacitance is extracted from the lib information of the buffers in CTS_CLK_BUFFERS.",
2075
+ units="pF",
2076
+ ),
2077
+ Variable(
2078
+ "CTS_MAX_SLEW",
2079
+ Optional[Decimal],
2080
+ "Overrides the maximum transition time CTS characterization will test. If omitted, the slew is extracted from the lib information of the buffers in CTS_CLK_BUFFERS.",
2081
+ units="ns",
2082
+ ),
2083
+ ]
2084
+ )
2085
+
2086
+ def get_script_path(self):
2087
+ return os.path.join(get_script_dir(), "openroad", "cts.tcl")
2088
+
2089
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
2090
+ kwargs, env = self.extract_env(kwargs)
2091
+ if self.config.get("CLOCK_NET") is None:
2092
+ if clock_port := self.config["CLOCK_PORT"]:
2093
+ if isinstance(clock_port, list):
2094
+ env["CLOCK_NET"] = TclUtils.join(clock_port)
2095
+ else:
2096
+ env["CLOCK_NET"] = clock_port
2097
+ else:
2098
+ self.warn(
2099
+ "No CLOCK_NET (or CLOCK_PORT) specified. CTS cannot be performed. Returning state unaltered…"
2100
+ )
2101
+ return {}, {}
2102
+
2103
+ views_updates, metrics_updates = super().run(
2104
+ state_in, corners_key="CTS_CORNERS", env=env, **kwargs
2105
+ )
2106
+
2107
+ return views_updates, metrics_updates
2108
+
2109
+
2110
+ @Step.factory.register()
2111
+ class RepairDesignPostGPL(ResizerStep):
2112
+ """
2113
+ Runs a number of design "repairs" on a global-placed ODB file.
2114
+ """
2115
+
2116
+ id = "OpenROAD.RepairDesignPostGPL"
2117
+ name = "Repair Design (Post-Global Placement)"
2118
+
2119
+ config_vars = ResizerStep.config_vars + [
2120
+ Variable(
2121
+ "DESIGN_REPAIR_BUFFER_INPUT_PORTS",
2122
+ bool,
2123
+ "Specifies whether or not to insert buffers on input ports when design repairs are run.",
2124
+ default=True,
2125
+ deprecated_names=["PL_RESIZER_BUFFER_INPUT_PORTS"],
2126
+ ),
2127
+ Variable(
2128
+ "DESIGN_REPAIR_BUFFER_OUTPUT_PORTS",
2129
+ bool,
2130
+ "Specifies whether or not to insert buffers on input ports when design repairs are run.",
2131
+ default=True,
2132
+ deprecated_names=["PL_RESIZER_BUFFER_OUTPUT_PORTS"],
2133
+ ),
2134
+ Variable(
2135
+ "DESIGN_REPAIR_TIE_FANOUT",
2136
+ bool,
2137
+ "Specifies whether or not to repair tie cells fanout when design repairs are run.",
2138
+ default=True,
2139
+ deprecated_names=["PL_RESIZER_REPAIR_TIE_FANOUT"],
2140
+ ),
2141
+ Variable(
2142
+ "DESIGN_REPAIR_TIE_SEPARATION",
2143
+ bool,
2144
+ "Allows tie separation when performing design repairs.",
2145
+ default=False,
2146
+ deprecated_names=["PL_RESIZER_TIE_SEPERATION"],
2147
+ ),
2148
+ Variable(
2149
+ "DESIGN_REPAIR_MAX_WIRE_LENGTH",
2150
+ Decimal,
2151
+ "Specifies the maximum wire length cap used by resizer to insert buffers during design repair. If set to 0, no buffers will be inserted.",
2152
+ default=0,
2153
+ units="µm",
2154
+ deprecated_names=["PL_RESIZER_MAX_WIRE_LENGTH"],
2155
+ ),
2156
+ Variable(
2157
+ "DESIGN_REPAIR_MAX_SLEW_PCT",
2158
+ Decimal,
2159
+ "Specifies a margin for the slews during design repair.",
2160
+ default=20,
2161
+ units="%",
2162
+ deprecated_names=["PL_RESIZER_MAX_SLEW_MARGIN"],
2163
+ ),
2164
+ Variable(
2165
+ "DESIGN_REPAIR_MAX_CAP_PCT",
2166
+ Decimal,
2167
+ "Specifies a margin for the capacitances during design repair.",
2168
+ default=20,
2169
+ units="%",
2170
+ deprecated_names=["PL_RESIZER_MAX_CAP_MARGIN"],
2171
+ ),
2172
+ Variable(
2173
+ "DESIGN_REPAIR_REMOVE_BUFFERS",
2174
+ bool,
2175
+ "Invokes OpenROAD's remove_buffers command to remove buffers from synthesis, which gives OpenROAD more flexibility when buffering nets.",
2176
+ default=False,
2177
+ ),
2178
+ ]
2179
+
2180
+ def get_script_path(self):
2181
+ return os.path.join(get_script_dir(), "openroad", "repair_design.tcl")
2182
+
2183
+
2184
+ @Step.factory.register()
2185
+ class RepairDesign(RepairDesignPostGPL):
2186
+ """
2187
+ This is identical to OpenROAD.RepairDesignPostGPL. It is retained for backwards compatibility.
2188
+ """
2189
+
2190
+ id = "OpenROAD.RepairDesign"
2191
+ name = "Repair Design (Post-Global Placement)"
2192
+
2193
+
2194
+ @Step.factory.register()
2195
+ class RepairDesignPostGRT(ResizerStep):
2196
+ """
2197
+ Runs a number of design "repairs" on a global-routed ODB file.
2198
+ """
2199
+
2200
+ id = "OpenROAD.RepairDesignPostGRT"
2201
+ name = "Repair Design (Post-Global Routing)"
2202
+
2203
+ config_vars = ResizerStep.config_vars + [
2204
+ Variable(
2205
+ "GRT_DESIGN_REPAIR_RUN_GRT",
2206
+ bool,
2207
+ "Enables running GRT before and after running resizer",
2208
+ default=True,
2209
+ ),
2210
+ Variable(
2211
+ "GRT_DESIGN_REPAIR_MAX_WIRE_LENGTH",
2212
+ Decimal,
2213
+ "Specifies the maximum wire length cap used by resizer to insert buffers during post-grt design repair. If set to 0, no buffers will be inserted.",
2214
+ default=0,
2215
+ units="µm",
2216
+ deprecated_names=["GLB_RESIZER_MAX_WIRE_LENGTH"],
2217
+ ),
2218
+ Variable(
2219
+ "GRT_DESIGN_REPAIR_MAX_SLEW_PCT",
2220
+ Decimal,
2221
+ "Specifies a margin for the slews during post-grt design repair.",
2222
+ default=10,
2223
+ units="%",
2224
+ deprecated_names=["GLB_RESIZER_MAX_SLEW_MARGIN"],
2225
+ ),
2226
+ Variable(
2227
+ "GRT_DESIGN_REPAIR_MAX_CAP_PCT",
2228
+ Decimal,
2229
+ "Specifies a margin for the capacitances during design post-grt repair.",
2230
+ default=10,
2231
+ units="%",
2232
+ deprecated_names=["GLB_RESIZER_MAX_CAP_MARGIN"],
2233
+ ),
2234
+ ]
2235
+
2236
+ def get_script_path(self):
2237
+ return os.path.join(get_script_dir(), "openroad", "repair_design_postgrt.tcl")
2238
+
2239
+
2240
+ @Step.factory.register()
2241
+ class ResizerTimingPostCTS(ResizerStep):
2242
+ """
2243
+ First attempt to meet timing requirements for a cell based on basic timing
2244
+ information after clock tree synthesis.
2245
+
2246
+ Standard cells may be resized, and buffer cells may be inserted to ensure
2247
+ that no hold violations exist and no setup violations exist at the current
2248
+ clock.
2249
+ """
2250
+
2251
+ id = "OpenROAD.ResizerTimingPostCTS"
2252
+ name = "Resizer Timing Optimizations (Post-Clock Tree Synthesis)"
2253
+
2254
+ config_vars = ResizerStep.config_vars + [
2255
+ Variable(
2256
+ "PL_RESIZER_HOLD_SLACK_MARGIN",
2257
+ Decimal,
2258
+ "Specifies a time margin for the slack when fixing hold violations. Normally the resizer will stop when it reaches zero slack. This option allows you to overfix.",
2259
+ default=0.1,
2260
+ units="ns",
2261
+ ),
2262
+ Variable(
2263
+ "PL_RESIZER_SETUP_SLACK_MARGIN",
2264
+ Decimal,
2265
+ "Specifies a time margin for the slack when fixing setup violations.",
2266
+ default=0.05,
2267
+ units="ns",
2268
+ ),
2269
+ Variable(
2270
+ "PL_RESIZER_HOLD_MAX_BUFFER_PCT",
2271
+ Decimal,
2272
+ "Specifies a max number of buffers to insert to fix hold violations. This number is calculated as a percentage of the number of instances in the design.",
2273
+ default=50,
2274
+ deprecated_names=["PL_RESIZER_HOLD_MAX_BUFFER_PERCENT"],
2275
+ ),
2276
+ Variable(
2277
+ "PL_RESIZER_SETUP_MAX_BUFFER_PCT",
2278
+ Decimal,
2279
+ "Specifies a max number of buffers to insert to fix setup violations. This number is calculated as a percentage of the number of instances in the design.",
2280
+ default=50,
2281
+ units="%",
2282
+ deprecated_names=["PL_RESIZER_SETUP_MAX_BUFFER_PERCENT"],
2283
+ ),
2284
+ Variable(
2285
+ "PL_RESIZER_ALLOW_SETUP_VIOS",
2286
+ bool,
2287
+ "Allows the creation of setup violations when fixing hold violations. Setup violations are less dangerous as they simply mean a chip may not run at its rated speed, however, chips with hold violations are essentially dead-on-arrival.",
2288
+ default=False,
2289
+ ),
2290
+ Variable(
2291
+ "PL_RESIZER_GATE_CLONING",
2292
+ bool,
2293
+ "Enables gate cloning when attempting to fix setup violations",
2294
+ default=True,
2295
+ ),
2296
+ Variable(
2297
+ "PL_RESIZER_FIX_HOLD_FIRST",
2298
+ bool,
2299
+ "Experimental: attempt to fix hold violations before setup violations, which may lead to better timing results.",
2300
+ default=False,
2301
+ ),
2302
+ ]
2303
+
2304
+ def get_script_path(self):
2305
+ return os.path.join(get_script_dir(), "openroad", "rsz_timing_postcts.tcl")
2306
+
2307
+
2308
+ @Step.factory.register()
2309
+ class ResizerTimingPostGRT(ResizerStep):
2310
+ """
2311
+ Second attempt to meet timing requirements for a cell based on timing
2312
+ information after estimating resistance and capacitance values based on
2313
+ global routing.
2314
+
2315
+ Standard cells may be resized, and buffer cells may be inserted to ensure
2316
+ that no hold violations exist and no setup violations exist at the current
2317
+ clock.
2318
+ """
2319
+
2320
+ id = "OpenROAD.ResizerTimingPostGRT"
2321
+ name = "Resizer Timing Optimizations (Post-Global Routing)"
2322
+
2323
+ config_vars = ResizerStep.config_vars + [
2324
+ Variable(
2325
+ "GRT_RESIZER_HOLD_SLACK_MARGIN",
2326
+ Decimal,
2327
+ "Specifies a time margin for the slack when fixing hold violations. Normally the resizer will stop when it reaches zero slack. This option allows you to overfix.",
2328
+ default=0.05,
2329
+ units="ns",
2330
+ deprecated_names=["GLB_RESIZER_HOLD_SLACK_MARGIN"],
2331
+ ),
2332
+ Variable(
2333
+ "GRT_RESIZER_SETUP_SLACK_MARGIN",
2334
+ Decimal,
2335
+ "Specifies a time margin for the slack when fixing setup violations.",
2336
+ default=0.025,
2337
+ units="ns",
2338
+ deprecated_names=["GLB_RESIZER_SETUP_SLACK_MARGIN"],
2339
+ ),
2340
+ Variable(
2341
+ "GRT_RESIZER_HOLD_MAX_BUFFER_PCT",
2342
+ Decimal,
2343
+ "Specifies a max number of buffers to insert to fix hold violations. This number is calculated as a percentage of the number of instances in the design.",
2344
+ default=50,
2345
+ units="%",
2346
+ deprecated_names=["GLB_RESIZER_HOLD_MAX_BUFFER_PERCENT"],
2347
+ ),
2348
+ Variable(
2349
+ "GRT_RESIZER_SETUP_MAX_BUFFER_PCT",
2350
+ Decimal,
2351
+ "Specifies a max number of buffers to insert to fix setup violations. This number is calculated as a percentage of the number of instances in the design.",
2352
+ default=50,
2353
+ units="%",
2354
+ deprecated_names=["GLB_RESIZER_SETUP_MAX_BUFFER_PERCENT"],
2355
+ ),
2356
+ Variable(
2357
+ "GRT_RESIZER_ALLOW_SETUP_VIOS",
2358
+ bool,
2359
+ "Allows setup violations when fixing hold.",
2360
+ default=False,
2361
+ deprecated_names=["GLB_RESIZER_ALLOW_SETUP_VIOS"],
2362
+ ),
2363
+ Variable(
2364
+ "GRT_RESIZER_GATE_CLONING",
2365
+ bool,
2366
+ "Enables gate cloning when attempting to fix setup violations",
2367
+ default=True,
2368
+ ),
2369
+ Variable(
2370
+ "GRT_RESIZER_RUN_GRT",
2371
+ bool,
2372
+ "Gates running global routing after resizer steps. May be useful to disable for designs where global routing takes non-trivial time.",
2373
+ default=True,
2374
+ ),
2375
+ Variable(
2376
+ "GRT_RESIZER_FIX_HOLD_FIRST",
2377
+ bool,
2378
+ "Experimental: attempt to fix hold violations before setup violations, which may lead to better timing results.",
2379
+ default=False,
2380
+ ),
2381
+ ]
2382
+
2383
+ def get_script_path(self):
2384
+ return os.path.join(get_script_dir(), "openroad", "rsz_timing_postgrt.tcl")
2385
+
2386
+
2387
+ @Step.factory.register()
2388
+ class DEFtoODB(OpenROADStep):
2389
+ """
2390
+ Converts a DEF view to an ODB view.
2391
+
2392
+ Useful if you have a custom step that manipulates the layout outside of
2393
+ OpenROAD, but you would like to update the OpenROAD database.
2394
+ """
2395
+
2396
+ id = "OpenROAD.DEFtoODB"
2397
+ name = "DEF to OpenDB"
2398
+
2399
+ inputs = [DesignFormat.DEF]
2400
+ outputs = [DesignFormat.ODB]
2401
+
2402
+ def get_script_path(self) -> str:
2403
+ return os.path.join(get_script_dir(), "openroad", "write_views.tcl")
2404
+
2405
+
2406
+ @Step.factory.register()
2407
+ class OpenGUI(Step):
2408
+ """
2409
+ Opens the ODB view in the OpenROAD GUI. Useful to inspect some parameters,
2410
+ such as routing density and whatnot.
2411
+ """
2412
+
2413
+ id = "OpenROAD.OpenGUI"
2414
+ name = "Open In GUI"
2415
+
2416
+ inputs = [DesignFormat.ODB]
2417
+ outputs = []
2418
+
2419
+ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]:
2420
+ with tempfile.NamedTemporaryFile("a+", suffix=".tcl") as f:
2421
+ f.write(f"read_db \"{state_in['odb']}\"")
2422
+ f.flush()
2423
+
2424
+ subprocess.check_call(
2425
+ [
2426
+ "openroad",
2427
+ "-no_splash",
2428
+ "-gui",
2429
+ f.name,
2430
+ ]
2431
+ )
2432
+
2433
+ return {}, {}