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
librelane/flows/flow.py
ADDED
|
@@ -0,0 +1,1049 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
15
|
+
import os
|
|
16
|
+
import glob
|
|
17
|
+
import shutil
|
|
18
|
+
import fnmatch
|
|
19
|
+
import logging
|
|
20
|
+
import datetime
|
|
21
|
+
import textwrap
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from abc import abstractmethod, ABC
|
|
24
|
+
from concurrent.futures import Future
|
|
25
|
+
from functools import wraps
|
|
26
|
+
from typing import (
|
|
27
|
+
List,
|
|
28
|
+
Sequence,
|
|
29
|
+
Tuple,
|
|
30
|
+
Type,
|
|
31
|
+
ClassVar,
|
|
32
|
+
Optional,
|
|
33
|
+
Dict,
|
|
34
|
+
Callable,
|
|
35
|
+
TypeVar,
|
|
36
|
+
Union,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from rich.progress import (
|
|
40
|
+
Progress,
|
|
41
|
+
TextColumn,
|
|
42
|
+
BarColumn,
|
|
43
|
+
MofNCompleteColumn,
|
|
44
|
+
TimeElapsedColumn,
|
|
45
|
+
TaskID,
|
|
46
|
+
)
|
|
47
|
+
from deprecated.sphinx import deprecated
|
|
48
|
+
from librelane.common.types import Path
|
|
49
|
+
|
|
50
|
+
from ..config import Config, Variable, universal_flow_config_variables, AnyConfigs
|
|
51
|
+
from ..state import State, DesignFormat, DesignFormatObject
|
|
52
|
+
from ..steps import Step, StepNotFound
|
|
53
|
+
from ..logging import (
|
|
54
|
+
LevelFilter,
|
|
55
|
+
console,
|
|
56
|
+
info,
|
|
57
|
+
warn,
|
|
58
|
+
verbose,
|
|
59
|
+
register_additional_handler,
|
|
60
|
+
deregister_additional_handler,
|
|
61
|
+
options,
|
|
62
|
+
)
|
|
63
|
+
from ..common import (
|
|
64
|
+
get_tpe,
|
|
65
|
+
mkdirp,
|
|
66
|
+
protected,
|
|
67
|
+
final,
|
|
68
|
+
slugify,
|
|
69
|
+
Toolbox,
|
|
70
|
+
get_latest_file,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FlowError(RuntimeError):
|
|
75
|
+
"""
|
|
76
|
+
A ``RuntimeError`` that occurs when a Flow, or one of its underlying Steps,
|
|
77
|
+
fails to finish execution properly.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FlowException(FlowError):
|
|
84
|
+
"""
|
|
85
|
+
A variant of :class:`FlowError` for unexpected failures or failures due
|
|
86
|
+
to misconfiguration, such as:
|
|
87
|
+
|
|
88
|
+
* A :class:`StepException` raised by an underlying Step
|
|
89
|
+
* Invalid inputs
|
|
90
|
+
* Mis-use of class interfaces of the :class:`Flow`
|
|
91
|
+
* Other unexpected failures
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
T = TypeVar("T", bound=Callable)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def ensure_progress_started(method: T) -> Callable:
|
|
101
|
+
"""
|
|
102
|
+
If a method of :class:`FlowProgressBar`decorated with `ensure_started`
|
|
103
|
+
and :meth:`start` had not been called yet, a :class:`FlowException` will be
|
|
104
|
+
thrown.
|
|
105
|
+
|
|
106
|
+
The docstring will also be amended to reflect that fact.
|
|
107
|
+
|
|
108
|
+
:param method: The method of :class:`FlowProgressBar` in question.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@wraps(method)
|
|
112
|
+
def _impl(obj: FlowProgressBar, *method_args, **method_kwargs):
|
|
113
|
+
if not obj.started:
|
|
114
|
+
raise FlowException(
|
|
115
|
+
f"Attempted to call method '{method}' before initializing progress bar"
|
|
116
|
+
)
|
|
117
|
+
return method(obj, *method_args, **method_kwargs)
|
|
118
|
+
|
|
119
|
+
if method.__doc__ is None:
|
|
120
|
+
method.__doc__ = ""
|
|
121
|
+
|
|
122
|
+
method.__doc__ = (
|
|
123
|
+
"This method may not be called before the progress bar is started.\n"
|
|
124
|
+
+ method.__doc__
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return _impl
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FlowProgressBar(object):
|
|
131
|
+
"""
|
|
132
|
+
A wrapper for a flow's progress bar, rendered using Rich at the bottom of
|
|
133
|
+
interactive terminals.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, flow_name: str, starting_ordinal: int = 1) -> None:
|
|
137
|
+
self.__flow_name: str = flow_name
|
|
138
|
+
self.__stages_completed: int = 0
|
|
139
|
+
self.__max_stage: int = 0
|
|
140
|
+
self.__task_id: TaskID = TaskID(-1)
|
|
141
|
+
self.__ordinal: int = starting_ordinal
|
|
142
|
+
self.__progress = Progress(
|
|
143
|
+
TextColumn("[progress.description]{task.description}"),
|
|
144
|
+
BarColumn(),
|
|
145
|
+
MofNCompleteColumn(),
|
|
146
|
+
TimeElapsedColumn(),
|
|
147
|
+
console=console,
|
|
148
|
+
disable=not options.get_show_progress_bar(),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def start(self):
|
|
152
|
+
"""
|
|
153
|
+
Starts rendering the progress bar.
|
|
154
|
+
"""
|
|
155
|
+
self.__progress.start()
|
|
156
|
+
self.__task_id = self.__progress.add_task(
|
|
157
|
+
f"{self.__flow_name}",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def end(self):
|
|
161
|
+
"""
|
|
162
|
+
Stops rendering the progress bar.
|
|
163
|
+
"""
|
|
164
|
+
self.__progress.stop()
|
|
165
|
+
self.__task_id = TaskID(-1)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def started(self) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
:returns: If the progress bar has started or not
|
|
171
|
+
"""
|
|
172
|
+
return self.__task_id != TaskID(-1)
|
|
173
|
+
|
|
174
|
+
@ensure_progress_started
|
|
175
|
+
def set_max_stage_count(self, count: int):
|
|
176
|
+
"""
|
|
177
|
+
A helper function, used to set the total number of stages the progress
|
|
178
|
+
bar is expected to keep tally of.
|
|
179
|
+
|
|
180
|
+
:param count: The total number of stages.
|
|
181
|
+
"""
|
|
182
|
+
self.__max_stage = count
|
|
183
|
+
self.__progress.update(self.__task_id, total=count)
|
|
184
|
+
|
|
185
|
+
@ensure_progress_started
|
|
186
|
+
def start_stage(self, name: str):
|
|
187
|
+
"""
|
|
188
|
+
Starts a new stage, updating the progress bar appropriately.
|
|
189
|
+
|
|
190
|
+
:param name: The name of the stage.
|
|
191
|
+
"""
|
|
192
|
+
self.__progress.update(
|
|
193
|
+
self.__task_id,
|
|
194
|
+
description=f"{self.__flow_name} - Stage {self.__stages_completed + 1} - {name}",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@ensure_progress_started
|
|
198
|
+
def end_stage(self, *, increment_ordinal: bool = True):
|
|
199
|
+
"""
|
|
200
|
+
Ends the current stage, updating the progress bar appropriately.
|
|
201
|
+
|
|
202
|
+
:param increment_ordinal: Increment the step ordinal, which is used in the creation of step directories.
|
|
203
|
+
|
|
204
|
+
You may want to set this to ``False`` if the stage is being skipped.
|
|
205
|
+
|
|
206
|
+
Please note that step ordinal is not equal to stages- a skipped step
|
|
207
|
+
increments the stage but not the step ordinal.
|
|
208
|
+
"""
|
|
209
|
+
self.__stages_completed += 1
|
|
210
|
+
if increment_ordinal:
|
|
211
|
+
self.__ordinal += 1
|
|
212
|
+
self.__progress.update(self.__task_id, completed=float(self.__stages_completed))
|
|
213
|
+
|
|
214
|
+
@ensure_progress_started
|
|
215
|
+
def get_ordinal_prefix(self) -> str:
|
|
216
|
+
"""
|
|
217
|
+
:returns: A string with the current step ordinal, which can be
|
|
218
|
+
used to create a step directory.
|
|
219
|
+
"""
|
|
220
|
+
max_stage_digits = len(str(self.__max_stage))
|
|
221
|
+
return f"{str(self.__ordinal).zfill(max_stage_digits)}-"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Flow(ABC):
|
|
225
|
+
"""
|
|
226
|
+
An abstract base class for a flow.
|
|
227
|
+
|
|
228
|
+
Flows encapsulate a the running of multiple :class:`Step`\\s in any order.
|
|
229
|
+
The sequence (or lack thereof) of running the steps is left to the Flow
|
|
230
|
+
itself.
|
|
231
|
+
|
|
232
|
+
The Flow ABC offers a number of convenience functions, including handling the
|
|
233
|
+
progress bar at the bottom of the terminal, which shows what stage the flow
|
|
234
|
+
is currently in and the remaining stages.
|
|
235
|
+
|
|
236
|
+
:param config: Either a resolved :class:`librelane.config.Config` object, or an
|
|
237
|
+
input to :meth:`librelane.config.Config.load`.
|
|
238
|
+
|
|
239
|
+
:param name: An optional string name for the Flow itself, and not a run of it.
|
|
240
|
+
|
|
241
|
+
If not provided, there are two fallbacks:
|
|
242
|
+
|
|
243
|
+
* The value of the ``name`` property (``NotImplemented`` by default)
|
|
244
|
+
* The name of the concrete ``Flow`` class
|
|
245
|
+
|
|
246
|
+
:param config_override_strings: See :meth:`librelane.config.Config.load`
|
|
247
|
+
:param pdk: See :meth:`librelane.config.Config.load`
|
|
248
|
+
:param pdk_root: See :meth:`librelane.config.Config.load`
|
|
249
|
+
:param scl: See :meth:`librelane.config.Config.load`
|
|
250
|
+
:param design_dir: See :meth:`librelane.config.Config.load`
|
|
251
|
+
|
|
252
|
+
:cvar Steps:
|
|
253
|
+
A list of :class:`Step` **types** used by the Flow (not Step objects.)
|
|
254
|
+
|
|
255
|
+
Subclasses of :class:`Flow` are expected to override the default value
|
|
256
|
+
as a class member- but subclasses may allow this value to be further
|
|
257
|
+
overridden during construction (and only then.)
|
|
258
|
+
|
|
259
|
+
:class:`Flow` subclasses without the ``Steps`` class property declared
|
|
260
|
+
are considered abstract and cannot be initialized.
|
|
261
|
+
|
|
262
|
+
:cvar config_vars:
|
|
263
|
+
A list of **flow-specific** configuration variables. These configuration
|
|
264
|
+
variables are used entirely within the logic of the flow itself and
|
|
265
|
+
are not exposed to ``Step``\\(s).
|
|
266
|
+
|
|
267
|
+
:ivar step_objects:
|
|
268
|
+
A list of :class:`Step` **objects** from the last run of the flow,
|
|
269
|
+
if it exists.
|
|
270
|
+
|
|
271
|
+
If :meth:`start` is called again, the reference is destroyed.
|
|
272
|
+
|
|
273
|
+
:ivar run_dir:
|
|
274
|
+
The directory of the last run of the flow, if it exists.
|
|
275
|
+
|
|
276
|
+
If :meth:`start` is called again, the reference is destroyed.
|
|
277
|
+
|
|
278
|
+
:ivar toolbox:
|
|
279
|
+
The :class:`Toolbox` of the last run of the flow, if it exists.
|
|
280
|
+
|
|
281
|
+
If :meth:`start` is called again, the reference is destroyed.
|
|
282
|
+
|
|
283
|
+
:ivar config_resolved_path:
|
|
284
|
+
The path to the serialization of the resolved configuration for the
|
|
285
|
+
last run of the flow.
|
|
286
|
+
|
|
287
|
+
If :meth:`start` is called again, the reference is destroyed.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
class _StepWarningHandler(logging.Handler):
|
|
291
|
+
@dataclass
|
|
292
|
+
class Record:
|
|
293
|
+
message: str
|
|
294
|
+
step: Optional[str] = None
|
|
295
|
+
repeats: int = 0
|
|
296
|
+
similar: int = 0
|
|
297
|
+
|
|
298
|
+
def __str__(self) -> str:
|
|
299
|
+
prefix = ""
|
|
300
|
+
if self.step is not None:
|
|
301
|
+
prefix = f"[{self.step}] "
|
|
302
|
+
postfix = ""
|
|
303
|
+
if self.repeats + self.similar:
|
|
304
|
+
postfix = f"and {self.repeats + self.similar} similar warnings"
|
|
305
|
+
if len(postfix):
|
|
306
|
+
postfix = f" ({postfix})"
|
|
307
|
+
return f"{prefix}{self.message}{postfix}"
|
|
308
|
+
|
|
309
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
310
|
+
super().__init__(*args, **kwargs)
|
|
311
|
+
self.warnings: Dict[str, Flow._StepWarningHandler.Record] = {}
|
|
312
|
+
|
|
313
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
314
|
+
step = None
|
|
315
|
+
if hasattr(record, "step"):
|
|
316
|
+
step = record.step
|
|
317
|
+
|
|
318
|
+
key = record.key if hasattr(record, "key") else record.msg
|
|
319
|
+
if key in self.warnings:
|
|
320
|
+
existing = self.warnings[key]
|
|
321
|
+
if record.msg == existing.message:
|
|
322
|
+
existing.repeats += 1
|
|
323
|
+
else:
|
|
324
|
+
existing.similar += 1
|
|
325
|
+
else:
|
|
326
|
+
self.warnings[key] = Flow._StepWarningHandler.Record(record.msg, step)
|
|
327
|
+
|
|
328
|
+
name: str = NotImplemented
|
|
329
|
+
Steps: List[Type[Step]] = NotImplemented # Override
|
|
330
|
+
config_vars: List[Variable] = []
|
|
331
|
+
step_objects: Optional[List[Step]] = None
|
|
332
|
+
run_dir: Optional[str] = None
|
|
333
|
+
toolbox: Optional[Toolbox] = None
|
|
334
|
+
config_resolved_path: Optional[str] = None
|
|
335
|
+
|
|
336
|
+
def __init__(
|
|
337
|
+
self,
|
|
338
|
+
config: AnyConfigs,
|
|
339
|
+
*,
|
|
340
|
+
name: Optional[str] = None,
|
|
341
|
+
pdk: Optional[str] = None,
|
|
342
|
+
pdk_root: Optional[str] = None,
|
|
343
|
+
scl: Optional[str] = None,
|
|
344
|
+
design_dir: Optional[str] = None,
|
|
345
|
+
config_override_strings: Optional[Sequence[str]] = None,
|
|
346
|
+
):
|
|
347
|
+
if self.__class__.Steps == NotImplemented:
|
|
348
|
+
raise NotImplementedError(
|
|
349
|
+
f"Abstract flow {self.__class__.__qualname__} does not implement the .Steps property and cannot be initialized."
|
|
350
|
+
)
|
|
351
|
+
for step in self.Steps:
|
|
352
|
+
step.assert_concrete("used in a Flow")
|
|
353
|
+
|
|
354
|
+
self.name = (
|
|
355
|
+
self.__class__.__name__ if self.name == NotImplemented else self.name
|
|
356
|
+
)
|
|
357
|
+
if name is not None:
|
|
358
|
+
self.name = name
|
|
359
|
+
|
|
360
|
+
self.Steps = self.Steps.copy() # Break global reference
|
|
361
|
+
|
|
362
|
+
if not isinstance(config, Config):
|
|
363
|
+
config, design_dir = Config.load(
|
|
364
|
+
config_in=config,
|
|
365
|
+
flow_config_vars=self.get_all_config_variables(),
|
|
366
|
+
config_override_strings=config_override_strings,
|
|
367
|
+
pdk=pdk,
|
|
368
|
+
pdk_root=pdk_root,
|
|
369
|
+
scl=scl,
|
|
370
|
+
design_dir=design_dir,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
self.config: Config = config
|
|
374
|
+
self.design_dir: str = str(self.config["DESIGN_DIR"])
|
|
375
|
+
self.progress_bar = FlowProgressBar(self.name)
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def get_help_md(Self, myst_anchors: bool = False) -> str: # pragma: no cover
|
|
379
|
+
"""
|
|
380
|
+
:returns: rendered Markdown help for this Flow
|
|
381
|
+
"""
|
|
382
|
+
doc_string = ""
|
|
383
|
+
if Self.__doc__:
|
|
384
|
+
doc_string = textwrap.dedent(Self.__doc__)
|
|
385
|
+
|
|
386
|
+
flow_anchor = f"(flow-{slugify(Self.__name__, lower=True)})="
|
|
387
|
+
|
|
388
|
+
result = (
|
|
389
|
+
textwrap.dedent(
|
|
390
|
+
f"""\
|
|
391
|
+
{flow_anchor * myst_anchors}
|
|
392
|
+
### {Self.__name__}
|
|
393
|
+
|
|
394
|
+
```{{eval-rst}}
|
|
395
|
+
%s
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### Using from the CLI
|
|
399
|
+
|
|
400
|
+
```sh
|
|
401
|
+
librelane --flow {Self.__name__} [...]
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
#### Importing
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from librelane.flows import Flow
|
|
408
|
+
|
|
409
|
+
{Self.__name__} = Flow.factory.get("{Self.__name__}")
|
|
410
|
+
```
|
|
411
|
+
"""
|
|
412
|
+
)
|
|
413
|
+
% doc_string
|
|
414
|
+
)
|
|
415
|
+
flow_config_vars = Self.config_vars
|
|
416
|
+
|
|
417
|
+
if len(flow_config_vars):
|
|
418
|
+
config_var_anchors = f"({slugify(Self.__name__, lower=True)}-config-vars)="
|
|
419
|
+
result += textwrap.dedent(
|
|
420
|
+
f"""
|
|
421
|
+
{config_var_anchors * myst_anchors}
|
|
422
|
+
#### Flow-specific Configuration Variables
|
|
423
|
+
|
|
424
|
+
| Variable Name | Type | Description | Default | Units |
|
|
425
|
+
| - | - | - | - | - |
|
|
426
|
+
"""
|
|
427
|
+
)
|
|
428
|
+
for var in flow_config_vars:
|
|
429
|
+
units = var.units or ""
|
|
430
|
+
pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
|
|
431
|
+
var_anchor = f"{{#{var._get_docs_identifier(Self.__name__)}}}"
|
|
432
|
+
result += f"| `{var.name}`{var_anchor * myst_anchors} {pdk_superscript} | {var.type_repr_md()} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
|
|
433
|
+
result += "\n"
|
|
434
|
+
|
|
435
|
+
if len(Self.Steps):
|
|
436
|
+
result += "#### Included Steps\n"
|
|
437
|
+
for step in Self.Steps:
|
|
438
|
+
imp_id = step.get_implementation_id()
|
|
439
|
+
if myst_anchors:
|
|
440
|
+
result += f"* [`{step.id}`](./step_config_vars.md#step-{slugify(imp_id, lower=True)})\n"
|
|
441
|
+
else:
|
|
442
|
+
variant_str = ""
|
|
443
|
+
if imp_id != step.id:
|
|
444
|
+
variant_str = f" (implementation: `{imp_id}`)"
|
|
445
|
+
result += f"* `{step.id}`{variant_str}\n"
|
|
446
|
+
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
@classmethod
|
|
450
|
+
def display_help(Self): # pragma: no cover
|
|
451
|
+
"""
|
|
452
|
+
Displays Markdown help for a given flow.
|
|
453
|
+
|
|
454
|
+
If in an IPython environment, it's rendered using ``IPython.display``.
|
|
455
|
+
Otherwise, it's rendered using ``rich.markdown``.
|
|
456
|
+
"""
|
|
457
|
+
try:
|
|
458
|
+
get_ipython() # type: ignore
|
|
459
|
+
|
|
460
|
+
import IPython.display
|
|
461
|
+
|
|
462
|
+
IPython.display.display(IPython.display.Markdown(Self.get_help_md()))
|
|
463
|
+
except NameError:
|
|
464
|
+
from ..logging import console
|
|
465
|
+
from rich.markdown import Markdown
|
|
466
|
+
|
|
467
|
+
console.log(Markdown(Self.get_help_md()))
|
|
468
|
+
|
|
469
|
+
def get_all_config_variables(self) -> List[Variable]:
|
|
470
|
+
"""
|
|
471
|
+
:returns: All configuration variables for this Flow, including
|
|
472
|
+
universal configuration variables, flow-specific configuration
|
|
473
|
+
variables and step-specific configuration variables.
|
|
474
|
+
"""
|
|
475
|
+
flow_variables_by_name: Dict[str, Tuple[Variable, str]] = {
|
|
476
|
+
variable.name: (variable, "universal flow variables")
|
|
477
|
+
for variable in universal_flow_config_variables
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for variable in self.config_vars:
|
|
481
|
+
if flow_variables_by_name.get(variable.name) is not None:
|
|
482
|
+
existing_variable, source = flow_variables_by_name[variable.name]
|
|
483
|
+
if variable != existing_variable:
|
|
484
|
+
raise FlowException(
|
|
485
|
+
f"Misconfigured flow: Unrelated variables in {source} and flow-specific variables share a name: {variable.name}"
|
|
486
|
+
)
|
|
487
|
+
flow_variables_by_name[variable.name] = (
|
|
488
|
+
variable,
|
|
489
|
+
"flow-specific variables",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
for step_cls in self.Steps:
|
|
493
|
+
for variable in step_cls.config_vars:
|
|
494
|
+
if flow_variables_by_name.get(variable.name) is not None:
|
|
495
|
+
existing_variable, existing_step = flow_variables_by_name[
|
|
496
|
+
variable.name
|
|
497
|
+
]
|
|
498
|
+
if variable != existing_variable:
|
|
499
|
+
raise FlowException(
|
|
500
|
+
f"Misconfigured flow: Unrelated variables in {existing_step} and {step_cls.__name__} share a name: {variable.name}"
|
|
501
|
+
)
|
|
502
|
+
flow_variables_by_name[variable.name] = (variable, step_cls.__name__)
|
|
503
|
+
|
|
504
|
+
return [variable for variable, _ in flow_variables_by_name.values()]
|
|
505
|
+
|
|
506
|
+
@classmethod
|
|
507
|
+
@deprecated(
|
|
508
|
+
version="2.0.0a29",
|
|
509
|
+
reason="Use the constructor for the class instead",
|
|
510
|
+
action="once",
|
|
511
|
+
)
|
|
512
|
+
def init_with_config(
|
|
513
|
+
Self,
|
|
514
|
+
config_in: Union[Config, str, os.PathLike, Dict],
|
|
515
|
+
**kwargs,
|
|
516
|
+
): # pragma: no cover
|
|
517
|
+
kwargs["config"] = config_in
|
|
518
|
+
return Self(**kwargs)
|
|
519
|
+
|
|
520
|
+
@final
|
|
521
|
+
def start(
|
|
522
|
+
self,
|
|
523
|
+
with_initial_state: Optional[State] = None,
|
|
524
|
+
tag: Optional[str] = None,
|
|
525
|
+
last_run: bool = False,
|
|
526
|
+
_force_run_dir: Optional[str] = None,
|
|
527
|
+
_no_load_previous_steps: bool = False,
|
|
528
|
+
*,
|
|
529
|
+
overwrite: bool = False,
|
|
530
|
+
**kwargs,
|
|
531
|
+
) -> State:
|
|
532
|
+
"""
|
|
533
|
+
The entry point for a flow.
|
|
534
|
+
|
|
535
|
+
:param with_initial_state: An optional initial state object to use.
|
|
536
|
+
If not provided:
|
|
537
|
+
* If resuming a previous run, the latest ``state_out.json`` (by filesystem modification date)
|
|
538
|
+
* If not, an empty state object is created.
|
|
539
|
+
:param tag: A name for this invocation of the flow. If not provided,
|
|
540
|
+
one based on a date string will be created.
|
|
541
|
+
|
|
542
|
+
This tag is used to create the "run directory", which will be placed
|
|
543
|
+
under the directory ``runs/`` in the design directory.
|
|
544
|
+
:param last_run: Use the latest run (by modification time) as the tag.
|
|
545
|
+
|
|
546
|
+
If no runs exist, a :class:`FlowException` will be raised.
|
|
547
|
+
|
|
548
|
+
If ``last_run`` and ``tag`` are both set, a :class:`FlowException` will
|
|
549
|
+
also be raised.
|
|
550
|
+
:param overwrite: If true and a run with the desired tag was found, the
|
|
551
|
+
contents will be deleted instead of appended.
|
|
552
|
+
|
|
553
|
+
:returns: ``(success, state_list)``
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
handlers: List[logging.Handler] = []
|
|
557
|
+
|
|
558
|
+
warning_handler = Flow._StepWarningHandler()
|
|
559
|
+
warning_handler.addFilter(LevelFilter("WARNING"))
|
|
560
|
+
handlers.append(warning_handler)
|
|
561
|
+
register_additional_handler(warning_handler)
|
|
562
|
+
|
|
563
|
+
if last_run and tag is not None:
|
|
564
|
+
raise FlowException("tag and last_run cannot be used simultaneously.")
|
|
565
|
+
|
|
566
|
+
tag = tag or datetime.datetime.now().astimezone().strftime(
|
|
567
|
+
"RUN_%Y-%m-%d_%H-%M-%S"
|
|
568
|
+
)
|
|
569
|
+
if last_run:
|
|
570
|
+
runs = sorted(glob.glob(os.path.join(self.design_dir, "runs", "*")))
|
|
571
|
+
|
|
572
|
+
latest_time: float = 0
|
|
573
|
+
latest_run: Optional[str] = None
|
|
574
|
+
for run in runs:
|
|
575
|
+
time = os.path.getmtime(run)
|
|
576
|
+
if time > latest_time:
|
|
577
|
+
latest_time = time
|
|
578
|
+
latest_run = run
|
|
579
|
+
|
|
580
|
+
if latest_run is not None:
|
|
581
|
+
tag = os.path.basename(latest_run)
|
|
582
|
+
else:
|
|
583
|
+
raise FlowException("last_run used without any existing runs")
|
|
584
|
+
|
|
585
|
+
# Stored until next start()
|
|
586
|
+
self.run_dir = os.path.abspath(
|
|
587
|
+
_force_run_dir or os.path.join(self.design_dir, "runs", tag)
|
|
588
|
+
)
|
|
589
|
+
initial_state = with_initial_state or State()
|
|
590
|
+
|
|
591
|
+
self.step_objects = []
|
|
592
|
+
starting_ordinal = 1
|
|
593
|
+
try:
|
|
594
|
+
entries = os.listdir(self.run_dir)
|
|
595
|
+
if len(entries) == 0:
|
|
596
|
+
raise FileNotFoundError(self.run_dir) # Treat as non-existent directory
|
|
597
|
+
elif overwrite:
|
|
598
|
+
verbose(f"Removing '{self.run_dir}'…")
|
|
599
|
+
shutil.rmtree(self.run_dir)
|
|
600
|
+
raise FileNotFoundError(self.run_dir) # Treat as non-existent directory
|
|
601
|
+
|
|
602
|
+
info(f"Using existing run at '{tag}' with the '{self.name}' flow.")
|
|
603
|
+
|
|
604
|
+
# Extract maximum step ordinal + load finished steps
|
|
605
|
+
entries_sorted = sorted(
|
|
606
|
+
filter(
|
|
607
|
+
lambda x: "-" in x and x.split("-", maxsplit=1)[0].isdigit(),
|
|
608
|
+
entries,
|
|
609
|
+
),
|
|
610
|
+
key=lambda x: int(x.split("-", maxsplit=1)[0]),
|
|
611
|
+
)
|
|
612
|
+
for entry in entries_sorted:
|
|
613
|
+
components = entry.split("-", maxsplit=1)
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
extracted_ordinal = int(components[0])
|
|
617
|
+
except ValueError:
|
|
618
|
+
continue
|
|
619
|
+
|
|
620
|
+
if not _no_load_previous_steps:
|
|
621
|
+
try:
|
|
622
|
+
self.step_objects.append(
|
|
623
|
+
Step.load_finished(
|
|
624
|
+
os.path.join(self.run_dir, entry),
|
|
625
|
+
self.config["PDK_ROOT"],
|
|
626
|
+
self.Steps,
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
except StepNotFound as e:
|
|
630
|
+
raise FlowException(
|
|
631
|
+
f"Error while loading concluded step in {entry}: {e}"
|
|
632
|
+
)
|
|
633
|
+
except FileNotFoundError:
|
|
634
|
+
pass
|
|
635
|
+
|
|
636
|
+
starting_ordinal = max(starting_ordinal, extracted_ordinal + 1)
|
|
637
|
+
|
|
638
|
+
# Extract Maximum State
|
|
639
|
+
if with_initial_state is None:
|
|
640
|
+
if latest_json := get_latest_file(self.run_dir, "state_out.json"):
|
|
641
|
+
verbose(f"Using state at '{latest_json}'.")
|
|
642
|
+
|
|
643
|
+
initial_state = State.loads(
|
|
644
|
+
open(latest_json, encoding="utf8").read()
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
except NotADirectoryError:
|
|
648
|
+
raise FlowException(
|
|
649
|
+
f"Run directory for '{tag}' already exists as a file and not a directory."
|
|
650
|
+
)
|
|
651
|
+
except FileNotFoundError:
|
|
652
|
+
info(f"Starting a new run of the '{self.name}' flow with the tag '{tag}'.")
|
|
653
|
+
mkdirp(self.run_dir)
|
|
654
|
+
|
|
655
|
+
# Stored until next start()
|
|
656
|
+
self.toolbox = Toolbox(os.path.join(self.run_dir, "tmp"))
|
|
657
|
+
|
|
658
|
+
for level in ["WARNING", "ERROR"]:
|
|
659
|
+
path = os.path.join(self.run_dir, f"{level.lower()}.log")
|
|
660
|
+
handler = logging.FileHandler(path, mode="a+")
|
|
661
|
+
handler.setLevel(level)
|
|
662
|
+
handler.addFilter(LevelFilter([level]))
|
|
663
|
+
handlers.append(handler)
|
|
664
|
+
register_additional_handler(handler)
|
|
665
|
+
|
|
666
|
+
path = os.path.join(self.run_dir, "flow.log")
|
|
667
|
+
handler = logging.FileHandler(path, mode="a+")
|
|
668
|
+
handler.setLevel("VERBOSE")
|
|
669
|
+
handlers.append(handler)
|
|
670
|
+
register_additional_handler(handler)
|
|
671
|
+
|
|
672
|
+
try:
|
|
673
|
+
self.config_resolved_path = os.path.join(self.run_dir, "resolved.json")
|
|
674
|
+
with open(self.config_resolved_path, "w") as f:
|
|
675
|
+
f.write(self.config.dumps())
|
|
676
|
+
|
|
677
|
+
self.progress_bar = FlowProgressBar(
|
|
678
|
+
self.name, starting_ordinal=starting_ordinal
|
|
679
|
+
)
|
|
680
|
+
self.progress_bar.start()
|
|
681
|
+
final_state, step_objects = self.run(
|
|
682
|
+
initial_state=initial_state,
|
|
683
|
+
starting_ordinal=starting_ordinal,
|
|
684
|
+
**kwargs,
|
|
685
|
+
)
|
|
686
|
+
self.progress_bar.end()
|
|
687
|
+
|
|
688
|
+
# Stored until next start()
|
|
689
|
+
self.step_objects += step_objects
|
|
690
|
+
|
|
691
|
+
return final_state
|
|
692
|
+
finally:
|
|
693
|
+
self.progress_bar.end()
|
|
694
|
+
for registered_handlers in handlers:
|
|
695
|
+
deregister_additional_handler(registered_handlers)
|
|
696
|
+
if len(warning_handler.warnings):
|
|
697
|
+
warn("The following warnings were generated by the flow:")
|
|
698
|
+
for record in warning_handler.warnings.values():
|
|
699
|
+
warn(f"{record}")
|
|
700
|
+
|
|
701
|
+
@protected
|
|
702
|
+
@abstractmethod
|
|
703
|
+
def run(
|
|
704
|
+
self,
|
|
705
|
+
initial_state: State,
|
|
706
|
+
**kwargs,
|
|
707
|
+
) -> Tuple[State, List[Step]]:
|
|
708
|
+
"""
|
|
709
|
+
The core of the Flow. Subclasses of flow are expected to override this
|
|
710
|
+
method.
|
|
711
|
+
|
|
712
|
+
:param initial_state: An initial state object to use.
|
|
713
|
+
:returns: A tuple of states and instantiated step objects for inspection.
|
|
714
|
+
"""
|
|
715
|
+
pass
|
|
716
|
+
|
|
717
|
+
@protected
|
|
718
|
+
def dir_for_step(self, step: Step) -> str:
|
|
719
|
+
"""
|
|
720
|
+
May only be called while :attr:`run_dir` is not None, i.e., the flow
|
|
721
|
+
has started. Otherwise, a :class:`FlowException` is raised.
|
|
722
|
+
|
|
723
|
+
:returns: A directory within the run directory for a specific step,
|
|
724
|
+
prefixed with the current progress bar stage number.
|
|
725
|
+
"""
|
|
726
|
+
if self.run_dir is None:
|
|
727
|
+
raise FlowException(
|
|
728
|
+
"Attempted to call dir_for_step on a flow that has not been started."
|
|
729
|
+
)
|
|
730
|
+
return os.path.join(
|
|
731
|
+
self.run_dir,
|
|
732
|
+
f"{self.progress_bar.get_ordinal_prefix()}{slugify(step.id)}",
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
@protected
|
|
736
|
+
def start_step(
|
|
737
|
+
self,
|
|
738
|
+
step: Step,
|
|
739
|
+
*args,
|
|
740
|
+
**kwargs,
|
|
741
|
+
) -> State:
|
|
742
|
+
"""
|
|
743
|
+
A helper function that handles passing parameters to :mod:`Step.start`.'
|
|
744
|
+
|
|
745
|
+
It is essentially equivalent to:
|
|
746
|
+
|
|
747
|
+
.. code-block:: python
|
|
748
|
+
|
|
749
|
+
step.start(
|
|
750
|
+
toolbox=self.toolbox,
|
|
751
|
+
step_dir=self.dir_for_step(step),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
See :meth:`Step.start` for more info.
|
|
756
|
+
|
|
757
|
+
:param step: The step object to run
|
|
758
|
+
:param args: Arguments to `step.start`
|
|
759
|
+
:param kwargs: Keyword arguments to `step.start`
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
kwargs["toolbox"] = self.toolbox
|
|
763
|
+
kwargs["step_dir"] = self.dir_for_step(step)
|
|
764
|
+
|
|
765
|
+
return step.start(*args, **kwargs)
|
|
766
|
+
|
|
767
|
+
@protected
|
|
768
|
+
def start_step_async(
|
|
769
|
+
self,
|
|
770
|
+
step: Step,
|
|
771
|
+
*args,
|
|
772
|
+
**kwargs,
|
|
773
|
+
) -> Future[State]:
|
|
774
|
+
"""
|
|
775
|
+
An asynchronous equivalent to :meth:`start_step`.
|
|
776
|
+
|
|
777
|
+
:param step: The step object to run
|
|
778
|
+
:param args: Arguments to `step.start`
|
|
779
|
+
:param kwargs: Keyword arguments to `step.start`
|
|
780
|
+
:returns: A ``Future`` encapsulating a State object, which can be used
|
|
781
|
+
as an input to the next step (where the next step will wait for the
|
|
782
|
+
``Future`` to be realized before calling :meth:`Step.run`)
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
kwargs["toolbox"] = self.toolbox
|
|
786
|
+
kwargs["step_dir"] = self.dir_for_step(step)
|
|
787
|
+
|
|
788
|
+
return get_tpe().submit(step.start, *args, **kwargs)
|
|
789
|
+
|
|
790
|
+
def _save_snapshot_ef(self, path: Union[str, os.PathLike]):
|
|
791
|
+
if (
|
|
792
|
+
self.step_objects is None
|
|
793
|
+
or self.toolbox is None
|
|
794
|
+
or self.config_resolved_path is None
|
|
795
|
+
):
|
|
796
|
+
raise RuntimeError(
|
|
797
|
+
"Flow was not run before attempting to save views in the Efabless format."
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
if len(self.step_objects) == 0:
|
|
801
|
+
# No steps, no data
|
|
802
|
+
return
|
|
803
|
+
|
|
804
|
+
last_step = self.step_objects[-1]
|
|
805
|
+
last_state = last_step.state_out
|
|
806
|
+
|
|
807
|
+
if last_state is None:
|
|
808
|
+
raise FlowException(
|
|
809
|
+
f"Misconfigured flow: Step {last_step.id} was appended to step objects without having been run first."
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# 1. Copy Files
|
|
813
|
+
last_state.validate()
|
|
814
|
+
info(
|
|
815
|
+
f"Saving views in the Efabless/Caravel User Project format to '{os.path.abspath(path)}'…"
|
|
816
|
+
)
|
|
817
|
+
mkdirp(path)
|
|
818
|
+
|
|
819
|
+
supported_formats = {
|
|
820
|
+
DesignFormat.POWERED_NETLIST: (os.path.join("verilog", "gl"), "v"),
|
|
821
|
+
DesignFormat.DEF: ("def", "def"),
|
|
822
|
+
DesignFormat.LEF: ("lef", "lef"),
|
|
823
|
+
DesignFormat.SPEF: (os.path.join("spef", "multicorner"), "spef"),
|
|
824
|
+
DesignFormat.LIB: (os.path.join("lib", "multicorner"), "lib"),
|
|
825
|
+
DesignFormat.GDS: ("gds", "gds"),
|
|
826
|
+
DesignFormat.MAG: ("mag", "mag"),
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
def visitor(key, value, top_key, _, __):
|
|
830
|
+
df = DesignFormat.by_id(top_key)
|
|
831
|
+
assert df is not None
|
|
832
|
+
if df not in supported_formats:
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
dfo = df.value
|
|
836
|
+
assert isinstance(dfo, DesignFormatObject)
|
|
837
|
+
|
|
838
|
+
subdirectory, extension = supported_formats[df]
|
|
839
|
+
|
|
840
|
+
target_dir = os.path.join(path, subdirectory)
|
|
841
|
+
if not isinstance(value, Path):
|
|
842
|
+
if isinstance(value, dict):
|
|
843
|
+
assert (
|
|
844
|
+
self.toolbox is not None
|
|
845
|
+
), "toolbox check was not executed properly"
|
|
846
|
+
default_corner_view = self.toolbox.filter_views(self.config, value)
|
|
847
|
+
default_corner_target_dir = os.path.dirname(target_dir)
|
|
848
|
+
mkdirp(default_corner_target_dir)
|
|
849
|
+
if len(default_corner_view) == 1:
|
|
850
|
+
target_basename = f"{self.config['DESIGN_NAME']}.{extension}"
|
|
851
|
+
target_path = os.path.join(
|
|
852
|
+
default_corner_target_dir, target_basename
|
|
853
|
+
)
|
|
854
|
+
shutil.copyfile(
|
|
855
|
+
default_corner_view[0], target_path, follow_symlinks=True
|
|
856
|
+
)
|
|
857
|
+
else:
|
|
858
|
+
for file in default_corner_view:
|
|
859
|
+
shutil.copyfile(file, target_dir, follow_symlinks=True)
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
target_basename = os.path.basename(str(value))
|
|
863
|
+
target_basename = target_basename[: -len(dfo.extension)] + extension
|
|
864
|
+
target_path = os.path.join(target_dir, target_basename)
|
|
865
|
+
mkdirp(target_dir)
|
|
866
|
+
shutil.copyfile(value, target_path, follow_symlinks=True)
|
|
867
|
+
|
|
868
|
+
last_state._walk(last_state.to_raw_dict(metrics=False), path, visit=visitor)
|
|
869
|
+
|
|
870
|
+
# 2. Copy Logs, Reports, & Signoff Information
|
|
871
|
+
def copy_dir_contents(from_dir, to_dir, filter="*"):
|
|
872
|
+
for file in os.listdir(from_dir):
|
|
873
|
+
file_path = os.path.join(from_dir, file)
|
|
874
|
+
if os.path.isdir(file_path):
|
|
875
|
+
continue
|
|
876
|
+
if fnmatch.fnmatch(file, filter):
|
|
877
|
+
shutil.copyfile(
|
|
878
|
+
file_path, os.path.join(to_dir, file), follow_symlinks=True
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
def find_one(pattern):
|
|
882
|
+
result = glob.glob(pattern)
|
|
883
|
+
if len(result) == 0:
|
|
884
|
+
return None
|
|
885
|
+
return result[0]
|
|
886
|
+
|
|
887
|
+
signoff_dir = os.path.join(path, "signoff", self.config["DESIGN_NAME"])
|
|
888
|
+
openlane_signoff_dir = os.path.join(signoff_dir, "openlane-signoff")
|
|
889
|
+
mkdirp(openlane_signoff_dir)
|
|
890
|
+
|
|
891
|
+
## resolved.json
|
|
892
|
+
shutil.copyfile(
|
|
893
|
+
self.config_resolved_path,
|
|
894
|
+
os.path.join(openlane_signoff_dir, "resolved.json"),
|
|
895
|
+
follow_symlinks=True,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
## metrics
|
|
899
|
+
with open(os.path.join(signoff_dir, "metrics.csv"), "w", encoding="utf8") as f:
|
|
900
|
+
last_state.metrics_to_csv(f)
|
|
901
|
+
|
|
902
|
+
## flow logs
|
|
903
|
+
mkdirp(openlane_signoff_dir)
|
|
904
|
+
copy_dir_contents(self.run_dir, openlane_signoff_dir, "*.log")
|
|
905
|
+
|
|
906
|
+
### step-specific signoff logs and reports
|
|
907
|
+
for step in self.step_objects:
|
|
908
|
+
reports_dir = os.path.join(step.step_dir, "reports")
|
|
909
|
+
step_imp_id = step.get_implementation_id()
|
|
910
|
+
if step_imp_id == "Magic.DRC":
|
|
911
|
+
if drc_rpt := find_one(os.path.join(reports_dir, "*.rpt")):
|
|
912
|
+
shutil.copyfile(
|
|
913
|
+
drc_rpt, os.path.join(openlane_signoff_dir, "drc.rpt")
|
|
914
|
+
)
|
|
915
|
+
if drc_xml := find_one(os.path.join(reports_dir, "*.xml")):
|
|
916
|
+
# Despite the name, this is the Magic DRC report simply
|
|
917
|
+
# converted into a KLayout-compatible format. Confusing!
|
|
918
|
+
drc_xml_out = os.path.join(openlane_signoff_dir, "drc.klayout.xml")
|
|
919
|
+
with open(drc_xml, encoding="utf8") as i, open(
|
|
920
|
+
drc_xml_out, "w", encoding="utf8"
|
|
921
|
+
) as o:
|
|
922
|
+
o.write(
|
|
923
|
+
"<!-- Despite the name, this is the Magic DRC report in KLayout format. -->\n"
|
|
924
|
+
)
|
|
925
|
+
shutil.copyfileobj(i, o)
|
|
926
|
+
if step_imp_id == "Netgen.LVS":
|
|
927
|
+
if lvs_rpt := find_one(os.path.join(reports_dir, "*.rpt")):
|
|
928
|
+
shutil.copyfile(
|
|
929
|
+
lvs_rpt, os.path.join(openlane_signoff_dir, "lvs.rpt")
|
|
930
|
+
)
|
|
931
|
+
if step_imp_id.endswith("DRC") or step_imp_id.endswith("LVS"):
|
|
932
|
+
copy_dir_contents(step.step_dir, openlane_signoff_dir, "*.log")
|
|
933
|
+
if step_imp_id.endswith("CheckAntennas"):
|
|
934
|
+
if os.path.exists(reports_dir):
|
|
935
|
+
copy_dir_contents(
|
|
936
|
+
reports_dir, openlane_signoff_dir, "antenna_summary.rpt"
|
|
937
|
+
)
|
|
938
|
+
if step_imp_id.endswith("STAPostPNR"):
|
|
939
|
+
timing_report_folder = os.path.join(
|
|
940
|
+
openlane_signoff_dir, "timing-reports"
|
|
941
|
+
)
|
|
942
|
+
mkdirp(timing_report_folder)
|
|
943
|
+
copy_dir_contents(step.step_dir, timing_report_folder, "*summary.rpt")
|
|
944
|
+
for dir in os.listdir(step.step_dir):
|
|
945
|
+
dir_path = os.path.join(step.step_dir, dir)
|
|
946
|
+
if not os.path.isdir(dir_path):
|
|
947
|
+
continue
|
|
948
|
+
target = os.path.join(timing_report_folder, dir)
|
|
949
|
+
mkdirp(target)
|
|
950
|
+
copy_dir_contents(dir_path, target, "*.rpt")
|
|
951
|
+
|
|
952
|
+
# 3. SDF
|
|
953
|
+
# (This one, as with many things in the Efabless format, is special)
|
|
954
|
+
if sdf := last_state[DesignFormat.SDF]:
|
|
955
|
+
assert isinstance(sdf, dict), "SDF is not a dictionary"
|
|
956
|
+
for corner, view in sdf.items():
|
|
957
|
+
assert isinstance(view, Path), "SDF state out returned multiple paths"
|
|
958
|
+
target_dir = os.path.join(signoff_dir, "sdf", corner)
|
|
959
|
+
mkdirp(target_dir)
|
|
960
|
+
shutil.copyfile(
|
|
961
|
+
view, os.path.join(target_dir, f"{self.config['DESIGN_NAME']}.sdf")
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
@deprecated(
|
|
965
|
+
version="2.0.0a46",
|
|
966
|
+
reason="Use .progress_bar.set_max_stage_count",
|
|
967
|
+
action="once",
|
|
968
|
+
)
|
|
969
|
+
@protected
|
|
970
|
+
def set_max_stage_count(self, count: int): # pragma: no cover
|
|
971
|
+
"""
|
|
972
|
+
Alias for ``self.progress_bar``'s :py:meth:`FlowProgressBar.set_max_stage_count`.
|
|
973
|
+
"""
|
|
974
|
+
self.progress_bar.set_max_stage_count(count)
|
|
975
|
+
|
|
976
|
+
@deprecated(
|
|
977
|
+
version="2.0.0a46",
|
|
978
|
+
reason="Use .progress_bar.start_stage",
|
|
979
|
+
action="once",
|
|
980
|
+
)
|
|
981
|
+
@protected
|
|
982
|
+
def start_stage(self, name: str): # pragma: no cover
|
|
983
|
+
"""
|
|
984
|
+
Alias for ``self.progress_bar``'s :py:meth:`FlowProgressBar.start_stage`.
|
|
985
|
+
"""
|
|
986
|
+
self.progress_bar.start_stage(name)
|
|
987
|
+
|
|
988
|
+
@deprecated(
|
|
989
|
+
version="2.0.0a46",
|
|
990
|
+
reason="Use .progress_bar.end_stage",
|
|
991
|
+
action="once",
|
|
992
|
+
)
|
|
993
|
+
@protected
|
|
994
|
+
def end_stage(self, increment_ordinal: bool = True): # pragma: no cover
|
|
995
|
+
"""
|
|
996
|
+
Alias for ``self.progress_bar``'s :py:meth:`FlowProgressBar.end_stage`.
|
|
997
|
+
"""
|
|
998
|
+
self.progress_bar.end_stage(increment_ordinal=increment_ordinal)
|
|
999
|
+
|
|
1000
|
+
class FlowFactory(object):
|
|
1001
|
+
"""
|
|
1002
|
+
A factory singleton for Flows, allowing Flow types to be registered and then
|
|
1003
|
+
retrieved by name.
|
|
1004
|
+
|
|
1005
|
+
See https://en.wikipedia.org/wiki/Factory_(object-oriented_programming) for
|
|
1006
|
+
a primer.
|
|
1007
|
+
"""
|
|
1008
|
+
|
|
1009
|
+
__registry: ClassVar[Dict[str, Type[Flow]]] = {}
|
|
1010
|
+
|
|
1011
|
+
@classmethod
|
|
1012
|
+
def register(
|
|
1013
|
+
Self, registered_name: Optional[str] = None
|
|
1014
|
+
) -> Callable[[Type[Flow]], Type[Flow]]:
|
|
1015
|
+
"""
|
|
1016
|
+
A decorator that adds a flow type to the registry.
|
|
1017
|
+
|
|
1018
|
+
:param registered_name: An optional registered name for the flow.
|
|
1019
|
+
|
|
1020
|
+
If not specified, the flow will be referred to by its Python
|
|
1021
|
+
class name.
|
|
1022
|
+
"""
|
|
1023
|
+
|
|
1024
|
+
def decorator(cls: Type[Flow]) -> Type[Flow]:
|
|
1025
|
+
name = cls.__name__
|
|
1026
|
+
if registered_name is not None:
|
|
1027
|
+
name = registered_name
|
|
1028
|
+
Self.__registry[name] = cls
|
|
1029
|
+
return cls
|
|
1030
|
+
|
|
1031
|
+
return decorator
|
|
1032
|
+
|
|
1033
|
+
@classmethod
|
|
1034
|
+
def get(Self, name: str) -> Optional[Type[Flow]]:
|
|
1035
|
+
"""
|
|
1036
|
+
Retrieves a Flow type from the registry using a lookup string.
|
|
1037
|
+
|
|
1038
|
+
:param name: The registered name of the Flow. Case-sensitive.
|
|
1039
|
+
"""
|
|
1040
|
+
return Self.__registry.get(name)
|
|
1041
|
+
|
|
1042
|
+
@classmethod
|
|
1043
|
+
def list(Self) -> List[str]:
|
|
1044
|
+
"""
|
|
1045
|
+
:returns: A list of strings representing all registered flows.
|
|
1046
|
+
"""
|
|
1047
|
+
return list(Self.__registry.keys())
|
|
1048
|
+
|
|
1049
|
+
factory = FlowFactory
|