librelane 2.4.0.dev2__py3-none-any.whl → 2.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. librelane/__init__.py +1 -1
  2. librelane/__main__.py +34 -27
  3. librelane/common/__init__.py +2 -0
  4. librelane/common/cli.py +1 -1
  5. librelane/common/drc.py +1 -0
  6. librelane/common/generic_dict.py +1 -1
  7. librelane/common/metrics/__main__.py +1 -1
  8. librelane/common/misc.py +58 -2
  9. librelane/common/tcl.py +2 -1
  10. librelane/common/types.py +2 -3
  11. librelane/config/__main__.py +1 -4
  12. librelane/config/flow.py +2 -2
  13. librelane/config/preprocessor.py +1 -1
  14. librelane/config/variable.py +136 -7
  15. librelane/container.py +55 -31
  16. librelane/env_info.py +129 -115
  17. librelane/examples/hold_eco_demo/config.yaml +18 -0
  18. librelane/examples/hold_eco_demo/demo.v +27 -0
  19. librelane/flows/cli.py +39 -23
  20. librelane/flows/flow.py +100 -36
  21. librelane/help/__main__.py +39 -0
  22. librelane/scripts/magic/def/mag_gds.tcl +0 -2
  23. librelane/scripts/magic/drc.tcl +0 -1
  24. librelane/scripts/magic/gds/extras_mag.tcl +0 -2
  25. librelane/scripts/magic/gds/mag_with_pointers.tcl +0 -1
  26. librelane/scripts/magic/lef/extras_maglef.tcl +0 -2
  27. librelane/scripts/magic/lef/maglef.tcl +0 -1
  28. librelane/scripts/magic/wrapper.tcl +2 -0
  29. librelane/scripts/odbpy/defutil.py +15 -10
  30. librelane/scripts/odbpy/eco_buffer.py +182 -0
  31. librelane/scripts/odbpy/eco_diode.py +140 -0
  32. librelane/scripts/odbpy/ioplace_parser/__init__.py +1 -1
  33. librelane/scripts/odbpy/ioplace_parser/parse.py +1 -1
  34. librelane/scripts/odbpy/power_utils.py +8 -6
  35. librelane/scripts/odbpy/reader.py +17 -13
  36. librelane/scripts/openroad/common/io.tcl +66 -2
  37. librelane/scripts/openroad/gui.tcl +23 -1
  38. librelane/state/design_format.py +16 -1
  39. librelane/state/state.py +11 -3
  40. librelane/steps/__init__.py +1 -1
  41. librelane/steps/__main__.py +4 -4
  42. librelane/steps/checker.py +7 -8
  43. librelane/steps/klayout.py +11 -1
  44. librelane/steps/magic.py +24 -14
  45. librelane/steps/misc.py +5 -0
  46. librelane/steps/odb.py +193 -28
  47. librelane/steps/openroad.py +64 -47
  48. librelane/steps/pyosys.py +18 -1
  49. librelane/steps/step.py +36 -17
  50. librelane/steps/yosys.py +9 -1
  51. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/METADATA +10 -11
  52. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/RECORD +54 -50
  53. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/entry_points.txt +1 -0
  54. librelane/scripts/odbpy/exception_codes.py +0 -17
  55. {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright 2025 Efabless Corporation
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ import sys
16
+ from reader import click_odb, click, odb
17
+ import grt as GRT
18
+
19
+
20
+ @click.command()
21
+ @click_odb
22
+ def cli(reader):
23
+ grt = reader.design.getGlobalRouter()
24
+ dpl = reader.design.getOpendp()
25
+
26
+ insts_to_temporarily_lock_then_unlock_later = []
27
+ for inst in reader.block.getInsts():
28
+ if inst.getPlacementStatus() != "LOCKED":
29
+ insts_to_temporarily_lock_then_unlock_later.append(
30
+ (inst, inst.getPlacementStatus())
31
+ )
32
+ inst.setPlacementStatus("LOCKED")
33
+
34
+ reader._grt_setup(grt)
35
+
36
+ diode_master, diode_pin = reader.config["DIODE_CELL"].split("/")
37
+
38
+ # print(grt)
39
+ grt_inc = GRT.IncrementalGRoute(grt, reader.block)
40
+ i = 0
41
+ diodes = reader.config["INSERT_ECO_DIODES"] or []
42
+ for target_info in diodes:
43
+ target_name, target_pin = target_info["target"].split("/")
44
+ name_escaped = reader.escape_verilog_name(target_name)
45
+
46
+ target = reader.block.findInst(name_escaped)
47
+ if target is None:
48
+ print(
49
+ f"[ERROR] Instance '{target_name}' not found.",
50
+ file=sys.stderr,
51
+ )
52
+ exit(-1)
53
+
54
+ master = reader.db.findMaster(diode_master)
55
+ if master is None:
56
+ print(
57
+ f"[ERROR] Cell kind '{diode_master}' not found.",
58
+ file=sys.stderr,
59
+ )
60
+ exit(-1)
61
+
62
+ target_iterm = target.findITerm(target_pin)
63
+ if target_iterm is None:
64
+ print(
65
+ f"[ERROR] Pin '{target_pin}' not found for instance {target_name}.",
66
+ file=sys.stderr,
67
+ )
68
+ exit(-1)
69
+
70
+ if target_iterm.getIoType() not in ["INPUT", "INOUT"]:
71
+ print(
72
+ f"[ERROR] Pin {target_info['target']} is an OUTPUT pin.",
73
+ file=sys.stderr,
74
+ )
75
+ exit(-1)
76
+
77
+ net = target_iterm.getNet()
78
+ if net is None:
79
+ print(
80
+ f"[ERROR] Pin {target_info['target']} has no nets connected.",
81
+ file=sys.stderr,
82
+ )
83
+ exit(-1)
84
+
85
+ eco_diode_name = f"eco_diode_{i}"
86
+ while reader.block.findInst(eco_diode_name) is not None:
87
+ i += 1
88
+ eco_diode_name = f"eco_diode_{i}"
89
+
90
+ eco_diode = odb.dbInst.create(
91
+ reader.block,
92
+ master,
93
+ eco_diode_name,
94
+ )
95
+
96
+ diode_iterm = eco_diode.findITerm(diode_pin)
97
+ if diode_iterm is None:
98
+ print(
99
+ f"[ERROR] Pin '{diode_pin}' on ECO diode not found- invalid DIODE_CELL definition.",
100
+ file=sys.stderr,
101
+ )
102
+ exit(-1)
103
+
104
+ sys.stdout.flush()
105
+
106
+ if target_info["placement"] is not None:
107
+ x, y = target_info["placement"]
108
+ x = reader.block.micronsToDbu(float(x))
109
+ y = reader.block.micronsToDbu(float(y))
110
+ else:
111
+ x, y = target.getLocation()
112
+
113
+ eco_diode.setOrient("R0")
114
+ eco_diode.setLocation(x, y)
115
+ eco_diode.setPlacementStatus("PLACED")
116
+
117
+ diode_iterm.connect(net)
118
+ grt.addDirtyNet(net)
119
+
120
+ site = reader.rows[0].getSite()
121
+ max_disp_x = int(
122
+ reader.design.micronToDBU(reader.config["PL_MAX_DISPLACEMENT_X"])
123
+ / site.getWidth()
124
+ )
125
+ max_disp_y = int(
126
+ reader.design.micronToDBU(reader.config["PL_MAX_DISPLACEMENT_Y"])
127
+ / site.getHeight()
128
+ )
129
+ dpl.detailedPlacement(max_disp_x, max_disp_y)
130
+
131
+ grt_inc.updateRoutes(True)
132
+
133
+ for inst, previous_status in insts_to_temporarily_lock_then_unlock_later:
134
+ inst.setPlacementStatus(previous_status)
135
+
136
+ reader.design.writeDef("out.def")
137
+
138
+
139
+ if __name__ == "__main__":
140
+ cli()
@@ -1,4 +1,4 @@
1
- # Copyright 2025 The American University in Cairo
1
+ # Copyright 2025 LibreLane Contributors
2
2
  #
3
3
  # Adapted from ioplace_parser
4
4
  #
@@ -1,4 +1,4 @@
1
- # Copyright 2025 The American University in Cairo
1
+ # Copyright 2025 LibreLane Contributors
2
2
  #
3
3
  # Adapted from ioplace_parser
4
4
  #
@@ -17,7 +17,6 @@ import utl
17
17
 
18
18
  import re
19
19
  import json
20
- import functools
21
20
  from dataclasses import dataclass
22
21
  from typing import Dict, List, Optional
23
22
 
@@ -49,11 +48,14 @@ class Design(object):
49
48
  def get_verilog_net_name_by_bit(self, top_module: str, target_bit: int):
50
49
  yosys_design_object = self.yosys_dict["modules"][top_module]
51
50
  if top_module not in self.verilog_net_names_by_bit_by_module:
52
- self.verilog_net_names_by_bit_by_module[top_module] = functools.reduce(
53
- lambda a, b: {**a, **{bit: b[0] for bit in b[1]["bits"]}},
54
- yosys_design_object["netnames"].items(),
55
- {},
56
- )
51
+ # check git history for a version of this loop that is drunk on power
52
+ netname_by_bit = {}
53
+
54
+ for netname, info in yosys_design_object["netnames"].items():
55
+ for bit in info["bits"]:
56
+ netname_by_bit[bit] = netname
57
+
58
+ self.verilog_net_names_by_bit_by_module[top_module] = netname_by_bit
57
59
  return self.verilog_net_names_by_bit_by_module[top_module][target_bit]
58
60
 
59
61
  def get_pins(self, module_name: str) -> Dict[str, odb.dbMTerm]:
@@ -20,7 +20,7 @@ import sys
20
20
  import json
21
21
  import locale
22
22
  import inspect
23
- import functools
23
+ from functools import wraps
24
24
  from decimal import Decimal
25
25
  from fnmatch import fnmatch
26
26
  from typing import Callable, Dict
@@ -131,16 +131,9 @@ class OdbReader(object):
131
131
  dpl.reportLegalizationStats()
132
132
  dpl.optimizeMirroring()
133
133
 
134
- def _grt(self):
135
- """
136
- The ``._grt()`` method is EXPERIMENTAL and SHOULD NOT BE USED YET.
137
-
138
- Use a composite step with ``OpenROAD.GlobalRouting``.
139
- """
140
- if self.config is None:
141
- raise RuntimeError("Attempted to call grt without config file")
134
+ def _grt_setup(self, grt):
135
+ grt.setAdjustment(float(self.config["GRT_ADJUSTMENT"]))
142
136
 
143
- grt = self.design.getGlobalRouter()
144
137
  routing_layers = [l for l in self.layers.values() if l.getRoutingLevel() >= 1]
145
138
  for layer, adj in zip(routing_layers, self.config["GRT_LAYER_ADJUSTMENTS"]):
146
139
  grt.addLayerAdjustment(
@@ -170,21 +163,32 @@ class OdbReader(object):
170
163
  raise RuntimeError(f"Unknown layer name '{max_clk_name}'")
171
164
  max_clk_idx = self.layers[max_clk_name].getRoutingLevel()
172
165
 
173
- grt.setMinRoutingLayer(min_layer_idx)
174
- grt.setMaxRoutingLayer(max_layer_idx)
175
166
  grt.setMinLayerForClock(min_clk_idx)
176
167
  grt.setMaxLayerForClock(max_clk_idx)
177
168
  grt.setMacroExtension(self.config["GRT_MACRO_EXTENSION"])
178
169
  grt.setOverflowIterations(self.config["GRT_OVERFLOW_ITERS"])
179
170
  grt.setAllowCongestion(self.config["GRT_ALLOW_CONGESTION"])
180
171
  grt.setVerbose(True)
172
+ grt.initFastRoute(min_layer_idx, max_layer_idx)
173
+
174
+ def _grt(self):
175
+ """
176
+ The ``._grt()`` method is EXPERIMENTAL and SHOULD NOT BE USED YET.
177
+
178
+ Use a composite step with ``OpenROAD.GlobalRouting``.
179
+ """
180
+ if self.config is None:
181
+ raise RuntimeError("Attempted to call grt without config file")
182
+
183
+ grt = self.design.getGlobalRouter()
184
+ self._grt_setup(grt)
181
185
  grt.globalRoute(
182
186
  True
183
187
  ) # The first variable updates guides- not sure why the default is False
184
188
 
185
189
 
186
190
  def click_odb(function):
187
- @functools.wraps(function)
191
+ @wraps(function)
188
192
  def wrapper(input_db, input_lefs, config_path, **kwargs):
189
193
  reader = OdbReader(input_db, config_path=config_path)
190
194
 
@@ -316,6 +316,70 @@ proc read_current_odb {args} {
316
316
  set_dont_use_cells
317
317
  }
318
318
 
319
+ proc _populate_cells_by_class {} {
320
+ if { [info exists ::_cells_by_class(physical)] } {
321
+ return
322
+ }
323
+
324
+ set ::_cells_by_class(physical) [list]
325
+ set ::_cells_by_class(non_timing) [list]
326
+ set _comment_ {
327
+ We naïvely assume anything not in these classes is not a cell with a
328
+ logical function. This may not be comprehensive, but is good enough.
329
+
330
+ CORE just means a macro used in the core area (i.e. a standard cell.)
331
+
332
+ Thing is, it has a lot of subclasses for physical cells:
333
+
334
+ `FEEDTHRU`,`SPACER`,`ANTENNACELL`,`WELLTAP`
335
+
336
+ Only `TIEHIGH`, `TIELOW` are for logical cells. Thus, the inclusion
337
+ list allows them as well. `BLOCKS` are macros, which we cannot discern
338
+ whether they have a logical function or not, so we include them
339
+ regardless.
340
+
341
+ We do make one exception for `ANTENNACELL`s. These are not counted as
342
+ logical cells but they are not exempt from the so-called SDF-friendly
343
+ netlist as they do affect timing ever so slightly.
344
+ }
345
+ set logical_classes {
346
+ BLOCK
347
+ BUMP
348
+ CORE
349
+ CORE_TIEHIGH
350
+ CORE_TIELOW
351
+ COVER
352
+ PAD
353
+ PAD_AREAIO
354
+ PAD_INOUT
355
+ PAD_INPUT
356
+ PAD_OUTPUT
357
+ PAD_POWER
358
+ PAD_SPACER
359
+ }
360
+
361
+ foreach lib $::libs {
362
+ foreach master [$lib getMasters] {
363
+ if { [lsearch -exact $logical_classes [$master getType]] == -1 } {
364
+ lappend ::_cells_by_class(physical) [$master getName]
365
+ if { "[$master getType]" != "CORE_ANTENNACELL" } {
366
+ lappend ::_cells_by_class(non_timing) [$master getName]
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ proc get_timing_excluded_cells {args} {
374
+ _populate_cells_by_class
375
+ return $::_cells_by_class(non_timing)
376
+ }
377
+
378
+ proc get_physical_cells {args} {
379
+ _populate_cells_by_class
380
+ return $::_cells_by_class(physical)
381
+ }
382
+
319
383
  proc write_views {args} {
320
384
  # This script will attempt to write views based on existing "SAVE_"
321
385
  # environment variables. If the SAVE_ variable exists, the script will
@@ -349,7 +413,7 @@ proc write_views {args} {
349
413
  }
350
414
 
351
415
  if { [info exists ::env(SAVE_POWERED_NETLIST_SDF_FRIENDLY)] } {
352
- set exclude_cells "[join $::env(FILL_CELL)] [join $::env(DECAP_CELL)] [join $::env(WELLTAP_CELL)] [join $::env(ENDCAP_CELL)]"
416
+ set exclude_cells "[get_timing_excluded_cells]"
353
417
  puts "Writing nofill powered netlist to '$::env(SAVE_POWERED_NETLIST_SDF_FRIENDLY)'…"
354
418
  puts "Excluding $exclude_cells"
355
419
  write_verilog -include_pwr_gnd \
@@ -358,7 +422,7 @@ proc write_views {args} {
358
422
  }
359
423
 
360
424
  if { [info exists ::env(SAVE_POWERED_NETLIST_NO_PHYSICAL_CELLS)] } {
361
- set exclude_cells "[join [lindex [split $::env(DIODE_CELL) "/"] 0]] [join $::env(FILL_CELL)] [join $::env(DECAP_CELL)] [join $::env(WELLTAP_CELL)] [join $::env(ENDCAP_CELL)]"
425
+ set exclude_cells "[get_physical_cells]"
362
426
  puts "Writing nofilldiode powered netlist to '$::env(SAVE_POWERED_NETLIST_NO_PHYSICAL_CELLS)'…"
363
427
  puts "Excluding $exclude_cells"
364
428
  write_verilog -include_pwr_gnd \
@@ -12,4 +12,26 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  source $::env(SCRIPTS_DIR)/openroad/common/io.tcl
15
- read_current_odb
15
+
16
+ puts "Reading OpenROAD database at '$::env(CURRENT_ODB)'…"
17
+ if { [ catch {read_db $::env(CURRENT_ODB)} errmsg ]} {
18
+ puts stderr $errmsg
19
+ exit 1
20
+ }
21
+
22
+ set_global_vars
23
+
24
+ define_corners $::env(DEFAULT_CORNER)
25
+
26
+ foreach lib $::env(_PNR_LIBS) {
27
+ puts "Reading library file at '$lib'…"
28
+ read_liberty $lib
29
+ }
30
+
31
+ read_current_sdc
32
+
33
+ if { [info exists ::env(_CURRENT_SPEF_BY_CORNER)] } {
34
+ set corner_name $::env(_CURRENT_CORNER_NAME)
35
+ puts "Reading top-level design parasitics for the '$corner_name' corner at '$::env(_CURRENT_SPEF_BY_CORNER)'…"
36
+ read_spef -corner $corner_name $::env(_CURRENT_SPEF_BY_CORNER)
37
+ }
@@ -12,8 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  from enum import Enum
15
- from dataclasses import dataclass
16
15
  from typing import Dict, Optional
16
+ from dataclasses import dataclass, replace
17
17
 
18
18
 
19
19
  @dataclass
@@ -44,10 +44,16 @@ class DesignFormatObject:
44
44
  folder_override: Optional[str] = None
45
45
  multiple: bool = False
46
46
 
47
+ _instance_optional: bool = False
48
+
47
49
  @property
48
50
  def folder(self) -> str:
49
51
  return self.folder_override or self.id
50
52
 
53
+ @property
54
+ def optional(self) -> bool:
55
+ return self._instance_optional
56
+
51
57
 
52
58
  class DesignFormat(Enum):
53
59
  """
@@ -174,6 +180,15 @@ class DesignFormat(Enum):
174
180
  def by_id(id: str) -> Optional["DesignFormat"]:
175
181
  return _designformat_by_id.get(id)
176
182
 
183
+ def mkOptional(self) -> "DesignFormat":
184
+ # HACK: Create ephemeral DesignFormat copy until 3.0.0 lets us do this
185
+ # a bit more appropriately.
186
+ clone = object.__new__(DesignFormat)
187
+ clone._name_ = self._name_
188
+ clone._value_ = replace(self._value_)
189
+ clone._value_._instance_optional = True
190
+ return clone
191
+
177
192
 
178
193
  _designformat_by_id: Dict[str, "DesignFormat"] = {
179
194
  format.value.id: format for format in DesignFormat
librelane/state/state.py CHANGED
@@ -13,7 +13,9 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ import io
16
17
  import os
18
+ import csv
17
19
  import sys
18
20
  import json
19
21
  import shutil
@@ -214,14 +216,20 @@ class State(GenericImmutableDict[str, StateElement]):
214
216
  self._walk(self, path, visitor)
215
217
  metrics_csv_path = os.path.join(path, "metrics.csv")
216
218
  with open(metrics_csv_path, "w", encoding="utf8") as f:
217
- f.write("Metric,Value\n")
218
- for metric in self.metrics:
219
- f.write(f"{metric},{self.metrics[metric]}\n")
219
+ self.metrics_to_csv(f)
220
220
 
221
221
  metrics_json_path = os.path.join(path, "metrics.json")
222
222
  with open(metrics_json_path, "w", encoding="utf8") as f:
223
223
  f.write(self.metrics.dumps())
224
224
 
225
+ def metrics_to_csv(
226
+ self, fp: io.TextIOWrapper, metrics_object: Optional[Dict[str, Any]] = None
227
+ ):
228
+ w = csv.writer(fp)
229
+ w.writerow(("Metric", "Value"))
230
+ for entry in (metrics_object or self.metrics).items():
231
+ w.writerow(entry)
232
+
225
233
  def validate(self):
226
234
  """
227
235
  Ensures that all paths exist in a State.
@@ -48,7 +48,7 @@ from . import openroad as OpenROAD
48
48
  from .openroad import OpenROADStep
49
49
 
50
50
  from . import odb as Odb
51
- from .odb import OdbpyStep
51
+ from .odb import OdbpyStep, ECOBuffer, ECODiode
52
52
 
53
53
  from . import magic as Magic
54
54
  from .magic import MagicStep
@@ -15,7 +15,6 @@ import os
15
15
  import shlex
16
16
  import shutil
17
17
  import datetime
18
- import functools
19
18
  import subprocess
20
19
  from functools import partial
21
20
  from typing import IO, Any, Dict, Optional, Sequence, Union
@@ -48,8 +47,9 @@ def load_step_from_inputs(
48
47
  if Found := Step.factory.get(id):
49
48
  Target = Found
50
49
  else:
51
- err(
52
- f"No step registered with id '{id}'. Ensure all relevant plugins are installed."
50
+ err(f"No step registered with id '{id}'.")
51
+ info(
52
+ f"If the step '{id}' is part of a plugin, make sure the plugin's parent directory is in the PYTHONPATH environment variable."
53
53
  )
54
54
  ctx.exit(-1)
55
55
 
@@ -239,7 +239,7 @@ def eject(ctx, output, state_in, config, id):
239
239
  found_stdin_data = found_stdin.read()
240
240
  raise Stop()
241
241
 
242
- step.run_subprocess = functools.partial(
242
+ step.run_subprocess = partial(
243
243
  step.run_subprocess,
244
244
  _popen_callable=popen_substitute,
245
245
  )
@@ -79,22 +79,21 @@ class MetricChecker(Step):
79
79
  deferred: ClassVar[bool] = True
80
80
  error_on_var: Optional[Variable] = None
81
81
 
82
- @classmethod
83
- def get_help_md(Self, **kwargs): # pragma: no cover
84
- threshold_string = Self.get_threshold_description(None)
82
+ def __init_subclass__(cls):
83
+ threshold_string = cls.get_threshold_description(None)
85
84
  if threshold_string is None:
86
- threshold_string = str(Self.get_threshold(None))
85
+ threshold_string = str(cls.get_threshold(None))
87
86
  dynamic_docstring = "Raises"
88
- if Self.deferred:
87
+ if cls.deferred:
89
88
  dynamic_docstring += " a deferred error"
90
89
  else:
91
90
  dynamic_docstring += " an immediate error"
92
- dynamic_docstring += f" if {Self.metric_description} (metric: ``{Self.metric_name}``) are >= {threshold_string}."
91
+ dynamic_docstring += f" if {cls.metric_description} (metric: ``{cls.metric_name}``) are >= {threshold_string}."
93
92
  dynamic_docstring += (
94
93
  " Doesn't raise an error depending on error_on_var if defined."
95
94
  )
96
-
97
- return super().get_help_md(docstring_override=dynamic_docstring, **kwargs)
95
+ cls.__doc__ = dynamic_docstring
96
+ return super().__init_subclass__()
98
97
 
99
98
  def get_threshold(self: Optional["MetricChecker"]) -> Optional[Decimal]:
100
99
  return Decimal(0)
@@ -268,8 +268,9 @@ class XOR(KLayoutStep):
268
268
  Variable(
269
269
  "KLAYOUT_XOR_TILE_SIZE",
270
270
  Optional[int],
271
- "A tile size for the XOR process in µm.",
271
+ "The tile size to parallelize the XOR process with.",
272
272
  pdk=True,
273
+ units="µm",
273
274
  ),
274
275
  ]
275
276
 
@@ -327,6 +328,15 @@ class XOR(KLayoutStep):
327
328
 
328
329
  @Step.factory.register()
329
330
  class DRC(KLayoutStep):
331
+ """
332
+ Runs DRC using KLayout.
333
+
334
+ Unlike most steps, the KLayout scripts vary quite wildly by PDK. If a PDK
335
+ is not supported by this step, it will simply be skipped.
336
+
337
+ Currently, only sky130A and sky130B are supported.
338
+ """
339
+
330
340
  id = "KLayout.DRC"
331
341
  name = "Design Rule Check (KLayout)"
332
342
 
librelane/steps/magic.py CHANGED
@@ -14,7 +14,6 @@
14
14
  import os
15
15
  import re
16
16
  import shutil
17
- import functools
18
17
  import subprocess
19
18
  from signal import SIGKILL
20
19
  from decimal import Decimal
@@ -34,7 +33,8 @@ from .tclstep import TclStep
34
33
  from ..state import DesignFormat, State
35
34
 
36
35
  from ..config import Variable
37
- from ..common import get_script_dir, DRC as DRCObject, Path, mkdirp
36
+ from ..common import get_script_dir, DRC as DRCObject, Path, mkdirp, count_occurences
37
+ from ..logging import warn
38
38
 
39
39
 
40
40
  class MagicOutputProcessor(OutputProcessor):
@@ -464,6 +464,12 @@ class SpiceExtraction(MagicStep):
464
464
  "Extracts a SPICE netlist based on black-boxed standard cells and macros (basically, anything with a LEF) rather than transistors. An error will be thrown if both this and `MAGIC_EXT_USE_GDS` is set to ``True``.",
465
465
  default=False,
466
466
  ),
467
+ Variable(
468
+ "MAGIC_FEEDBACK_CONVERSION_THRESHOLD",
469
+ int,
470
+ "If Magic provides more feedback items than this threshold, conversion to KLayout databases is skipped (as something has gone horribly wrong.)",
471
+ default=10000,
472
+ ),
467
473
  ]
468
474
 
469
475
  def get_script_path(self):
@@ -481,22 +487,29 @@ class SpiceExtraction(MagicStep):
481
487
 
482
488
  views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
483
489
 
484
- cif_scale = Decimal(open(os.path.join(self.step_dir, "cif_scale.txt")).read())
485
490
  feedback_path = os.path.join(self.step_dir, "feedback.txt")
491
+ with open(feedback_path, encoding="utf8") as f:
492
+ illegal_overlap_count = count_occurences(f, "Illegal overlap")
493
+
494
+ metrics_updates["magic__illegal_overlap__count"] = illegal_overlap_count
495
+ threshold = self.config["MAGIC_FEEDBACK_CONVERSION_THRESHOLD"]
496
+ if illegal_overlap_count > threshold:
497
+ warn(
498
+ f"Not converting the feedback to the KLayout database format: {illegal_overlap_count} > MAGIC_FEEDBACK_CONVERSION_THRESHOLD ({threshold}). You may manually increase the threshold, but it might take forever."
499
+ )
500
+ return views_updates, metrics_updates
501
+
502
+ cif_scale = Decimal(open(os.path.join(self.step_dir, "cif_scale.txt")).read())
486
503
  try:
487
504
  se_feedback, _ = DRCObject.from_magic_feedback(
488
505
  open(feedback_path, encoding="utf8"),
489
506
  cif_scale,
490
507
  self.config["DESIGN_NAME"],
491
508
  )
492
- illegal_overlap_count = functools.reduce(
493
- lambda a, b: a + len(b.bounding_boxes),
494
- [
495
- v
496
- for v in se_feedback.violations.values()
497
- if "Illegal overlap" in v.description
498
- ],
499
- 0,
509
+ illegal_overlap_count = sum(
510
+ len(v.bounding_boxes)
511
+ for v in se_feedback.violations.values()
512
+ if "Illegal overlap" in v.description
500
513
  )
501
514
  with open(os.path.join(self.step_dir, "feedback.xml"), "wb") as f:
502
515
  se_feedback.to_klayout_xml(f)
@@ -505,9 +518,6 @@ class SpiceExtraction(MagicStep):
505
518
  self.warn(
506
519
  f"Failed to convert SPICE extraction feedback to KLayout database format: {e}"
507
520
  )
508
- metrics_updates["magic__illegal_overlap__count"] = (
509
- open(feedback_path, encoding="utf8").read().count("Illegal overlap")
510
- )
511
521
  return views_updates, metrics_updates
512
522
 
513
523
 
librelane/steps/misc.py CHANGED
@@ -53,6 +53,11 @@ class LoadBaseSDC(Step):
53
53
 
54
54
  @Step.factory.register()
55
55
  class ReportManufacturability(Step):
56
+ """
57
+ Logs a simple "manufacturability report", i.e., the status of DRC, LVS, and
58
+ antenna violations.
59
+ """
60
+
56
61
  id = "Misc.ReportManufacturability"
57
62
  name = "Report Manufacturability"
58
63
  long_name = "Report Manufacturability (DRC, LVS, Antenna)"