librelane 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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