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.
- librelane/__init__.py +1 -1
- librelane/__main__.py +34 -27
- librelane/common/__init__.py +2 -0
- librelane/common/cli.py +1 -1
- librelane/common/drc.py +1 -0
- librelane/common/generic_dict.py +1 -1
- librelane/common/metrics/__main__.py +1 -1
- librelane/common/misc.py +58 -2
- librelane/common/tcl.py +2 -1
- librelane/common/types.py +2 -3
- librelane/config/__main__.py +1 -4
- librelane/config/flow.py +2 -2
- librelane/config/preprocessor.py +1 -1
- librelane/config/variable.py +136 -7
- librelane/container.py +55 -31
- librelane/env_info.py +129 -115
- librelane/examples/hold_eco_demo/config.yaml +18 -0
- librelane/examples/hold_eco_demo/demo.v +27 -0
- librelane/flows/cli.py +39 -23
- librelane/flows/flow.py +100 -36
- librelane/help/__main__.py +39 -0
- librelane/scripts/magic/def/mag_gds.tcl +0 -2
- librelane/scripts/magic/drc.tcl +0 -1
- librelane/scripts/magic/gds/extras_mag.tcl +0 -2
- librelane/scripts/magic/gds/mag_with_pointers.tcl +0 -1
- librelane/scripts/magic/lef/extras_maglef.tcl +0 -2
- librelane/scripts/magic/lef/maglef.tcl +0 -1
- librelane/scripts/magic/wrapper.tcl +2 -0
- librelane/scripts/odbpy/defutil.py +15 -10
- librelane/scripts/odbpy/eco_buffer.py +182 -0
- librelane/scripts/odbpy/eco_diode.py +140 -0
- librelane/scripts/odbpy/ioplace_parser/__init__.py +1 -1
- librelane/scripts/odbpy/ioplace_parser/parse.py +1 -1
- librelane/scripts/odbpy/power_utils.py +8 -6
- librelane/scripts/odbpy/reader.py +17 -13
- librelane/scripts/openroad/common/io.tcl +66 -2
- librelane/scripts/openroad/gui.tcl +23 -1
- librelane/state/design_format.py +16 -1
- librelane/state/state.py +11 -3
- librelane/steps/__init__.py +1 -1
- librelane/steps/__main__.py +4 -4
- librelane/steps/checker.py +7 -8
- librelane/steps/klayout.py +11 -1
- librelane/steps/magic.py +24 -14
- librelane/steps/misc.py +5 -0
- librelane/steps/odb.py +193 -28
- librelane/steps/openroad.py +64 -47
- librelane/steps/pyosys.py +18 -1
- librelane/steps/step.py +36 -17
- librelane/steps/yosys.py +9 -1
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/METADATA +10 -11
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/RECORD +54 -50
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/entry_points.txt +1 -0
- librelane/scripts/odbpy/exception_codes.py +0 -17
- {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()
|
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
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
|
-
@
|
|
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 "[
|
|
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 "[
|
|
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
|
-
|
|
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
|
+
}
|
librelane/state/design_format.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
librelane/steps/__init__.py
CHANGED
librelane/steps/__main__.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
242
|
+
step.run_subprocess = partial(
|
|
243
243
|
step.run_subprocess,
|
|
244
244
|
_popen_callable=popen_substitute,
|
|
245
245
|
)
|
librelane/steps/checker.py
CHANGED
|
@@ -79,22 +79,21 @@ class MetricChecker(Step):
|
|
|
79
79
|
deferred: ClassVar[bool] = True
|
|
80
80
|
error_on_var: Optional[Variable] = None
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
85
|
+
threshold_string = str(cls.get_threshold(None))
|
|
87
86
|
dynamic_docstring = "Raises"
|
|
88
|
-
if
|
|
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 {
|
|
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().
|
|
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)
|
librelane/steps/klayout.py
CHANGED
|
@@ -268,8 +268,9 @@ class XOR(KLayoutStep):
|
|
|
268
268
|
Variable(
|
|
269
269
|
"KLAYOUT_XOR_TILE_SIZE",
|
|
270
270
|
Optional[int],
|
|
271
|
-
"
|
|
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 =
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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)"
|