librelane 2.4.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (170) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +479 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +63 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +246 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +456 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +116 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +155 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +743 -0
  27. librelane/container.py +285 -0
  28. librelane/env_info.py +320 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +327 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +1049 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/help/__main__.py +39 -0
  52. librelane/logging/__init__.py +40 -0
  53. librelane/logging/logger.py +323 -0
  54. librelane/open_pdks_rev +1 -0
  55. librelane/plugins.py +21 -0
  56. librelane/py.typed +0 -0
  57. librelane/scripts/base.sdc +80 -0
  58. librelane/scripts/klayout/Readme.md +2 -0
  59. librelane/scripts/klayout/open_design.py +63 -0
  60. librelane/scripts/klayout/render.py +121 -0
  61. librelane/scripts/klayout/stream_out.py +176 -0
  62. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  63. librelane/scripts/klayout/xor.drc +120 -0
  64. librelane/scripts/magic/Readme.md +1 -0
  65. librelane/scripts/magic/common/read.tcl +114 -0
  66. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  67. librelane/scripts/magic/def/mag.tcl +19 -0
  68. librelane/scripts/magic/def/mag_gds.tcl +79 -0
  69. librelane/scripts/magic/drc.tcl +78 -0
  70. librelane/scripts/magic/extract_spice.tcl +98 -0
  71. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  72. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  73. librelane/scripts/magic/gds/extras_mag.tcl +45 -0
  74. librelane/scripts/magic/gds/mag_with_pointers.tcl +31 -0
  75. librelane/scripts/magic/get_bbox.tcl +11 -0
  76. librelane/scripts/magic/lef/extras_maglef.tcl +61 -0
  77. librelane/scripts/magic/lef/maglef.tcl +26 -0
  78. librelane/scripts/magic/lef.tcl +57 -0
  79. librelane/scripts/magic/open.tcl +28 -0
  80. librelane/scripts/magic/wrapper.tcl +21 -0
  81. librelane/scripts/netgen/setup.tcl +28 -0
  82. librelane/scripts/odbpy/apply_def_template.py +49 -0
  83. librelane/scripts/odbpy/cell_frequency.py +107 -0
  84. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  85. librelane/scripts/odbpy/contextualize.py +109 -0
  86. librelane/scripts/odbpy/defutil.py +573 -0
  87. librelane/scripts/odbpy/diodes.py +373 -0
  88. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  89. librelane/scripts/odbpy/eco_buffer.py +181 -0
  90. librelane/scripts/odbpy/eco_diode.py +139 -0
  91. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  92. librelane/scripts/odbpy/io_place.py +482 -0
  93. librelane/scripts/odbpy/ioplace_parser/__init__.py +23 -0
  94. librelane/scripts/odbpy/ioplace_parser/parse.py +147 -0
  95. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  96. librelane/scripts/odbpy/lefutil.py +97 -0
  97. librelane/scripts/odbpy/placers.py +162 -0
  98. librelane/scripts/odbpy/power_utils.py +397 -0
  99. librelane/scripts/odbpy/random_place.py +57 -0
  100. librelane/scripts/odbpy/reader.py +250 -0
  101. librelane/scripts/odbpy/remove_buffers.py +173 -0
  102. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  103. librelane/scripts/odbpy/wire_lengths.py +93 -0
  104. librelane/scripts/openroad/antenna_check.tcl +20 -0
  105. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  106. librelane/scripts/openroad/basic_mp.tcl +24 -0
  107. librelane/scripts/openroad/buffer_list.tcl +10 -0
  108. librelane/scripts/openroad/common/dpl.tcl +24 -0
  109. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  110. librelane/scripts/openroad/common/grt.tcl +32 -0
  111. librelane/scripts/openroad/common/io.tcl +540 -0
  112. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  113. librelane/scripts/openroad/common/resizer.tcl +103 -0
  114. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  115. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  116. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  117. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  118. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  119. librelane/scripts/openroad/cts.tcl +80 -0
  120. librelane/scripts/openroad/cut_rows.tcl +24 -0
  121. librelane/scripts/openroad/dpl.tcl +24 -0
  122. librelane/scripts/openroad/drt.tcl +37 -0
  123. librelane/scripts/openroad/fill.tcl +30 -0
  124. librelane/scripts/openroad/floorplan.tcl +145 -0
  125. librelane/scripts/openroad/gpl.tcl +88 -0
  126. librelane/scripts/openroad/grt.tcl +30 -0
  127. librelane/scripts/openroad/gui.tcl +37 -0
  128. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  129. librelane/scripts/openroad/ioplacer.tcl +67 -0
  130. librelane/scripts/openroad/irdrop.tcl +51 -0
  131. librelane/scripts/openroad/pdn.tcl +52 -0
  132. librelane/scripts/openroad/rcx.tcl +32 -0
  133. librelane/scripts/openroad/repair_design.tcl +70 -0
  134. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  135. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  136. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  137. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  138. librelane/scripts/openroad/sta/corner.tcl +393 -0
  139. librelane/scripts/openroad/tapcell.tcl +25 -0
  140. librelane/scripts/openroad/write_views.tcl +27 -0
  141. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  142. librelane/scripts/pyosys/json_header.py +84 -0
  143. librelane/scripts/pyosys/synthesize.py +493 -0
  144. librelane/scripts/pyosys/ys_common.py +153 -0
  145. librelane/scripts/tclsh/hello.tcl +1 -0
  146. librelane/state/__init__.py +24 -0
  147. librelane/state/__main__.py +61 -0
  148. librelane/state/design_format.py +195 -0
  149. librelane/state/state.py +359 -0
  150. librelane/steps/__init__.py +61 -0
  151. librelane/steps/__main__.py +510 -0
  152. librelane/steps/checker.py +637 -0
  153. librelane/steps/common_variables.py +340 -0
  154. librelane/steps/cvc_rv.py +169 -0
  155. librelane/steps/klayout.py +509 -0
  156. librelane/steps/magic.py +576 -0
  157. librelane/steps/misc.py +160 -0
  158. librelane/steps/netgen.py +253 -0
  159. librelane/steps/odb.py +1088 -0
  160. librelane/steps/openroad.py +2460 -0
  161. librelane/steps/openroad_alerts.py +102 -0
  162. librelane/steps/pyosys.py +640 -0
  163. librelane/steps/step.py +1571 -0
  164. librelane/steps/tclstep.py +288 -0
  165. librelane/steps/verilator.py +222 -0
  166. librelane/steps/yosys.py +371 -0
  167. librelane-2.4.0.dist-info/METADATA +169 -0
  168. librelane-2.4.0.dist-info/RECORD +170 -0
  169. librelane-2.4.0.dist-info/WHEEL +4 -0
  170. librelane-2.4.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,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