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.
- librelane/__init__.py +38 -0
- librelane/__main__.py +479 -0
- librelane/__version__.py +43 -0
- librelane/common/__init__.py +63 -0
- librelane/common/cli.py +75 -0
- librelane/common/drc.py +246 -0
- librelane/common/generic_dict.py +319 -0
- librelane/common/metrics/__init__.py +35 -0
- librelane/common/metrics/__main__.py +413 -0
- librelane/common/metrics/library.py +354 -0
- librelane/common/metrics/metric.py +186 -0
- librelane/common/metrics/util.py +279 -0
- librelane/common/misc.py +456 -0
- librelane/common/ring_buffer.py +63 -0
- librelane/common/tcl.py +80 -0
- librelane/common/toolbox.py +549 -0
- librelane/common/tpe.py +41 -0
- librelane/common/types.py +116 -0
- librelane/config/__init__.py +32 -0
- librelane/config/__main__.py +155 -0
- librelane/config/config.py +1025 -0
- librelane/config/flow.py +490 -0
- librelane/config/pdk_compat.py +255 -0
- librelane/config/preprocessor.py +464 -0
- librelane/config/removals.py +45 -0
- librelane/config/variable.py +743 -0
- librelane/container.py +285 -0
- librelane/env_info.py +320 -0
- librelane/examples/spm/config.yaml +33 -0
- librelane/examples/spm/pin_order.cfg +14 -0
- librelane/examples/spm/src/impl.sdc +73 -0
- librelane/examples/spm/src/signoff.sdc +68 -0
- librelane/examples/spm/src/spm.v +73 -0
- librelane/examples/spm/verify/spm_tb.v +106 -0
- librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
- librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
- librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
- librelane/examples/spm-user_project_wrapper/config.json +13 -0
- librelane/examples/spm-user_project_wrapper/defines.v +66 -0
- librelane/examples/spm-user_project_wrapper/template.def +7656 -0
- librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
- librelane/flows/__init__.py +24 -0
- librelane/flows/builtins.py +18 -0
- librelane/flows/classic.py +327 -0
- librelane/flows/cli.py +463 -0
- librelane/flows/flow.py +1049 -0
- librelane/flows/misc.py +71 -0
- librelane/flows/optimizing.py +179 -0
- librelane/flows/sequential.py +367 -0
- librelane/flows/synth_explore.py +173 -0
- librelane/help/__main__.py +39 -0
- librelane/logging/__init__.py +40 -0
- librelane/logging/logger.py +323 -0
- librelane/open_pdks_rev +1 -0
- librelane/plugins.py +21 -0
- librelane/py.typed +0 -0
- librelane/scripts/base.sdc +80 -0
- librelane/scripts/klayout/Readme.md +2 -0
- librelane/scripts/klayout/open_design.py +63 -0
- librelane/scripts/klayout/render.py +121 -0
- librelane/scripts/klayout/stream_out.py +176 -0
- librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
- librelane/scripts/klayout/xor.drc +120 -0
- librelane/scripts/magic/Readme.md +1 -0
- librelane/scripts/magic/common/read.tcl +114 -0
- librelane/scripts/magic/def/antenna_check.tcl +35 -0
- librelane/scripts/magic/def/mag.tcl +19 -0
- librelane/scripts/magic/def/mag_gds.tcl +79 -0
- librelane/scripts/magic/drc.tcl +78 -0
- librelane/scripts/magic/extract_spice.tcl +98 -0
- librelane/scripts/magic/gds/drc_batch.tcl +74 -0
- librelane/scripts/magic/gds/erase_box.tcl +32 -0
- librelane/scripts/magic/gds/extras_mag.tcl +45 -0
- librelane/scripts/magic/gds/mag_with_pointers.tcl +31 -0
- librelane/scripts/magic/get_bbox.tcl +11 -0
- librelane/scripts/magic/lef/extras_maglef.tcl +61 -0
- librelane/scripts/magic/lef/maglef.tcl +26 -0
- librelane/scripts/magic/lef.tcl +57 -0
- librelane/scripts/magic/open.tcl +28 -0
- librelane/scripts/magic/wrapper.tcl +21 -0
- librelane/scripts/netgen/setup.tcl +28 -0
- librelane/scripts/odbpy/apply_def_template.py +49 -0
- librelane/scripts/odbpy/cell_frequency.py +107 -0
- librelane/scripts/odbpy/check_antenna_properties.py +116 -0
- librelane/scripts/odbpy/contextualize.py +109 -0
- librelane/scripts/odbpy/defutil.py +573 -0
- librelane/scripts/odbpy/diodes.py +373 -0
- librelane/scripts/odbpy/disconnected_pins.py +305 -0
- librelane/scripts/odbpy/eco_buffer.py +181 -0
- librelane/scripts/odbpy/eco_diode.py +139 -0
- librelane/scripts/odbpy/filter_unannotated.py +100 -0
- librelane/scripts/odbpy/io_place.py +482 -0
- librelane/scripts/odbpy/ioplace_parser/__init__.py +23 -0
- librelane/scripts/odbpy/ioplace_parser/parse.py +147 -0
- librelane/scripts/odbpy/label_macro_pins.py +277 -0
- librelane/scripts/odbpy/lefutil.py +97 -0
- librelane/scripts/odbpy/placers.py +162 -0
- librelane/scripts/odbpy/power_utils.py +397 -0
- librelane/scripts/odbpy/random_place.py +57 -0
- librelane/scripts/odbpy/reader.py +250 -0
- librelane/scripts/odbpy/remove_buffers.py +173 -0
- librelane/scripts/odbpy/snap_to_grid.py +57 -0
- librelane/scripts/odbpy/wire_lengths.py +93 -0
- librelane/scripts/openroad/antenna_check.tcl +20 -0
- librelane/scripts/openroad/antenna_repair.tcl +31 -0
- librelane/scripts/openroad/basic_mp.tcl +24 -0
- librelane/scripts/openroad/buffer_list.tcl +10 -0
- librelane/scripts/openroad/common/dpl.tcl +24 -0
- librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
- librelane/scripts/openroad/common/grt.tcl +32 -0
- librelane/scripts/openroad/common/io.tcl +540 -0
- librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
- librelane/scripts/openroad/common/resizer.tcl +103 -0
- librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
- librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
- librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
- librelane/scripts/openroad/common/set_rc.tcl +75 -0
- librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
- librelane/scripts/openroad/cts.tcl +80 -0
- librelane/scripts/openroad/cut_rows.tcl +24 -0
- librelane/scripts/openroad/dpl.tcl +24 -0
- librelane/scripts/openroad/drt.tcl +37 -0
- librelane/scripts/openroad/fill.tcl +30 -0
- librelane/scripts/openroad/floorplan.tcl +145 -0
- librelane/scripts/openroad/gpl.tcl +88 -0
- librelane/scripts/openroad/grt.tcl +30 -0
- librelane/scripts/openroad/gui.tcl +37 -0
- librelane/scripts/openroad/insert_buffer.tcl +127 -0
- librelane/scripts/openroad/ioplacer.tcl +67 -0
- librelane/scripts/openroad/irdrop.tcl +51 -0
- librelane/scripts/openroad/pdn.tcl +52 -0
- librelane/scripts/openroad/rcx.tcl +32 -0
- librelane/scripts/openroad/repair_design.tcl +70 -0
- librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
- librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
- librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
- librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
- librelane/scripts/openroad/sta/corner.tcl +393 -0
- librelane/scripts/openroad/tapcell.tcl +25 -0
- librelane/scripts/openroad/write_views.tcl +27 -0
- librelane/scripts/pyosys/construct_abc_script.py +177 -0
- librelane/scripts/pyosys/json_header.py +84 -0
- librelane/scripts/pyosys/synthesize.py +493 -0
- librelane/scripts/pyosys/ys_common.py +153 -0
- librelane/scripts/tclsh/hello.tcl +1 -0
- librelane/state/__init__.py +24 -0
- librelane/state/__main__.py +61 -0
- librelane/state/design_format.py +195 -0
- librelane/state/state.py +359 -0
- librelane/steps/__init__.py +61 -0
- librelane/steps/__main__.py +510 -0
- librelane/steps/checker.py +637 -0
- librelane/steps/common_variables.py +340 -0
- librelane/steps/cvc_rv.py +169 -0
- librelane/steps/klayout.py +509 -0
- librelane/steps/magic.py +576 -0
- librelane/steps/misc.py +160 -0
- librelane/steps/netgen.py +253 -0
- librelane/steps/odb.py +1088 -0
- librelane/steps/openroad.py +2460 -0
- librelane/steps/openroad_alerts.py +102 -0
- librelane/steps/pyosys.py +640 -0
- librelane/steps/step.py +1571 -0
- librelane/steps/tclstep.py +288 -0
- librelane/steps/verilator.py +222 -0
- librelane/steps/yosys.py +371 -0
- librelane-2.4.0.dist-info/METADATA +169 -0
- librelane-2.4.0.dist-info/RECORD +170 -0
- librelane-2.4.0.dist-info/WHEEL +4 -0
- librelane-2.4.0.dist-info/entry_points.txt +9 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
# Copyright 2023 Efabless Corporation
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import shlex
|
|
15
|
+
import inspect
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from decimal import Decimal, InvalidOperation
|
|
18
|
+
from dataclasses import (
|
|
19
|
+
_MISSING_TYPE,
|
|
20
|
+
MISSING,
|
|
21
|
+
asdict,
|
|
22
|
+
dataclass,
|
|
23
|
+
field,
|
|
24
|
+
fields,
|
|
25
|
+
is_dataclass,
|
|
26
|
+
)
|
|
27
|
+
from typing import (
|
|
28
|
+
ClassVar,
|
|
29
|
+
Dict,
|
|
30
|
+
List,
|
|
31
|
+
Literal,
|
|
32
|
+
Optional,
|
|
33
|
+
Set,
|
|
34
|
+
Tuple,
|
|
35
|
+
Union,
|
|
36
|
+
Mapping,
|
|
37
|
+
Callable,
|
|
38
|
+
Type,
|
|
39
|
+
Any,
|
|
40
|
+
get_origin,
|
|
41
|
+
get_args,
|
|
42
|
+
)
|
|
43
|
+
from ..state import DesignFormat, State
|
|
44
|
+
from ..common import GenericDict, Path, is_string, zip_first, Number, slugify
|
|
45
|
+
|
|
46
|
+
# Scalar = Union[Type[str], Type[Decimal], Type[Path], Type[bool]]
|
|
47
|
+
# VType = Union[Scalar, List[Scalar]]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MissingRequiredVariable(ValueError):
|
|
51
|
+
def __init__(self, variable: "Variable", pdk: bool = False) -> None:
|
|
52
|
+
self.variable = variable
|
|
53
|
+
if self.variable.pdk:
|
|
54
|
+
super().__init__(
|
|
55
|
+
f"Required PDK variable '{self.variable.name}' did not get a specified value. This PDK may be incompatible with your flow."
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
super().__init__(
|
|
59
|
+
f"Required variable '{self.variable.name}' did not get a specified value."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Orientation(str, Enum):
|
|
64
|
+
N = "N"
|
|
65
|
+
FN = "FN"
|
|
66
|
+
W = "W"
|
|
67
|
+
FW = "FW"
|
|
68
|
+
S = "S"
|
|
69
|
+
FS = "FS"
|
|
70
|
+
E = "E"
|
|
71
|
+
FE = "FE"
|
|
72
|
+
# OpenAccess
|
|
73
|
+
R0 = "N"
|
|
74
|
+
MY = "FN"
|
|
75
|
+
R90 = "W"
|
|
76
|
+
MXR90 = "FW"
|
|
77
|
+
R180 = "S"
|
|
78
|
+
MX = "FS"
|
|
79
|
+
R270 = "E"
|
|
80
|
+
MYR90 = "FE"
|
|
81
|
+
|
|
82
|
+
def __str__(self) -> str:
|
|
83
|
+
return self.value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Instance:
|
|
88
|
+
"""
|
|
89
|
+
Location information for an instance of a cell or macro.
|
|
90
|
+
|
|
91
|
+
:param location: The physical co-ordinates of the object's origin. Leave
|
|
92
|
+
empty for automatic placement.
|
|
93
|
+
:param orientation: The orientation of the object's placement. 'N'/'R0' by default.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
location: Optional[Tuple[Decimal, Decimal]]
|
|
97
|
+
orientation: Optional[Orientation]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class Macro:
|
|
102
|
+
"""
|
|
103
|
+
A data structure for storing definitions of Macros.
|
|
104
|
+
|
|
105
|
+
As it is typically stored in a dictionary in its superclass, the module name
|
|
106
|
+
is not stored in any of these fields.
|
|
107
|
+
|
|
108
|
+
You will note most fields correspond to a :class:`librelane.state.DesignFormat`
|
|
109
|
+
entry IDs. This is not coincidental.
|
|
110
|
+
|
|
111
|
+
:param gds: A list of GDSII files representing the design. At least one is required.
|
|
112
|
+
:param lef: A list of LEF files representing the design. At least one is required.
|
|
113
|
+
:param instances: A dictionary of :class:`Instance` objects representing the
|
|
114
|
+
instances of said macro.
|
|
115
|
+
|
|
116
|
+
The keys for the dictionaries are the names of the instances.
|
|
117
|
+
:param nl: A list of netlists constituting the design.
|
|
118
|
+
|
|
119
|
+
The netlists must be valid Verilog netlists readable by tools such as
|
|
120
|
+
OpenSTA.
|
|
121
|
+
|
|
122
|
+
Can be empty, however SPEF-based hierarchical static timing analysis
|
|
123
|
+
will be unavailable.
|
|
124
|
+
:param spef: A dictionary of parasitics annotations for the various netlists of the
|
|
125
|
+
Macro.
|
|
126
|
+
|
|
127
|
+
The keys are wildcards for timing corners supported by a certain SPEF file.
|
|
128
|
+
|
|
129
|
+
Can be empty, however SPEF-based hierarchical static timing analysis
|
|
130
|
+
will be unavailable.
|
|
131
|
+
:param lib: A dictionary of timing library files.
|
|
132
|
+
|
|
133
|
+
The keys are wildcards for timing corners supported by a certain LIB file.
|
|
134
|
+
|
|
135
|
+
If both SPEF and LIB views are empty, the design may be black-boxed
|
|
136
|
+
during STA.
|
|
137
|
+
:param spice: A list of SPICE netlists constituting the design. May be
|
|
138
|
+
useful in some flows.
|
|
139
|
+
:param sdf: A dictionary of standard delay format files. May be useful in some flows.
|
|
140
|
+
|
|
141
|
+
The keys are wildcards for timing corners supported by a certain SPEF file.
|
|
142
|
+
:param json_h: A JSON file as generated by Yosys. Helpful in some flows.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
gds: List[Path]
|
|
146
|
+
lef: List[Path]
|
|
147
|
+
instances: Dict[str, Instance] = field(default_factory=lambda: {})
|
|
148
|
+
|
|
149
|
+
vh: List[Path] = field(default_factory=lambda: [])
|
|
150
|
+
nl: List[Path] = field(default_factory=lambda: [])
|
|
151
|
+
pnl: List[Path] = field(default_factory=lambda: [])
|
|
152
|
+
spef: Dict[str, List[Path]] = field(default_factory=lambda: {})
|
|
153
|
+
lib: Dict[str, List[Path]] = field(default_factory=lambda: {})
|
|
154
|
+
spice: List[Path] = field(default_factory=lambda: [])
|
|
155
|
+
sdf: Dict[str, List[Path]] = field(default_factory=lambda: {})
|
|
156
|
+
|
|
157
|
+
json_h: Optional[Path] = None
|
|
158
|
+
|
|
159
|
+
def view_by_df(
|
|
160
|
+
self, df: DesignFormat
|
|
161
|
+
) -> Union[None, Path, List[Path], Dict[str, List[Path]]]:
|
|
162
|
+
try:
|
|
163
|
+
return getattr(self, df.value.id)
|
|
164
|
+
except AttributeError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def __post_init__(self):
|
|
168
|
+
if len(self.gds) < 1:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"Macro definition invalid- at least one GDSII file must be specified."
|
|
171
|
+
)
|
|
172
|
+
if len(self.lef) < 1:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"Macro definition invalid- at least one LEF file must be specified."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def __repr__(self) -> str:
|
|
178
|
+
return f"{self.__class__.__qualname__}(%s)" % ", ".join(
|
|
179
|
+
[f"{k}={repr(v)}" for k, v in asdict(self).items()]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def __str__(self) -> str:
|
|
183
|
+
return self.__repr__()
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_state(Self, state: State) -> "Macro":
|
|
187
|
+
kwargs = {}
|
|
188
|
+
for macro_field in fields(Self):
|
|
189
|
+
views = state.get(macro_field.name)
|
|
190
|
+
if views is None:
|
|
191
|
+
if macro_field.default_factory is not MISSING:
|
|
192
|
+
kwargs[macro_field.name] = macro_field.default_factory()
|
|
193
|
+
elif macro_field.default is not MISSING:
|
|
194
|
+
kwargs[macro_field.name] = macro_field.default
|
|
195
|
+
else: # gds or lef
|
|
196
|
+
raise ValueError(
|
|
197
|
+
f"Macro cannot be made out of input state: View {macro_field.name} is missing"
|
|
198
|
+
)
|
|
199
|
+
continue
|
|
200
|
+
var_name = f"{Self.__name__}.{macro_field.name}"
|
|
201
|
+
_, final = Variable(var_name, macro_field.type, "").compile(
|
|
202
|
+
GenericDict({var_name: views}),
|
|
203
|
+
warning_list_ref=[],
|
|
204
|
+
permissive_typing=True,
|
|
205
|
+
)
|
|
206
|
+
kwargs[macro_field.name] = final
|
|
207
|
+
|
|
208
|
+
return Self(**kwargs) # type: ignore
|
|
209
|
+
|
|
210
|
+
def instantiate(
|
|
211
|
+
self,
|
|
212
|
+
instance_name: str,
|
|
213
|
+
location: Tuple[Number, Number],
|
|
214
|
+
orientation: Orientation = Orientation.N,
|
|
215
|
+
):
|
|
216
|
+
location = (Decimal(location[0]), Decimal(location[1]))
|
|
217
|
+
self.instances[instance_name] = Instance(location, Orientation[orientation])
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def is_optional(t: Type[Any]) -> bool:
|
|
221
|
+
type_args = get_args(t)
|
|
222
|
+
return get_origin(t) is Union and type(None) in type_args
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def some_of(t: Type[Any]) -> Type[Any]:
|
|
226
|
+
if not is_optional(t):
|
|
227
|
+
return t
|
|
228
|
+
|
|
229
|
+
# t must be a Union with None if we're here
|
|
230
|
+
|
|
231
|
+
type_args = get_args(t)
|
|
232
|
+
|
|
233
|
+
args_without_none = [arg for arg in type_args if arg != type(None)]
|
|
234
|
+
if len(args_without_none) == 1:
|
|
235
|
+
return args_without_none[0]
|
|
236
|
+
|
|
237
|
+
new_union = Union[tuple(args_without_none)] # type: ignore
|
|
238
|
+
return new_union # type: ignore
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def repr_type(t: Type[Any], for_document: bool = False) -> str: # pragma: no cover
|
|
242
|
+
optional = is_optional(t)
|
|
243
|
+
some = some_of(t)
|
|
244
|
+
|
|
245
|
+
if hasattr(some, "__name__"): # Python 3.10+
|
|
246
|
+
type_string = some.__name__
|
|
247
|
+
else:
|
|
248
|
+
type_string = str(some)
|
|
249
|
+
|
|
250
|
+
if is_dataclass(t):
|
|
251
|
+
type_string = (
|
|
252
|
+
f"{{class}}`{some.__qualname__} <{some.__module__}.{some.__qualname__}>`"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
separator = "|<br />" if for_document else "|"
|
|
256
|
+
|
|
257
|
+
if inspect.isclass(some) and issubclass(some, Enum):
|
|
258
|
+
type_string = separator.join([str(e.name) for e in some])
|
|
259
|
+
type_string = f"`{type_string}`"
|
|
260
|
+
else:
|
|
261
|
+
origin, args = get_origin(some), get_args(some)
|
|
262
|
+
if origin is not None:
|
|
263
|
+
if origin == Union:
|
|
264
|
+
arg_strings = [repr_type(arg) for arg in args]
|
|
265
|
+
type_string = separator.join(arg_strings)
|
|
266
|
+
type_string = f"({type_string})"
|
|
267
|
+
elif origin == Literal:
|
|
268
|
+
return separator.join([repr(arg) for arg in args])
|
|
269
|
+
else:
|
|
270
|
+
arg_strings = [repr_type(arg) for arg in args]
|
|
271
|
+
type_string = f"{type_string}[{', '.join(arg_strings)}]"
|
|
272
|
+
|
|
273
|
+
return type_string + ("?" if optional else "")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class Variable:
|
|
278
|
+
"""
|
|
279
|
+
An object encapsulating metadata on an LibreLane configuration variable, which
|
|
280
|
+
is used to name, document and validate values supplied to
|
|
281
|
+
:class:`librelane.steps.Step`\\s or :class:`librelane.flows.Flow`\\s.
|
|
282
|
+
|
|
283
|
+
Values supplied for configuration variables are the primary interface by
|
|
284
|
+
which users configure LibreLane flows.
|
|
285
|
+
|
|
286
|
+
:param name: A string name for the Variable. Because of backwards compatibility
|
|
287
|
+
with LibreLane 1, the convention is ``UPPER_SNAKE_CASE``.
|
|
288
|
+
|
|
289
|
+
:param type: A Python type object representing the variable.
|
|
290
|
+
|
|
291
|
+
Supported scalars:
|
|
292
|
+
|
|
293
|
+
- ``int``
|
|
294
|
+
- ``decimal.Decimal``
|
|
295
|
+
- ``bool``
|
|
296
|
+
- ``str``
|
|
297
|
+
- :class:`Path`
|
|
298
|
+
|
|
299
|
+
Supported products:
|
|
300
|
+
|
|
301
|
+
- ``Union`` (incl. ``Optional``)
|
|
302
|
+
- ``List``
|
|
303
|
+
- ``Tuple``
|
|
304
|
+
- ``Dict``
|
|
305
|
+
- ``Enum``
|
|
306
|
+
|
|
307
|
+
Other:
|
|
308
|
+
|
|
309
|
+
- ``dataclass`` types composed of the above.
|
|
310
|
+
|
|
311
|
+
:param description: A human-readable description of the variable. Used to
|
|
312
|
+
generate help strings and documentation.
|
|
313
|
+
|
|
314
|
+
:param default: A default value for the variable.
|
|
315
|
+
|
|
316
|
+
Optional variables have an implicit default value of ``None``.
|
|
317
|
+
|
|
318
|
+
:param deprecated_names: A list of deprecated names for said variable.
|
|
319
|
+
|
|
320
|
+
An element of the list can alternative be a tuple of a name and a Callable
|
|
321
|
+
used to perform a translation for when a renamed variable is also slightly
|
|
322
|
+
modified.
|
|
323
|
+
|
|
324
|
+
:param units: Used only in documentation: the unit corresponding to this
|
|
325
|
+
object, i.e., µm, pF, etc. Can be any string, but for consistency, SI units
|
|
326
|
+
must be represented in terms of their official symbols.
|
|
327
|
+
|
|
328
|
+
:param pdk: Whether this variable is expected to be given a default value
|
|
329
|
+
by a PDK or not.
|
|
330
|
+
|
|
331
|
+
If this is true, and the variable is not of an option type, a PDK *must*
|
|
332
|
+
give this variable a default value in order to be marked compatible
|
|
333
|
+
with a step.
|
|
334
|
+
|
|
335
|
+
If this is true and the variable is of an option type, a PDK may
|
|
336
|
+
optionally provide a default value for this variable, however steps
|
|
337
|
+
must presume it is ``null``.
|
|
338
|
+
|
|
339
|
+
If this is false, a PDK is not allowed to set a default value for
|
|
340
|
+
this variable. In current versions of LibreLane, the value will be
|
|
341
|
+
silently ignored, but warnings or errors may occur in future versions.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
known_variable_names: ClassVar[Set[str]] = set()
|
|
345
|
+
|
|
346
|
+
name: str
|
|
347
|
+
type: Any
|
|
348
|
+
description: str
|
|
349
|
+
default: Any = None
|
|
350
|
+
deprecated_names: List[Union[str, Tuple[str, Callable]]] = field(
|
|
351
|
+
default_factory=list
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
units: Optional[str] = None
|
|
355
|
+
pdk: bool = False
|
|
356
|
+
|
|
357
|
+
def __post_init__(self):
|
|
358
|
+
Variable.known_variable_names.add(self.name)
|
|
359
|
+
for name in self.deprecated_names:
|
|
360
|
+
if isinstance(name, tuple):
|
|
361
|
+
name, _ = name
|
|
362
|
+
Variable.known_variable_names.add(name)
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def optional(self) -> bool:
|
|
366
|
+
"""
|
|
367
|
+
:returns: Whether a variable's type is an `Option type <https://en.wikipedia.org/wiki/Option_type>`_.
|
|
368
|
+
"""
|
|
369
|
+
return is_optional(self.type)
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def some(self) -> Any:
|
|
373
|
+
"""
|
|
374
|
+
:returns: The type of a variable presuming it is not None.
|
|
375
|
+
|
|
376
|
+
If a variable is not Optional, that is simply the type specified in the
|
|
377
|
+
``type`` attribute.
|
|
378
|
+
"""
|
|
379
|
+
return some_of(self.type)
|
|
380
|
+
|
|
381
|
+
def type_repr_md(self, for_document: bool = False) -> str: # pragma: no cover
|
|
382
|
+
"""
|
|
383
|
+
:param for_document: Adds HTML line breaks between sum type separators
|
|
384
|
+
for easier wrapping by web browsers/PDF renderers/what have you
|
|
385
|
+
:returns: A pretty Markdown string representation of the Variable's type.
|
|
386
|
+
"""
|
|
387
|
+
return repr_type(self.type, for_document=for_document)
|
|
388
|
+
|
|
389
|
+
def desc_repr_md(self) -> str: # pragma: no cover
|
|
390
|
+
"""
|
|
391
|
+
:returns: The description, but with newlines escaped for Markdown.
|
|
392
|
+
"""
|
|
393
|
+
return self.description.replace("\n", "<br />")
|
|
394
|
+
|
|
395
|
+
def __process(
|
|
396
|
+
self,
|
|
397
|
+
key_path: str,
|
|
398
|
+
value: Any,
|
|
399
|
+
validating_type: Type[Any],
|
|
400
|
+
default: Any = None,
|
|
401
|
+
explicitly_specified: bool = True,
|
|
402
|
+
permissive_typing: bool = False,
|
|
403
|
+
depth: int = 0,
|
|
404
|
+
):
|
|
405
|
+
if value is None:
|
|
406
|
+
if explicitly_specified:
|
|
407
|
+
# User explicitly specified "null" for this value: only error if
|
|
408
|
+
# value is not optional
|
|
409
|
+
if not is_optional(validating_type):
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"Non-optional variable '{key_path}' explicitly assigned a null value."
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
return None
|
|
415
|
+
else:
|
|
416
|
+
# User did not specify a value for this variable: couple outcomes
|
|
417
|
+
if default is not None:
|
|
418
|
+
return self.__process(
|
|
419
|
+
key_path=key_path,
|
|
420
|
+
value=default,
|
|
421
|
+
validating_type=validating_type,
|
|
422
|
+
permissive_typing=permissive_typing,
|
|
423
|
+
depth=depth + 1,
|
|
424
|
+
)
|
|
425
|
+
elif not is_optional(validating_type):
|
|
426
|
+
if depth == 0:
|
|
427
|
+
raise MissingRequiredVariable(self, self.pdk)
|
|
428
|
+
else:
|
|
429
|
+
raise ValueError(f"'{key_path}' must be non-null.")
|
|
430
|
+
else:
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
if is_optional(validating_type):
|
|
434
|
+
validating_type = some_of(validating_type)
|
|
435
|
+
|
|
436
|
+
type_origin = get_origin(validating_type)
|
|
437
|
+
type_args = get_args(validating_type)
|
|
438
|
+
|
|
439
|
+
if type_origin in [list, tuple]:
|
|
440
|
+
return_value = list()
|
|
441
|
+
raw = value
|
|
442
|
+
if isinstance(raw, list) or isinstance(raw, tuple):
|
|
443
|
+
if validating_type == List[Path]:
|
|
444
|
+
if any(isinstance(item, List) for item in raw):
|
|
445
|
+
Variable.__flatten_list(value)
|
|
446
|
+
pass
|
|
447
|
+
elif is_string(raw):
|
|
448
|
+
if not permissive_typing:
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"Refusing to automatically convert string at '{key_path}' to list"
|
|
451
|
+
)
|
|
452
|
+
if "," in raw:
|
|
453
|
+
raw = raw.split(",")
|
|
454
|
+
elif ";" in raw:
|
|
455
|
+
raw = raw.split(";")
|
|
456
|
+
else:
|
|
457
|
+
raw = raw.split()
|
|
458
|
+
if len(raw) and raw[-1] == "":
|
|
459
|
+
raw.pop() # Trailing commas
|
|
460
|
+
else:
|
|
461
|
+
raise ValueError(
|
|
462
|
+
f"List provided for variable '{key_path}' is invalid: {value}"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if type_origin == tuple:
|
|
466
|
+
if len(raw) != len(type_args):
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"Value provided for variable '{key_path}' of type {validating_type} is invalid: ({len(raw)}/{len(type_args)}) tuple entries provided"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
for i, (item, value_type) in enumerate(
|
|
472
|
+
zip_first(raw, type_args, fillvalue=type_args[0])
|
|
473
|
+
):
|
|
474
|
+
return_value.append(
|
|
475
|
+
self.__process(
|
|
476
|
+
key_path=f"{key_path}[{i}]",
|
|
477
|
+
value=item,
|
|
478
|
+
validating_type=value_type,
|
|
479
|
+
permissive_typing=permissive_typing,
|
|
480
|
+
depth=depth + 1,
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if type_origin == tuple:
|
|
485
|
+
return tuple(return_value)
|
|
486
|
+
|
|
487
|
+
return return_value
|
|
488
|
+
elif type_origin == dict:
|
|
489
|
+
raw = value
|
|
490
|
+
key_type, value_type = type_args
|
|
491
|
+
if isinstance(raw, dict):
|
|
492
|
+
pass
|
|
493
|
+
elif isinstance(raw, list) or is_string(raw):
|
|
494
|
+
if not permissive_typing:
|
|
495
|
+
raise ValueError(
|
|
496
|
+
f"Refusing to automatically convert string at '{key_path}' to dict"
|
|
497
|
+
)
|
|
498
|
+
components = raw
|
|
499
|
+
if is_string(raw):
|
|
500
|
+
components = shlex.split(raw)
|
|
501
|
+
assert isinstance(components, list)
|
|
502
|
+
# Assuming Tcl format:
|
|
503
|
+
if len(components) % 2 != 0:
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"Tcl-style flat dictionary provided for variable '{key_path}' is invalid: uneven number of components ({len(components)})"
|
|
506
|
+
)
|
|
507
|
+
raw = {}
|
|
508
|
+
for i in range(0, len(components) // 2):
|
|
509
|
+
key = components[2 * i]
|
|
510
|
+
val = components[2 * i + 1]
|
|
511
|
+
raw[key] = val
|
|
512
|
+
else:
|
|
513
|
+
raise ValueError(
|
|
514
|
+
f"Value provided for variable '{key_path}' of type {validating_type} is invalid: '{value}'"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
processed = {}
|
|
518
|
+
for key, val in raw.items():
|
|
519
|
+
key_validated = self.__process(
|
|
520
|
+
key_path=key_path,
|
|
521
|
+
value=key,
|
|
522
|
+
validating_type=key_type,
|
|
523
|
+
permissive_typing=permissive_typing,
|
|
524
|
+
depth=depth + 1,
|
|
525
|
+
)
|
|
526
|
+
value_validated = self.__process(
|
|
527
|
+
key_path=f"{key_path}.{key_validated}",
|
|
528
|
+
value=val,
|
|
529
|
+
validating_type=value_type,
|
|
530
|
+
permissive_typing=permissive_typing,
|
|
531
|
+
depth=depth + 1,
|
|
532
|
+
)
|
|
533
|
+
processed[key_validated] = value_validated
|
|
534
|
+
|
|
535
|
+
return processed
|
|
536
|
+
elif type_origin == Union:
|
|
537
|
+
final_value = None
|
|
538
|
+
errors = []
|
|
539
|
+
for arg in type_args:
|
|
540
|
+
try:
|
|
541
|
+
final_value = self.__process(
|
|
542
|
+
key_path=key_path,
|
|
543
|
+
value=value,
|
|
544
|
+
validating_type=arg,
|
|
545
|
+
permissive_typing=permissive_typing,
|
|
546
|
+
depth=depth + 1,
|
|
547
|
+
)
|
|
548
|
+
if final_value is not None:
|
|
549
|
+
return final_value
|
|
550
|
+
except ValueError as e:
|
|
551
|
+
errors.append(f"\t{str(e)}")
|
|
552
|
+
raise ValueError(
|
|
553
|
+
"\n".join(
|
|
554
|
+
[
|
|
555
|
+
f"Value for '{key_path}' is invalid for union {repr_type(validating_type)}:"
|
|
556
|
+
]
|
|
557
|
+
+ errors
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
elif type_origin == Literal:
|
|
561
|
+
if value in type_args:
|
|
562
|
+
return value
|
|
563
|
+
else:
|
|
564
|
+
raise ValueError(
|
|
565
|
+
f"Value for '{key_path}' is invalid for {repr_type(validating_type)}: '{value}'"
|
|
566
|
+
)
|
|
567
|
+
elif is_dataclass(validating_type):
|
|
568
|
+
if isinstance(value, validating_type):
|
|
569
|
+
# Do not validate further
|
|
570
|
+
return value
|
|
571
|
+
|
|
572
|
+
raw = value
|
|
573
|
+
if not isinstance(raw, dict):
|
|
574
|
+
raise ValueError(
|
|
575
|
+
f"Value provided for deserializable class {validating_type} at '{key_path}' is not a dictionary."
|
|
576
|
+
)
|
|
577
|
+
raw = value.copy()
|
|
578
|
+
kwargs_dict = {}
|
|
579
|
+
for current_field in fields(validating_type):
|
|
580
|
+
key = current_field.name
|
|
581
|
+
subtype = current_field.type
|
|
582
|
+
explicitly_specified = False
|
|
583
|
+
if key in raw:
|
|
584
|
+
explicitly_specified = True
|
|
585
|
+
field_value = raw.get(key)
|
|
586
|
+
field_default = None
|
|
587
|
+
if (
|
|
588
|
+
current_field.default is not None
|
|
589
|
+
and type(current_field.default) != _MISSING_TYPE
|
|
590
|
+
):
|
|
591
|
+
field_default = current_field.default
|
|
592
|
+
if current_field.default_factory != MISSING:
|
|
593
|
+
field_default = current_field.default_factory()
|
|
594
|
+
value__processed = self.__process(
|
|
595
|
+
key_path=f"{key_path}.{key}",
|
|
596
|
+
value=field_value,
|
|
597
|
+
explicitly_specified=explicitly_specified,
|
|
598
|
+
default=field_default,
|
|
599
|
+
validating_type=subtype,
|
|
600
|
+
permissive_typing=permissive_typing,
|
|
601
|
+
depth=depth + 1,
|
|
602
|
+
)
|
|
603
|
+
kwargs_dict[key] = value__processed
|
|
604
|
+
if explicitly_specified:
|
|
605
|
+
del raw[key]
|
|
606
|
+
if len(raw):
|
|
607
|
+
raise ValueError(
|
|
608
|
+
f"One or more keys unrecognized for dataclass {validating_type.__qualname__}: {' '.join(raw.keys())}"
|
|
609
|
+
)
|
|
610
|
+
return validating_type(**kwargs_dict)
|
|
611
|
+
elif validating_type == Path:
|
|
612
|
+
# Handle one-file globs
|
|
613
|
+
if isinstance(value, list) and len(value) == 1:
|
|
614
|
+
value = value[0]
|
|
615
|
+
result = Path(value)
|
|
616
|
+
result.validate(f"Path provided for variable '{key_path}' is invalid")
|
|
617
|
+
return result
|
|
618
|
+
elif validating_type == bool:
|
|
619
|
+
if not permissive_typing and not isinstance(value, bool):
|
|
620
|
+
raise ValueError(
|
|
621
|
+
f"Refusing to automatically convert '{value}' at '{key_path}' to a Boolean"
|
|
622
|
+
)
|
|
623
|
+
if value in ["1", "true", "True", 1, True]:
|
|
624
|
+
return True
|
|
625
|
+
elif value in ["0", "false", "False", 0, False]:
|
|
626
|
+
return False
|
|
627
|
+
else:
|
|
628
|
+
raise ValueError(
|
|
629
|
+
f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}'"
|
|
630
|
+
)
|
|
631
|
+
elif issubclass(validating_type, Enum):
|
|
632
|
+
if type(value) == validating_type:
|
|
633
|
+
return value
|
|
634
|
+
try:
|
|
635
|
+
return validating_type[value]
|
|
636
|
+
except KeyError:
|
|
637
|
+
raise ValueError(
|
|
638
|
+
f"Variable provided for variable '{key_path}' of enumerated type {validating_type.__name__} is invalid: '{value}'"
|
|
639
|
+
)
|
|
640
|
+
elif issubclass(validating_type, str):
|
|
641
|
+
if not is_string(value):
|
|
642
|
+
raise ValueError(
|
|
643
|
+
f"Refusing to automatically convert value at '{key_path}' to a string"
|
|
644
|
+
)
|
|
645
|
+
return str(value)
|
|
646
|
+
elif issubclass(validating_type, Decimal) or issubclass(validating_type, int):
|
|
647
|
+
try:
|
|
648
|
+
final = validating_type(value)
|
|
649
|
+
except (InvalidOperation, TypeError):
|
|
650
|
+
raise ValueError(
|
|
651
|
+
f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}'"
|
|
652
|
+
)
|
|
653
|
+
if not permissive_typing and not (
|
|
654
|
+
isinstance(value, int)
|
|
655
|
+
or isinstance(value, float)
|
|
656
|
+
or isinstance(value, Decimal)
|
|
657
|
+
):
|
|
658
|
+
raise ValueError(
|
|
659
|
+
f"Refusing to automatically convert value at '{key_path}' to a {validating_type.__name__}"
|
|
660
|
+
)
|
|
661
|
+
return final
|
|
662
|
+
|
|
663
|
+
else:
|
|
664
|
+
try:
|
|
665
|
+
return validating_type(value)
|
|
666
|
+
except ValueError as e:
|
|
667
|
+
raise ValueError(
|
|
668
|
+
f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}' {e}"
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
def compile(
|
|
672
|
+
self,
|
|
673
|
+
mutable_config: GenericDict[str, Any],
|
|
674
|
+
warning_list_ref: List[str],
|
|
675
|
+
values_so_far: Optional[Mapping[str, Any]] = None,
|
|
676
|
+
permissive_typing: bool = False,
|
|
677
|
+
) -> Tuple[Optional[str], Any]:
|
|
678
|
+
exists: Optional[str] = None
|
|
679
|
+
value: Optional[Any] = None
|
|
680
|
+
|
|
681
|
+
i = 0
|
|
682
|
+
while (
|
|
683
|
+
not exists
|
|
684
|
+
and self.deprecated_names is not None
|
|
685
|
+
and i < len(self.deprecated_names)
|
|
686
|
+
):
|
|
687
|
+
deprecated_name = self.deprecated_names[i]
|
|
688
|
+
deprecated_callable = lambda x: x
|
|
689
|
+
if not isinstance(deprecated_name, str):
|
|
690
|
+
deprecated_name, deprecated_callable = deprecated_name
|
|
691
|
+
exists, value = mutable_config.check(deprecated_name)
|
|
692
|
+
if exists:
|
|
693
|
+
warning_list_ref.append(
|
|
694
|
+
f"The configuration variable '{deprecated_name}' is deprecated. Please check the docs for the usage on the replacement variable '{self.name}'."
|
|
695
|
+
)
|
|
696
|
+
if value is not None:
|
|
697
|
+
value = deprecated_callable(value)
|
|
698
|
+
i = i + 1
|
|
699
|
+
|
|
700
|
+
if not exists:
|
|
701
|
+
exists, value = mutable_config.check(self.name)
|
|
702
|
+
|
|
703
|
+
processed = self.__process(
|
|
704
|
+
key_path=self.name,
|
|
705
|
+
value=value,
|
|
706
|
+
default=self.default,
|
|
707
|
+
validating_type=self.type,
|
|
708
|
+
explicitly_specified=exists is not None,
|
|
709
|
+
permissive_typing=permissive_typing,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
return (exists, processed)
|
|
713
|
+
|
|
714
|
+
def _get_docs_identifier(self, parent: Optional[str] = None) -> str:
|
|
715
|
+
identifier = f"var-{self.name.lower()}"
|
|
716
|
+
if parent is not None:
|
|
717
|
+
identifier = f"var-{slugify(parent)}-{self.name.lower()}"
|
|
718
|
+
return identifier
|
|
719
|
+
|
|
720
|
+
def __hash__(self) -> int:
|
|
721
|
+
return hash((self.name, self.type, self.default))
|
|
722
|
+
|
|
723
|
+
def __eq__(self, rhs: object) -> bool:
|
|
724
|
+
if not isinstance(rhs, Variable):
|
|
725
|
+
raise NotImplementedError()
|
|
726
|
+
return (
|
|
727
|
+
self.name == rhs.name
|
|
728
|
+
and self.type == rhs.type
|
|
729
|
+
and self.default == rhs.default
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# Flatten list. Note: Must modify value, not return a new list.
|
|
733
|
+
@staticmethod
|
|
734
|
+
def __flatten_list(value: list):
|
|
735
|
+
new_list = []
|
|
736
|
+
for item in value:
|
|
737
|
+
if isinstance(item, list):
|
|
738
|
+
for sub_item in item:
|
|
739
|
+
new_list.append(sub_item)
|
|
740
|
+
else:
|
|
741
|
+
new_list.append(item)
|
|
742
|
+
|
|
743
|
+
value[:] = new_list
|