From 7959772a9a938f2e75ef2f4227dcd98dc770acf7 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Sun, 15 Dec 2024 19:22:25 +0200 Subject: [PATCH 1/3] feat!: dynamic designformat registration (#612) ## Misc Enhancements/Bugfixes * `openlane.state` * `DesignFormat` * Now a dataclass encapsulating the information about the DesignFormat directly. * `.factory` is a factory for retrieval of DesignFormats by ID * `DesignFormats` may be registered to the factory using `.register()` * Registrations for previously included `DesignFormat`s now moved to where appropriate. * Renamed `POWERED_NETLIST_NO_PHYSICAL_CELLS` to `LOGICAL_POWERED_NETLIST` * Renamed `POWERED_NETLIST_SDF_FRIENDLY` to `SDF_FRIENDLY_POWERED_NETLIST` * `State` * States initialized with keys that have values that are `None` now remove said keys. ## API Breaks * `openlane.steps` * `TclStep` now uses the IDs uppercased for `CURRENT_` and `SAVE_`. * `openlane.state` * `State` no longer includes all `DesignFormat`s as guaranteed keys and `.get` must be used to avoide `KeyErrors` * `DesignFormat` is no longer an enumeration and is not iterable. However, to avoid massive codebase changes, you can still access `DesignFormat`s registered to the factory using the dot notation (e.g. `DesignFormat.NETLIST`), using either their `id` or any of their `alts`. * Removed `DesignFormatObject`: the DesignFormat class itself is now a dataclass incorporating these fields, except `name`, which has been renamed to `full_name`. The enumeration's name has been added to `alts`, while `.name` is now an alias for `.id`. --- Changelog.md | 39 +++ openlane/config/variable.py | 2 +- openlane/flows/flow.py | 9 +- openlane/scripts/openroad/common/io.tcl | 32 +-- openlane/state/__init__.py | 2 +- openlane/state/design_format.py | 324 ++++++++++++++---------- openlane/state/state.py | 38 ++- openlane/steps/klayout.py | 14 +- openlane/steps/magic.py | 20 +- openlane/steps/odb.py | 6 +- openlane/steps/openroad.py | 32 ++- openlane/steps/pyosys.py | 15 +- openlane/steps/step.py | 20 +- openlane/steps/tclstep.py | 12 +- test/flows/test_flow.py | 4 +- test/state/test_state.py | 6 +- test/steps/test_tclstep.py | 4 +- 17 files changed, 359 insertions(+), 220 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5e4944483..6fe12eb3a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,45 @@ ## Documentation --> +# Dev + +## Misc Enhancements/Bugfixes + +* `openlane.state` + * `DesignFormat` + * Now a dataclass encapsulating the information about the DesignFormat + directly. + * `.factory` is a factory for retrieval of DesignFormats by ID + * `DesignFormats` may be registered to the factory using `.register()` + * Registrations for previously included `DesignFormat`s now moved to + appropriate files. + * Renamed `POWERED_NETLIST_NO_PHYSICAL_CELLS` to + `LOGICAL_POWERED_NETLIST` + * Renamed `POWERED_NETLIST_SDF_FRIENDLY` to + `SDF_FRIENDLY_POWERED_NETLIST` + * `State` + * States initialized with keys that have values that are `None` now remove + said keys. + +## API Breaks + +* `openlane.steps` + + * `TclStep` now uses the IDs uppercased for `CURRENT_` and `SAVE_`. + +* `openlane.state` + + * `State` no longer includes all `DesignFormat`s as guaranteed keys and `.get` + must be used to avoide `KeyErrors` + * `DesignFormat` is no longer an enumeration and is not iterable. However, to + avoid massive codebase changes, you can still access `DesignFormat`s + registered to the factory using the dot notation (e.g. + `DesignFormat.NETLIST`), using either their `id` or any of their `alts`. + * Removed `DesignFormatObject`: the DesignFormat class itself is now a + dataclass incorporating these fields, except `name`, which has been renamed + to `full_name`. The enumeration's name has been added to `alts`, while + `.name` is now an alias for `.id`. + # 2.2.9 ## Steps diff --git a/openlane/config/variable.py b/openlane/config/variable.py index 405d4dbf2..7fd5d1528 100644 --- a/openlane/config/variable.py +++ b/openlane/config/variable.py @@ -160,7 +160,7 @@ def view_by_df( self, df: DesignFormat ) -> Union[None, Path, List[Path], Dict[str, List[Path]]]: try: - return getattr(self, df.value.id) + return getattr(self, df.id) except AttributeError: return None diff --git a/openlane/flows/flow.py b/openlane/flows/flow.py index 3c993523a..7ea089789 100644 --- a/openlane/flows/flow.py +++ b/openlane/flows/flow.py @@ -48,7 +48,7 @@ from openlane.common.types import Path from ..config import Config, Variable, universal_flow_config_variables, AnyConfigs -from ..state import State, DesignFormat, DesignFormatObject +from ..state import State, DesignFormat from ..steps import Step, StepNotFound from ..logging import ( LevelFilter, @@ -804,14 +804,11 @@ def _save_snapshot_ef(self, path: Union[str, os.PathLike]): } def visitor(key, value, top_key, _, __): - df = DesignFormat.by_id(top_key) + df = DesignFormat.factory.get(top_key) assert df is not None if df not in supported_formats: return - dfo = df.value - assert isinstance(dfo, DesignFormatObject) - subdirectory, extension = supported_formats[df] target_dir = os.path.join(path, subdirectory) @@ -837,7 +834,7 @@ def visitor(key, value, top_key, _, __): return target_basename = os.path.basename(str(value)) - target_basename = target_basename[: -len(dfo.extension)] + extension + target_basename = target_basename[: -len(df.extension)] + extension target_path = os.path.join(target_dir, target_basename) mkdirp(target_dir) shutil.copyfile(value, target_path, follow_symlinks=True) diff --git a/openlane/scripts/openroad/common/io.tcl b/openlane/scripts/openroad/common/io.tcl index a77a119a5..486211086 100644 --- a/openlane/scripts/openroad/common/io.tcl +++ b/openlane/scripts/openroad/common/io.tcl @@ -125,14 +125,14 @@ proc read_current_netlist {args} { flags {-powered} if { [info exists flags(-powered)] } { - puts "Reading top-level powered netlist at '$::env(CURRENT_POWERED_NETLIST)'…" - if {[catch {read_verilog $::env(CURRENT_POWERED_NETLIST)} errmsg]} { + puts "Reading top-level powered netlist at '$::env(CURRENT_PNL)'…" + if {[catch {read_verilog $::env(CURRENT_PNL)} errmsg]} { puts stderr $errmsg exit 1 } } else { - puts "Reading top-level netlist at '$::env(CURRENT_NETLIST)'…" - if {[catch {read_verilog $::env(CURRENT_NETLIST)} errmsg]} { + puts "Reading top-level netlist at '$::env(CURRENT_NL)'…" + if {[catch {read_verilog $::env(CURRENT_NL)} errmsg]} { puts stderr $errmsg exit 1 } @@ -338,32 +338,32 @@ proc write_views {args} { write_db $::env(SAVE_ODB) } - if { [info exists ::env(SAVE_NETLIST)] } { - puts "Writing netlist to '$::env(SAVE_NETLIST)'…" - write_verilog $::env(SAVE_NETLIST) + if { [info exists ::env(SAVE_NL)] } { + puts "Writing netlist to '$::env(SAVE_NL)'…" + write_verilog $::env(SAVE_NL) } - if { [info exists ::env(SAVE_POWERED_NETLIST)] } { - puts "Writing powered netlist to '$::env(SAVE_POWERED_NETLIST)'…" - write_verilog -include_pwr_gnd $::env(SAVE_POWERED_NETLIST) + if { [info exists ::env(SAVE_PNL)] } { + puts "Writing powered netlist to '$::env(SAVE_PNL)'…" + write_verilog -include_pwr_gnd $::env(SAVE_PNL) } - if { [info exists ::env(SAVE_POWERED_NETLIST_SDF_FRIENDLY)] } { + if { [info exists ::env(SAVE_SDF_PNL)] } { set exclude_cells "[join $::env(FILL_CELL)] [join $::env(DECAP_CELL)] [join $::env(WELLTAP_CELL)] [join $::env(ENDCAP_CELL)]" - puts "Writing nofill powered netlist to '$::env(SAVE_POWERED_NETLIST_SDF_FRIENDLY)'…" + puts "Writing nofill powered netlist to '$::env(SAVE_SDF_PNL)'…" puts "Excluding $exclude_cells" write_verilog -include_pwr_gnd \ -remove_cells "$exclude_cells"\ - $::env(SAVE_POWERED_NETLIST_SDF_FRIENDLY) + $::env(SAVE_SDF_PNL) } - if { [info exists ::env(SAVE_POWERED_NETLIST_NO_PHYSICAL_CELLS)] } { + if { [info exists ::env(SAVE_LOGICAL_PNL)] } { set exclude_cells "[join [lindex [split $::env(DIODE_CELL) "/"] 0]] [join $::env(FILL_CELL)] [join $::env(DECAP_CELL)] [join $::env(WELLTAP_CELL)] [join $::env(ENDCAP_CELL)]" - puts "Writing nofilldiode powered netlist to '$::env(SAVE_POWERED_NETLIST_NO_PHYSICAL_CELLS)'…" + puts "Writing nofilldiode powered netlist to '$::env(SAVE_LOGICAL_PNL)'…" puts "Excluding $exclude_cells" write_verilog -include_pwr_gnd \ -remove_cells "$exclude_cells"\ - $::env(SAVE_POWERED_NETLIST_NO_PHYSICAL_CELLS) + $::env(SAVE_LOGICAL_PNL) } if { [info exists ::env(SAVE_OPENROAD_LEF)] } { diff --git a/openlane/state/__init__.py b/openlane/state/__init__.py index 7748f546a..65d9de6fb 100644 --- a/openlane/state/__init__.py +++ b/openlane/state/__init__.py @@ -20,5 +20,5 @@ OpenLane step. The State is essentially a list of views in various formats in addition to the cumulative set of metrics created by previous Steps. """ -from .design_format import DesignFormat, DesignFormatObject +from .design_format import DesignFormat from .state import State, InvalidState, StateElement diff --git a/openlane/state/design_format.py b/openlane/state/design_format.py index 5c344e5ee..67bee1201 100644 --- a/openlane/state/design_format.py +++ b/openlane/state/design_format.py @@ -11,26 +11,38 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from enum import Enum -from dataclasses import dataclass -from typing import Dict, Optional +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Dict, List, Optional, ClassVar +from deprecated.sphinx import deprecated + + +class DFMetaclass(type): + def __getattr__(Self, key: str): + if df := Self.factory.get(key): + return df + raise AttributeError( + "Unknown DesignFormat or Type[DesignFormat] attribute", + key, + Self, + ) @dataclass -class DesignFormatObject: +class DesignFormat(metaclass=DFMetaclass): """ Metadata about the various possible text or binary representations (views) of any design. - For example, ``DesignFormat.NETLIST.value`` has the metadata for Netlist - views. + For example, ``DesignFormat.nl`` has the metadata for Netlist views. - :param id: A lowercase alphanumeric identifier for the design format. - Some IDs in OpenLane 2.X use dashes. This is an inconsistency that will - be addressed in the next major version of OpenLane as it would be a - breaking change. + :param id: A lowercase alphanumeric/underscore identifier for the design + format. :param extension: The file extension for designs saved in this format. - :param name: A human-readable name for this design format. + :param full_name: A human-readable name for this design format. + :param alts: A list of alternate ids used to access the DesignFormat by + the subscript operator. Includes its OpenLane <3.0.0 enumeration name + for limited backwards compatibility. :param folder_override: The subdirectory when :meth:`openlane.state.State.save_snapshot` is called on a state. If unset, the value for ``id`` will be used. @@ -40,7 +52,8 @@ class DesignFormatObject: id: str extension: str - name: str + full_name: str + alts: List[str] = field(default_factory=list) folder_override: Optional[str] = None multiple: bool = False @@ -48,133 +61,178 @@ class DesignFormatObject: def folder(self) -> str: return self.folder_override or self.id - -class DesignFormat(Enum): - """ - An `enumeration `_ of a number - of :class:`openlane.state.DesignFormatObject`\\s representing the various - possible text or binary representations (views) supported by OpenLane - states. - - Members of this enumeration are used as the keys of - :class:`openlane.state.State` objects. - """ - - NETLIST: DesignFormatObject = DesignFormatObject( - "nl", - "nl.v", - "Verilog Netlist", - ) - POWERED_NETLIST: DesignFormatObject = DesignFormatObject( - "pnl", - "pnl.v", - "Powered Verilog Netlist", - ) - POWERED_NETLIST_SDF_FRIENDLY: DesignFormatObject = DesignFormatObject( - "pnl-sdf-friendly", - "pnl-sdf.v", - "Powered Verilog Netlist For SDF Simulation (Without Fill Cells)", - folder_override="pnl", - ) - POWERED_NETLIST_NO_PHYSICAL_CELLS: DesignFormatObject = DesignFormatObject( - "pnl-npc", - "pnl-npc.v", - "Powered Verilog Netlist Without Physical Cells (Fill Cells and Diode Cells)", - folder_override="pnl", - ) - - DEF: DesignFormatObject = DesignFormatObject( - "def", - "def", - "Design Exchange Format", - ) - LEF: DesignFormatObject = DesignFormatObject( - "lef", - "lef", - "Library Exchange Format", - ) - OPENROAD_LEF: DesignFormatObject = DesignFormatObject( - "openroad-lef", - "openroad.lef", - "Library Exchange Format Generated by OpenROAD", - folder_override="lef", - ) - ODB: DesignFormatObject = DesignFormatObject( - "odb", - "odb", - "OpenDB Database", - ) - - SDC: DesignFormatObject = DesignFormatObject( - "sdc", - "sdc", - "Design Constraints", - ) - SDF: DesignFormatObject = DesignFormatObject( - "sdf", - "sdf", - "Standard Delay Format", - multiple=True, - ) - SPEF: DesignFormatObject = DesignFormatObject( - "spef", - "spef", - "Standard Parasitics Extraction Format", - multiple=True, # nom, min, max, ... - ) - LIB: DesignFormatObject = DesignFormatObject( - "lib", - "lib", - "LIB Timing Library Format", - multiple=True, - ) - SPICE: DesignFormatObject = DesignFormatObject( - "spice", - "spice", - "Simulation Program with Integrated Circuit Emphasis", - ) - - MAG: DesignFormatObject = DesignFormatObject( - "mag", - "mag", - "Magic VLSI View", + @property + @deprecated( + "The DesignFormat is directly returned now, no need for .value", + version="3.0.0", + action="once", ) + def value(self) -> DesignFormat: + return self - GDS: DesignFormatObject = DesignFormatObject( - "gds", - "gds", - "GDSII Stream", - ) - MAG_GDS: DesignFormatObject = DesignFormatObject( - "mag_gds", - "magic.gds", - "GDSII Stream (Magic)", - ) - KLAYOUT_GDS: DesignFormatObject = DesignFormatObject( - "klayout_gds", - "klayout.gds", - "GDSII Stream (KLayout)", + @property + @deprecated( + ".name has been removed because it's redundant, use .id", + version="3.0.0", + action="once", ) + def name(self) -> str: + return self.id - JSON_HEADER: DesignFormatObject = DesignFormatObject( - "json_h", - "h.json", - "Design JSON Header File", - ) - VERILOG_HEADER: DesignFormatObject = DesignFormatObject( - "vh", - "vh", - "Verilog Header", - ) + def register(self): + self.__class__.factory.register(self) def __str__(self) -> str: - return self.value.id + return self.id + + def __hash__(self): + return hash(self.id) @staticmethod + @deprecated( + "Use DesignFormat.factory.get", + version="3.0.0", + action="once", + ) def by_id(id: str) -> Optional["DesignFormat"]: - return _designformat_by_id.get(id) - - -_designformat_by_id: Dict[str, "DesignFormat"] = { - format.value.id: format for format in DesignFormat -} + return DesignFormat.factory.get(id) + + class DesignFormatFactory(object): + """ + A factory singleton for DesignFormats, allowing them to be registered + and then retrieved by a string name. + + See https://en.wikipedia.org/wiki/Factory_(object-oriented_programming) for + a primer. + """ + + _registry: ClassVar[Dict[str, DesignFormat]] = {} + + @classmethod + def register(Self, df: DesignFormat) -> DesignFormat: + """ + Adds a DesignFormat to the registry using its + :attr:`DesignFormat.id`, :attr:`DesignFormat.name` and + :attr:`DesignFormat.alts` attributes. + """ + Self._registry[df.id] = df + for alt in df.alts: + Self._registry[alt] = df + return df + + @classmethod + def get(Self, name: str) -> Optional[DesignFormat]: + """ + Retrieves a DesignFormat type from the registry using a lookup + string. + + :param name: The registered name of the Step. Case-insensitive. + """ + return Self._registry.get(name) + + @classmethod + def list(Self) -> List[str]: + """ + :returns: A list of IDs of all registered DesignFormat. + """ + return [cls.id for cls in Self._registry.values()] + + factory: ClassVar = DesignFormatFactory + + +# Common Design Formats +DesignFormat( + "nl", + "nl.v", + "Verilog Netlist", + alts=["NETLIST"], +).register() + +DesignFormat( + "pnl", + "pnl.v", + "Powered Verilog Netlist", + alts=["POWERED_NETLIST"], +).register() + +DesignFormat( + "sdf_pnl", + "sdf_pnl.v", + "Powered Verilog Netlist for SDF Simulation (No Fills)", + alts=["SDF_FRIENDLY_POWERED_NETLIST"], + folder_override="pnl", +).register() + +DesignFormat( + "logical_pnl", + "logical_pnl.v", + "Logical cell-only Powered Verilog Netlist", + alts=["LOGICAL_POWERED_NETLIST"], + folder_override="pnl", +).register() + +DesignFormat( + "def", + "def", + "Design Exchange Format", + alts=["def_", "DEF"], +).register() + +DesignFormat( + "lef", + "lef", + "Library Exchange Format", + alts=["LEF"], +).register() + +DesignFormat( + "sdc", + "sdc", + "Design Constraints", + alts=["SDC"], +).register() + +DesignFormat( + "sdf", + "sdf", + "Standard Delay Format", + alts=["SDF"], + multiple=True, +).register() + +DesignFormat( + "spef", + "spef", + "Standard Parasitics Extraction Format", + alts=["SPEF"], + multiple=True, # nom, min, max, ... +).register() + +DesignFormat( + "lib", + "lib", + "LIB Timing Library Format", + alts=["LIB"], + multiple=True, +).register() + +DesignFormat( + "spice", + "spice", + "Simulation Program with Integrated Circuit Emphasis", + alts=["SPICE"], +).register() + +DesignFormat( + "gds", + "gds", + "GDSII Stream", + alts=["GDS"], +).register() + +DesignFormat( + "vh", + "vh", + "Verilog Header", + alts=["VERILOG_HEADER"], +).register() diff --git a/openlane/state/state.py b/openlane/state/state.py index 1f8e6e377..4ade8117b 100644 --- a/openlane/state/state.py +++ b/openlane/state/state.py @@ -22,7 +22,6 @@ from .design_format import ( DesignFormat, - DesignFormatObject, ) from ..common import ( @@ -91,26 +90,21 @@ def __init__( if c_mapping := copying: for key, value in c_mapping.items(): if isinstance(key, DesignFormat): - copying_resolved[key.value.id] = value + copying_resolved[key.id] = value else: copying_resolved[key] = value - for format in DesignFormat: - assert isinstance(format.value, DesignFormatObject) # type checker shut up - if format.value.id not in copying_resolved: - copying_resolved[format.value.id] = None - overrides_resolved = {} if o_mapping := overrides: for k, value in o_mapping.items(): if isinstance(k, DesignFormat): - assert isinstance( - k.value, DesignFormatObject - ) # type checker shut up - k = k.value.id + k = k.id overrides_resolved[k] = value self.metrics = GenericImmutableDict(metrics or {}) + for key in list(copying_resolved.keys()): + if copying_resolved[key] is None: + del copying_resolved[key] super().__init__( copying_resolved, @@ -121,19 +115,19 @@ def __init__( def __getitem__(self, key: Union[DesignFormat, str]) -> StateElement: if isinstance(key, DesignFormat): - id: str = key.value.id + id: str = key.id key = id return super().__getitem__(key) def __setitem__(self, key: Union[DesignFormat, str], item: StateElement): if isinstance(key, DesignFormat): - id: str = key.value.id + id: str = key.id key = id return super().__setitem__(key, item) def __delitem__(self, key: Union[DesignFormat, str]): if isinstance(key, DesignFormat): - id: str = key.value.id + id: str = key.id key = id return super().__delitem__(key) @@ -164,10 +158,8 @@ def _walk( if current_top_key is None: current_top_key = key current_folder = key.strip("_*") - if df := DesignFormat.by_id(key): - # For type-checker: all guaranteed to be DesignFormatObjects - assert isinstance(df.value, DesignFormatObject) - current_folder = df.value.folder + if df := DesignFormat.factory.get(key): + current_folder = df.folder target_dir = os.path.join(save_directory, current_folder) @@ -228,7 +220,11 @@ def validate(self): """ def visitor(key, value, top_key, _, depth): - if depth == 0 and DesignFormat.by_id(top_key) is None: + if ( + depth == 0 + and DesignFormat.factory.get(top_key) is None + and value is not None + ): raise InvalidState( f"Key '{top_key}' does not match a known design format." ) @@ -314,8 +310,8 @@ def __mapping_to_html_rec( continue key_content = id - if format := DesignFormat.by_id(id): - key_content = format.value.id + if format := DesignFormat.factory.get(id): + key_content = format.id value_content = str(value) if isinstance(value, Mapping): diff --git a/openlane/steps/klayout.py b/openlane/steps/klayout.py index 14db3d1a0..527775633 100644 --- a/openlane/steps/klayout.py +++ b/openlane/steps/klayout.py @@ -29,6 +29,14 @@ from ..common import Path, get_script_dir, mkdirp, _get_process_limit +DesignFormat( + "klayout_gds", + "klayout.gds", + "GDSII Stream (KLayout)", + alts=["KLAYOUT_GDS"], +).register() + + class KLayoutStep(Step): config_vars = [ Variable( @@ -149,7 +157,7 @@ class Render(KLayoutStep): def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: input_view = state_in[DesignFormat.DEF] - if gds := state_in[DesignFormat.GDS]: + if gds := state_in.get(DesignFormat.GDS): input_view = gds assert isinstance(input_view, Path) @@ -193,7 +201,7 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: klayout_gds_out = os.path.join( self.step_dir, - f"{self.config['DESIGN_NAME']}.{DesignFormat.KLAYOUT_GDS.value.extension}", + f"{self.config['DESIGN_NAME']}.{DesignFormat.KLAYOUT_GDS.extension}", ) kwargs, env = self.extract_env(kwargs) @@ -205,7 +213,7 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: "klayout", "stream_out.py", ), - state_in[DesignFormat.DEF.value.id], + state_in[DesignFormat.DEF.id], "--output", abspath(klayout_gds_out), "--top", diff --git a/openlane/steps/magic.py b/openlane/steps/magic.py index bd27f9c1b..fc24fd82d 100644 --- a/openlane/steps/magic.py +++ b/openlane/steps/magic.py @@ -37,6 +37,22 @@ from ..common import get_script_dir, DRC as DRCObject, Path, mkdirp +DesignFormat( + "mag", + "mag", + "Magic VLSI View", + alts=["MAG"], +).register() + + +DesignFormat( + "mag_gds", + "magic.gds", + "GDSII Stream (Magic)", + alts=["MAG_GDS"], +).register() + + class MagicOutputProcessor(OutputProcessor): _error_patterns = [ re.compile(rx) @@ -192,10 +208,10 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: views_updates: ViewsUpdate = {} for output in self.outputs: - if output.value.multiple: + if output.multiple: # Too step-specific. continue - path = Path(env[f"SAVE_{output.name}"]) + path = Path(env[f"SAVE_{output.id.upper()}"]) if not path.exists(): continue views_updates[output] = path diff --git a/openlane/steps/odb.py b/openlane/steps/odb.py index 63a9d7e48..71ea6b77d 100644 --- a/openlane/steps/odb.py +++ b/openlane/steps/odb.py @@ -73,9 +73,9 @@ def run(self, state_in, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: views_updates: ViewsUpdate = {} command = self.get_command() for output in automatic_outputs: - filename = f"{self.config['DESIGN_NAME']}.{output.value.extension}" + filename = f"{self.config['DESIGN_NAME']}.{output.extension}" file_path = os.path.join(self.step_dir, filename) - command.append(f"--output-{output.value.id}") + command.append(f"--output-{output.id}") command.append(file_path) views_updates[output] = Path(file_path) @@ -123,7 +123,7 @@ def get_command(self) -> List[str]: for lef in extra_lefs: lefs.append("--input-lef") lefs.append(lef) - if (design_lef := self.state_in.result()[DesignFormat.LEF]) and ( + if (design_lef := self.state_in.result().get(DesignFormat.LEF)) and ( DesignFormat.LEF in self.inputs ): lefs.append("--design-lef") diff --git a/openlane/steps/openroad.py b/openlane/steps/openroad.py index 11cd6efe3..a74dafcbb 100644 --- a/openlane/steps/openroad.py +++ b/openlane/steps/openroad.py @@ -127,6 +127,22 @@ def pdn_macro_migrator(x): return [x.strip()] +DesignFormat( + "odb", + "odb", + "OpenDB Database", + alts=["ODB"], +).register() + +DesignFormat( + "openroad_lef", + "openroad.lef", + "Library Exchange Format Generated by OpenROAD", + alts=["OPENROAD_LEF"], + folder_override="lef", +).register() + + @Step.factory.register() class CheckSDCFiles(Step): """ @@ -284,10 +300,10 @@ def run(self, state_in, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: views_updates: ViewsUpdate = {} for output in self.outputs: - if output.value.multiple: + if output.multiple: # Too step-specific. continue - path = Path(env[f"SAVE_{output.name}"]) + path = Path(env[f"SAVE_{output.id.upper()}"]) if not path.exists(): continue views_updates[output] = path @@ -412,7 +428,7 @@ def _get_corner_files( name = timing_corner current_corner_spef = None - input_spef_dict = state_in[DesignFormat.SPEF] + input_spef_dict = state_in.get(DesignFormat.SPEF) if input_spef_dict is not None: if not isinstance(input_spef_dict, dict): raise StepException( @@ -729,7 +745,7 @@ def run_corner( def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: views_updates, metrics_updates = super().run(state_in, **kwargs) - sdf_dict = state_in[DesignFormat.SDF] or {} + sdf_dict = state_in.get(DesignFormat.SDF, {}) if not isinstance(sdf_dict, dict): raise StepException( "Malformed input state: incoming value for SDF is not a dictionary." @@ -863,7 +879,7 @@ def run_corner( def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: views_updates, metrics_updates = super().run(state_in, **kwargs) - lib_dict = state_in[DesignFormat.LIB] or {} + lib_dict = state_in.get(DesignFormat.LIB, {}) if not isinstance(lib_dict, dict): raise StepException( "Malformed input state: value for LIB is not a dictionary." @@ -1775,7 +1791,7 @@ def run_corner(corner: str): views_updates: ViewsUpdate = {} metrics_updates: MetricsUpdate = {} - spef_dict = state_in[DesignFormat.SPEF] or {} + spef_dict = state_in.get(DesignFormat.SPEF, {}) if not isinstance(spef_dict, dict): raise StepException( "Malformed input state: value for SPEF is not a dictionary." @@ -1940,8 +1956,8 @@ class WriteViews(OpenROADStep): id = "OpenROAD.WriteViews" name = "OpenROAD Write Views" outputs = OpenROADStep.outputs + [ - DesignFormat.POWERED_NETLIST_SDF_FRIENDLY, - DesignFormat.POWERED_NETLIST_NO_PHYSICAL_CELLS, + DesignFormat.SDF_FRIENDLY_POWERED_NETLIST, + DesignFormat.LOGICAL_POWERED_NETLIST, DesignFormat.OPENROAD_LEF, ] diff --git a/openlane/steps/pyosys.py b/openlane/steps/pyosys.py index 8f76ddc21..3ca457f23 100644 --- a/openlane/steps/pyosys.py +++ b/openlane/steps/pyosys.py @@ -133,6 +133,13 @@ def _parse_yosys_check( ), ] +DesignFormat( + "json_h", + "h.json", + "Design JSON Header File", + alts=["JSON_HEADER"], +).register() + class PyosysStep(Step): config_vars = [ @@ -308,14 +315,14 @@ def get_script_path(self) -> str: def get_command(self, state_in: State) -> List[str]: out_file = os.path.join( self.step_dir, - f"{self.config['DESIGN_NAME']}.{DesignFormat.JSON_HEADER.value.extension}", + f"{self.config['DESIGN_NAME']}.{DesignFormat.JSON_HEADER.extension}", ) return super().get_command(state_in) + ["--output", out_file] def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: out_file = os.path.join( self.step_dir, - f"{self.config['DESIGN_NAME']}.{DesignFormat.JSON_HEADER.value.extension}", + f"{self.config['DESIGN_NAME']}.{DesignFormat.JSON_HEADER.extension}", ) views_updates, metrics_updates = super().run(state_in, **kwargs) views_updates[DesignFormat.JSON_HEADER] = Path(out_file) @@ -466,7 +473,7 @@ def get_script_path(self) -> str: def get_command(self, state_in: State) -> List[str]: out_file = os.path.join( self.step_dir, - f"{self.config['DESIGN_NAME']}.{DesignFormat.NETLIST.value.extension}", + f"{self.config['DESIGN_NAME']}.{DesignFormat.NETLIST.extension}", ) cmd = super().get_command(state_in) if self.config["USE_LIGHTER"]: @@ -491,7 +498,7 @@ def get_command(self, state_in: State) -> List[str]: def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: out_file = os.path.join( self.step_dir, - f"{self.config['DESIGN_NAME']}.{DesignFormat.NETLIST.value.extension}", + f"{self.config['DESIGN_NAME']}.{DesignFormat.NETLIST.extension}", ) view_updates, metric_updates = super().run(state_in, **kwargs) diff --git a/openlane/steps/step.py b/openlane/steps/step.py index 15d18d575..2871b1110 100644 --- a/openlane/steps/step.py +++ b/openlane/steps/step.py @@ -53,7 +53,7 @@ Variable, universal_flow_config_variables, ) -from ..state import DesignFormat, DesignFormatObject, State, InvalidState, StateElement +from ..state import DesignFormat, State, InvalidState, StateElement from ..common import ( GenericDict, GenericImmutableDict, @@ -661,7 +661,7 @@ def get_help_md( for input, output in zip_longest(Self.inputs, Self.outputs): input_str = "" if input is not None: - input_str = f"{input.value.name} (.{input.value.extension})" + input_str = f"{input.full_name} (.{input.extension})" output_str = "" if output is not None: @@ -669,7 +669,7 @@ def get_help_md( raise StepException( f"Output '{output}' is not a valid DesignFormat enum object." ) - output_str = f"{output.value.name} (.{output.value.extension})" + output_str = f"{output.full_name} (.{output.extension})" result += f"| {input_str} | {output_str} |\n" if len(Self.config_vars): @@ -733,9 +733,9 @@ def _repr_markdown_(self) -> str: # pragma: no cover continue if state_in.get(id) != value: - df = DesignFormat.by_id(id) + df = DesignFormat.factory.get(id) assert df is not None - views_updated.append(df.value.name) + views_updated.append(df.full_name) if len(views_updated): result += "#### Views updated:\n" @@ -996,14 +996,14 @@ def filename_with_counter(): # 2. State state_in: GenericDict[str, Any] = self.state_in.result().copy_mut() - for format in DesignFormat: - assert isinstance(format.value, DesignFormatObject) # type checker shut up + for format_id in state_in: + format = DesignFormat.factory.get(format_id) if format not in self.__class__.inputs and not ( format == DesignFormat.DEF and DesignFormat.ODB in self.__class__.inputs # hack to write tests a bit more easily ): - state_in[format.value.id] = None + state_in[format.id] = None state_in["metrics"] = self.state_in.result().metrics.copy_mut() dumpable_state = copy_recursive(state_in, translator=visitor) state_path = os.path.join(target_dir, "state_in.json") @@ -1143,7 +1143,7 @@ def start( value = state_in_result[input] if value is None: raise StepException( - f"{type(self).__name__}: missing required input '{input.name}'" + f"{type(self).__name__}: missing required input '{input.id}'" ) from None try: @@ -1537,7 +1537,7 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: for key in state: if ( state_in.get(key) != state.get(key) - and DesignFormat.by_id(key) in self.outputs + and DesignFormat.factory.get(key) in self.outputs ): views_updates[key] = state[key] for key in state.metrics: diff --git a/openlane/steps/tclstep.py b/openlane/steps/tclstep.py index a4277b4fe..c78751680 100644 --- a/openlane/steps/tclstep.py +++ b/openlane/steps/tclstep.py @@ -158,15 +158,15 @@ def prepare_env(self, env: dict, state: State) -> dict: env[element] = TclStep.value_to_tcl(value) for input in self.inputs: - key = f"CURRENT_{input.name}" + key = f"CURRENT_{input.id.upper()}" env[key] = TclStep.value_to_tcl(state[input]) for output in self.outputs: - if output.value.multiple: + if output.multiple: # Too step-specific. continue - filename = f"{self.config['DESIGN_NAME']}.{output.value.extension}" - env[f"SAVE_{output.name}"] = os.path.join(self.step_dir, filename) + filename = f"{self.config['DESIGN_NAME']}.{output.extension}" + env[f"SAVE_{output.id.upper()}"] = os.path.join(self.step_dir, filename) return env @@ -210,10 +210,10 @@ def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: overrides: ViewsUpdate = {} for output in self.outputs: - if output.value.multiple: + if output.multiple: # Too step-specific. continue - path = Path(env[f"SAVE_{output.name}"]) + path = Path(env[f"SAVE_{output.id.upper()}"]) if not path.exists(): continue overrides[output] = path diff --git a/test/flows/test_flow.py b/test/flows/test_flow.py index 636f57174..0a8972058 100644 --- a/test/flows/test_flow.py +++ b/test/flows/test_flow.py @@ -71,7 +71,9 @@ def run(self, state_in: State, **kwargs): json.dump( { "probably_a_valid_header": False, - "previous_invalid_header": str(state_in[DesignFormat.JSON_HEADER]), + "previous_invalid_header": str( + state_in.get(DesignFormat.JSON_HEADER) + ), }, open(out_file, "w", encoding="utf8"), ) diff --git a/test/state/test_state.py b/test/state/test_state.py index 27a9d7d68..ed4bf36d9 100644 --- a/test/state/test_state.py +++ b/test/state/test_state.py @@ -126,15 +126,15 @@ def test_copy(): def test_empty(): - from openlane.state import DesignFormat, State + from openlane.state import State test_dict = {} test_metrics = {} state = State(test_dict, metrics=test_metrics) assert state.metrics == test_metrics - for format in DesignFormat: - assert state[format.value.id] is None, "new state has non-none value" + + assert len(state) == 0, "New empty state has entries" def test_path_fail_exists(): diff --git a/test/steps/test_tclstep.py b/test/steps/test_tclstep.py index 9166e0618..f2ac6c410 100644 --- a/test/steps/test_tclstep.py +++ b/test/steps/test_tclstep.py @@ -186,8 +186,8 @@ def get_script_path(self): assert ( env["STEP_DIR"] == TclStepTest.step_dir ), "Wrong prepared env. Bad STEP_DIR value" - assert env["CURRENT_NETLIST"] == "abc", "Wrong prepared env. Bad CURRENT_ input" - assert "SAVE_NETLIST" in env, "Wrong prepared env. SAVE_NETLIST missing" + assert env["CURRENT_NL"] == "abc", "Wrong prepared env. Bad CURRENT_ input" + assert "SAVE_NL" in env, "Wrong prepared env. SAVE_NL missing" for var in mock_config: if mock_config[var] is not None: assert var in env, "Wrong prepared env. Missing config variable" From ef539d261726ffd6367d782266b2506531c8cd8d Mon Sep 17 00:00:00 2001 From: Kareem Farid Date: Mon, 16 Dec 2024 00:04:53 +0200 Subject: [PATCH 2/3] feat: expose some CTS options (#589) * `OpenROAD.CTS` * Added flags `CTS_OBSTRUCTION_AWARE` and `CTS_BALANCE_LEVELS` * Added `CTS_SINK_BUFFER_MAX_CAP_DERATE_PCT` --- Changelog.md | 7 +++++++ openlane/scripts/openroad/cts.tcl | 14 +++++++++++++- openlane/steps/openroad.py | 26 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 6fe12eb3a..f7fe6d33a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,6 +16,13 @@ # Dev +## Steps + +* `OpenROAD.CTS` + * Added flags `CTS_OBSTRUCTION_AWARE` and `CTS_BALANCE_LEVELS` + * Added `CTS_SINK_BUFFER_MAX_CAP_DERATE_PCT` + * Added `CTS_DELAY_BUFFER_DERATE_PCT` + ## Misc Enhancements/Bugfixes * `openlane.state` diff --git a/openlane/scripts/openroad/cts.tcl b/openlane/scripts/openroad/cts.tcl index 60faa3441..81fcf05de 100755 --- a/openlane/scripts/openroad/cts.tcl +++ b/openlane/scripts/openroad/cts.tcl @@ -50,11 +50,23 @@ lappend arg_list -sink_clustering_enable if { $::env(CTS_DISTANCE_BETWEEN_BUFFERS) != 0 } { lappend arg_list -distance_between_buffers $::env(CTS_DISTANCE_BETWEEN_BUFFERS) } - if { $::env(CTS_DISABLE_POST_PROCESSING) } { lappend arg_list -post_cts_disable } +if { [info exists ::env(CTS_OBSTRUCTION_AWARE)] && $::env(CTS_OBSTRUCTION_AWARE) } { + lappend arg_list -obstruction_aware +} +if { [info exists ::env(CTS_SINK_BUFFER_MAX_CAP_DERATE_PCT)] } { + lappend arg_list -sink_buffer_max_cap_derate [expr $::env(CTS_SINK_BUFFER_MAX_CAP_DERATE_PCT) / 100.0] +} +if { [info exists ::env(CTS_DELAY_BUFFER_DERATE_PCT)] } { + lappend arg_list -delay_buffer_derate [expr $::env(CTS_DELAY_BUFFER_DERATE_PCT) / 100] +} +if { [info exists ::env(CTS_BALANCE_LEVELS)] && $::env(CTS_BALANCE_LEVELS) } { + lappend arg_list -balance_levels +} +puts "clock_tree_synthesis {*}$arg_list" clock_tree_synthesis {*}$arg_list set_propagated_clock [all_clocks] diff --git a/openlane/steps/openroad.py b/openlane/steps/openroad.py index a74dafcbb..470f9f3e2 100644 --- a/openlane/steps/openroad.py +++ b/openlane/steps/openroad.py @@ -2028,6 +2028,32 @@ class CTS(ResizerStep): OpenROADStep.config_vars + dpl_variables + [ + # sink_buffer_max_cap_derate + Variable( + "CTS_BALANCE_LEVELS", + Optional[bool], + "Attempts to keep a similar number of levels in the clock tree across non-register cells (e.g., clock-gate or inverter).", + ), + Variable( + "CTS_SINK_BUFFER_MAX_CAP_DERATE_PCT", + Optional[Decimal], + "Controls automatic buffer selection. To favor strong(weak) drive strength buffers use a small(large) value." + + "The value of 100 means no derating of max cap limit", + units="%", + ), + Variable( + "CTS_DELAY_BUFFER_DERATE_PCT", + Optional[Decimal], + "This option balances latencies between macro cells and registers by inserting delay buffers" + + "The value of 100 means all needed delay buffers are inserted", + units="%", + ), + Variable( + "CTS_OBSTRUCTION_AWARE", + Optional[bool], + "Enables obstruction-aware buffering such that clock buffers are not placed on top of blockages or hard macros. " + + "This option may reduce legalizer displacement, leading to better latency, skew or timing QoR.", + ), Variable( "CTS_SINK_CLUSTERING_SIZE", int, From 237bbe348f76618e8f28b644502113e41c7c2c47 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Mon, 16 Dec 2024 12:37:03 +0200 Subject: [PATCH 3/3] feat: expand synthesis options, initial state elements in cli (#604) * `Yosys.*Synthesis` * Created new variable `SYNTH_HIERARCHY_MODE`, replacing `SYNTH_NO_FLAT`. There are three options, `flatten`, `deferred_flatten` and `keep`. The first two correspond to `SYNTH_NO_FLAT` being false and true respectively. The third keeps the hierarchy in the final netlist. * Created new variable `SYNTH_TIE_UNDEFINED` to customize whether undefined and undriven values are tied low, high, or left as-is. * Created new variable `SYNTH_WRITE_NOATTR` to allow attributes to be propagated to the final netlist. * Created `Yosys.Resynthesis` * Like `Yosys.Synthesis`, but uses the current input state netlist as an input instead of RTL files ## CLI * Added new option: `-e`/`--initial-state-element-override`: allows an element in the initial state to be overridden straight from the commandline. --- While this is unorthodox, this will be release 2.3.0. The reasoning for this is that these options are critical for an on-going tapeout process. --- Changelog.md | 30 ++++ flake.lock | 23 ++- flake.nix | 2 +- nix/create-shell.nix | 5 +- openlane/__main__.py | 52 ++++-- openlane/flows/cli.py | 12 +- openlane/scripts/openroad/gpl.tcl | 4 + .../scripts/openroad/rsz_timing_postcts.tcl | 38 +++-- .../scripts/openroad/rsz_timing_postgrt.tcl | 36 ++-- openlane/scripts/pyosys/synthesize.py | 161 ++++++++++++++++-- openlane/steps/openroad.py | 17 ++ openlane/steps/pyosys.py | 50 +++++- pyproject.toml | 4 +- 13 files changed, 347 insertions(+), 87 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5e4944483..53e1d64c4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,36 @@ ## Documentation --> +# 2.3.0 + +## Steps + +* `OpenROAD.GlobalPlacement` + + * Exposed `-routability_check_overflow` argument as new variable + `PL_ROUTABILITY_OVERFLOW_THRESHOLD`. + +* `Yosys.*Synthesis` + + * Created new variable `SYNTH_HIERARCHY_MODE`, replacing `SYNTH_NO_FLAT`. + There are three options, `flatten`, `deferred_flatten` and `keep`. The first + two correspond to `SYNTH_NO_FLAT` being false and true respectively. The + third keeps the hierarchy in the final netlist. + * Created new variable `SYNTH_TIE_UNDEFINED` to customize whether undefined + and undriven values are tied low, high, or left as-is. + * Created new variable `SYNTH_WRITE_NOATTR` to allow attributes to be + propagated to the final netlist. + +* Created `Yosys.Resynthesis` + + * Like `Yosys.Synthesis`, but uses the current input state netlist as an input + instead of RTL files + +## CLI + +* Added new option: `-e`/`--initial-state-element-override`: allows an element + in the initial state to be overridden straight from the commandline. + # 2.2.9 ## Steps diff --git a/flake.lock b/flake.lock index a7eaab0ff..700f92bbb 100644 --- a/flake.lock +++ b/flake.lock @@ -37,17 +37,16 @@ }, "ioplace-parser": { "inputs": { - "nixpkgs": [ - "nix-eda", - "nixpkgs" + "nix-eda": [ + "nix-eda" ] }, "locked": { - "lastModified": 1719837045, - "narHash": "sha256-ya2KEXKAIn8oYUi9G9TaUcQAEfGkbENCgXF/V0d/kws=", + "lastModified": 1730973673, + "narHash": "sha256-F7jux5RLKTFEcXEQ2mRIH83zlSAT+uffYsf/XnKgoiU=", "owner": "efabless", "repo": "ioplace_parser", - "rev": "570fd3e352926f57e6eecbb8bd3892a5dec375b7", + "rev": "e31eb8553caba31c08416d89e31b6aec58002755", "type": "github" }, "original": { @@ -82,11 +81,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1728905391, - "narHash": "sha256-iox9yGNG4MwSKhQuwegLcDW6wVGzfdBPrh8SrhSLA8c=", + "lastModified": 1730973643, + "narHash": "sha256-8ZeUxluUbUD3IF0thq4Alioy3JaEVR0AY5+7PuFypBE=", "owner": "efabless", "repo": "nix-eda", - "rev": "0814aa6c1c7d556aa08212cc875063cff62cb9b0", + "rev": "1f8771758e26ad96da4947ed1b17e3942f978cfa", "type": "github" }, "original": { @@ -129,11 +128,11 @@ ] }, "locked": { - "lastModified": 1724178650, - "narHash": "sha256-p+Li/z3l7ShRSIKUrxVK+7uGhSvqPE7jOW4FA4k3SIw=", + "lastModified": 1728904439, + "narHash": "sha256-e6QXMol9kbrJplYamSDfz+X/BDhgP66rHmjkeIZlaNI=", "owner": "efabless", "repo": "volare", - "rev": "0090420799258d29adca54c8951d38bb8b605aeb", + "rev": "7458169adc63dfac4ecfcb1c967972c6d8cd888f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 708cd1268..e3210fd60 100644 --- a/flake.nix +++ b/flake.nix @@ -33,7 +33,7 @@ }; inputs.libparse.inputs.nixpkgs.follows = "nix-eda/nixpkgs"; - inputs.ioplace-parser.inputs.nixpkgs.follows = "nix-eda/nixpkgs"; + inputs.ioplace-parser.inputs.nix-eda.follows = "nix-eda"; inputs.volare.inputs.nixpkgs.follows = "nix-eda/nixpkgs"; inputs.devshell.inputs.nixpkgs.follows = "nix-eda/nixpkgs"; diff --git a/nix/create-shell.nix b/nix/create-shell.nix index 57f5fc49e..f3549551f 100644 --- a/nix/create-shell.nix +++ b/nix/create-shell.nix @@ -14,8 +14,9 @@ { extra-packages ? [], extra-python-packages ? [], + extra-env ? [], openlane-plugins ? [], - include-openlane ? true + include-openlane ? true, }: ({ lib, git, @@ -62,7 +63,7 @@ in name = "NIX_PYTHONPATH"; value = "${openlane-env-sitepackages}"; } - ]; + ] ++ extra-env; devshell.interactive.PS1 = { text = ''PS1="${prompt}"''; }; diff --git a/openlane/__main__.py b/openlane/__main__.py index 86780f0e5..a1dcef55b 100644 --- a/openlane/__main__.py +++ b/openlane/__main__.py @@ -23,12 +23,7 @@ from functools import partial from typing import Any, Dict, Sequence, Tuple, Type, Optional, List, Union -from click import ( - Parameter, - Context, - Path, - pass_context, -) +import click from cloup import ( option, option_group, @@ -40,7 +35,7 @@ from .__version__ import __version__ -from .state import State +from .state import State, DesignFormat from .logging import ( debug, err, @@ -56,7 +51,7 @@ def run( - ctx: Context, + ctx: click.Context, flow_name: Optional[str], pdk_root: Optional[str], pdk: str, @@ -73,6 +68,7 @@ def run( config_override_strings: List[str], _force_run_dir: Optional[str], design_dir: Optional[str], + initial_state_element_override: Sequence[str], view_save_path: Optional[str] = None, ef_view_save_path: Optional[str] = None, ): @@ -97,6 +93,27 @@ def run( if flow_description is None: flow_description = "Classic" + if len(initial_state_element_override): + if with_initial_state is None: + with_initial_state = State() + overrides = {} + for element in initial_state_element_override: + element_split = element.split("=", maxsplit=1) + if len(element_split) < 2: + err(f"Invalid initial state element override: '{element}'.") + ctx.exit(1) + df_id, path = element_split + design_format = DesignFormat.by_id(df_id) + if design_format is None: + err(f"Invalid design format ID: '{df_id}'.") + ctx.exit(1) + overrides[design_format] = common.Path(path) + + with_initial_state = with_initial_state.__class__( + with_initial_state, + overrides=overrides, + ) + TargetFlow: Type[Flow] if isinstance(flow_description, str): @@ -170,7 +187,7 @@ def run( flow._save_snapshot_ef(evsp) -def print_version(ctx: Context, param: Parameter, value: bool): +def print_version(ctx: click.Context, param: click.Parameter, value: bool): if not value: return @@ -198,8 +215,8 @@ def print_version(ctx: Context, param: Parameter, value: bool): def print_bare_version( - ctx: Context, - param: Parameter, + ctx: click.Context, + param: click.Parameter, value: bool, ): if not value: @@ -209,7 +226,7 @@ def print_bare_version( def run_included_example( - ctx: Context, + ctx: click.Context, smoke_test: bool, example: Optional[str], **kwargs, @@ -285,8 +302,8 @@ def run_included_example( def cli_in_container( - ctx: Context, - param: Parameter, + ctx: click.Context, + param: click.Parameter, value: bool, ): if not value: @@ -331,14 +348,14 @@ def cli_in_container( o( "--save-views-to", "view_save_path", - type=Path(file_okay=False, dir_okay=True), + type=click.Path(file_okay=False, dir_okay=True), default=None, help="A directory to copy the final views to, where each format is saved under a directory named after the corner ID (much like the 'final' directory after running a flow.)", ), o( "--ef-save-views-to", "ef_view_save_path", - type=Path(file_okay=False, dir_okay=True), + type=click.Path(file_okay=False, dir_okay=True), default=None, help="A directory to copy the final views to in the Efabless format, compatible with Caravel User Project.", ), @@ -401,8 +418,9 @@ def cli_in_container( _enable_debug_flags=True, sequential_flow_reproducible=True, enable_overwrite_flag=True, + enable_initial_state_element=True, ) -@pass_context +@click.pass_context def cli(ctx, /, **kwargs): """ Runs an OpenLane flow via the commandline using a design configuration diff --git a/openlane/flows/cli.py b/openlane/flows/cli.py index 09e1b8d0d..6378c83d2 100644 --- a/openlane/flows/cli.py +++ b/openlane/flows/cli.py @@ -139,6 +139,7 @@ def cloup_flow_opts( volare_pdk_override: Optional[str] = None, _enable_debug_flags: bool = False, enable_overwrite_flag: bool = False, + enable_initial_state_element: bool = False, ) -> Decorator: """ Creates a wrapper that appends a number of OpenLane flow-related flags to a @@ -214,7 +215,7 @@ def decorate(f): ), default=None, callback=initial_state_cb, - help="Use this JSON file as an initial state. If this is not specified, the latest `state_out.json` of the run directory will be used if available.", + help="Use this JSON file as an initial state. If this is not specified, the latest `state_out.json` of the run directory will be used. If none exist, an empty initial state is created.", )(f) f = o( "--design-dir", @@ -381,6 +382,15 @@ def decorate(f): callback=set_worker_count_cb, expose_value=False, )(f) + if enable_initial_state_element: + f = o( + "-e", + "--initial-state-element-override", + type=str, + multiple=True, + default=(), + help="Elements to override in the used initial state in the format DESIGN_FORMAT_ID=PATH", + )(f) if accept_config_files: f = argument( "config_files", diff --git a/openlane/scripts/openroad/gpl.tcl b/openlane/scripts/openroad/gpl.tcl index 1652456d3..ad1015e6a 100755 --- a/openlane/scripts/openroad/gpl.tcl +++ b/openlane/scripts/openroad/gpl.tcl @@ -47,6 +47,9 @@ if { [info exists ::env(PL_ROUTABILITY_DRIVEN)] && $::env(PL_ROUTABILITY_DRIVEN) set_macro_extension $::env(GRT_MACRO_EXTENSION) source $::env(SCRIPTS_DIR)/openroad/common/set_layer_adjustments.tcl lappend arg_list -routability_driven + if { [info exists ::env(PL_ROUTABILITY_OVERFLOW_THRESHOLD)] } { + lappend arg_list -routability_check_overflow $::env(PL_ROUTABILITY_OVERFLOW_THRESHOLD) + } } if { $::env(PL_SKIP_INITIAL_PLACEMENT) } { @@ -72,6 +75,7 @@ lappend arg_list -pad_right $cell_pad_side lappend arg_list -pad_left $cell_pad_side lappend arg_list -init_wirelength_coef $::env(PL_WIRE_LENGTH_COEF) +puts "+ global_placement $arg_list" global_placement {*}$arg_list diff --git a/openlane/scripts/openroad/rsz_timing_postcts.tcl b/openlane/scripts/openroad/rsz_timing_postcts.tcl index 5cc82b427..be22825ea 100644 --- a/openlane/scripts/openroad/rsz_timing_postcts.tcl +++ b/openlane/scripts/openroad/rsz_timing_postcts.tcl @@ -29,26 +29,32 @@ source $::env(SCRIPTS_DIR)/openroad/common/set_rc.tcl estimate_parasitics -placement # Resize -set arg_list [list] -lappend arg_list -verbose -lappend arg_list -setup -lappend arg_list -setup_margin $::env(PL_RESIZER_SETUP_SLACK_MARGIN) -lappend arg_list -max_buffer_percent $::env(PL_RESIZER_SETUP_MAX_BUFFER_PCT) +set setup_args [list] +lappend setup_args -verbose +lappend setup_args -setup +lappend setup_args -setup_margin $::env(PL_RESIZER_SETUP_SLACK_MARGIN) +lappend setup_args -max_buffer_percent $::env(PL_RESIZER_SETUP_MAX_BUFFER_PCT) if { $::env(PL_RESIZER_GATE_CLONING) != 1 } { - lappend arg_list -skip_gate_cloning + lappend setup_args -skip_gate_cloning } -repair_timing {*}$arg_list - -set arg_list [list] -lappend arg_list -verbose -lappend arg_list -hold -lappend arg_list -setup_margin $::env(PL_RESIZER_SETUP_SLACK_MARGIN) -lappend arg_list -hold_margin $::env(PL_RESIZER_HOLD_SLACK_MARGIN) -lappend arg_list -max_buffer_percent $::env(PL_RESIZER_HOLD_MAX_BUFFER_PCT) + +set hold_args [list] +lappend hold_args -verbose +lappend hold_args -hold +lappend hold_args -setup_margin $::env(PL_RESIZER_SETUP_SLACK_MARGIN) +lappend hold_args -hold_margin $::env(PL_RESIZER_HOLD_SLACK_MARGIN) +lappend hold_args -max_buffer_percent $::env(PL_RESIZER_HOLD_MAX_BUFFER_PCT) if { $::env(PL_RESIZER_ALLOW_SETUP_VIOS) == 1 } { - lappend arg_list -allow_setup_violations + lappend hold_args -allow_setup_violations +} + +if { $::env(PL_RESIZER_FIX_HOLD_FIRST) == 1 } { + repair_timing {*}$hold_args + repair_timing {*}$setup_args +} else { + repair_timing {*}$setup_args + repair_timing {*}$hold_args } -repair_timing {*}$arg_list # Legalize source $::env(SCRIPTS_DIR)/openroad/common/dpl.tcl diff --git a/openlane/scripts/openroad/rsz_timing_postgrt.tcl b/openlane/scripts/openroad/rsz_timing_postgrt.tcl index aa85a38a3..2ad629281 100644 --- a/openlane/scripts/openroad/rsz_timing_postgrt.tcl +++ b/openlane/scripts/openroad/rsz_timing_postgrt.tcl @@ -32,26 +32,32 @@ source $::env(SCRIPTS_DIR)/openroad/common/grt.tcl estimate_parasitics -global_routing # Resize -set arg_list [list] -lappend arg_list -verbose -lappend arg_list -setup -lappend arg_list -setup_margin $::env(GRT_RESIZER_SETUP_SLACK_MARGIN) -lappend arg_list -max_buffer_percent $::env(GRT_RESIZER_SETUP_MAX_BUFFER_PCT) +set setup_args [list] +lappend setup_args -verbose +lappend setup_args -setup +lappend setup_args -setup_margin $::env(GRT_RESIZER_SETUP_SLACK_MARGIN) +lappend setup_args -max_buffer_percent $::env(GRT_RESIZER_SETUP_MAX_BUFFER_PCT) if { $::env(GRT_RESIZER_GATE_CLONING) != 1 } { - lappend arg_list -skip_gate_cloning + lappend setup_args -skip_gate_cloning } -repair_timing {*}$arg_list -set arg_list [list] -lappend arg_list -verbose -lappend arg_list -hold -lappend arg_list -setup_margin $::env(GRT_RESIZER_SETUP_SLACK_MARGIN) -lappend arg_list -hold_margin $::env(GRT_RESIZER_HOLD_SLACK_MARGIN) -lappend arg_list -max_buffer_percent $::env(GRT_RESIZER_HOLD_MAX_BUFFER_PCT) +set hold_args [list] +lappend hold_args -verbose +lappend hold_args -hold +lappend hold_args -setup_margin $::env(GRT_RESIZER_SETUP_SLACK_MARGIN) +lappend hold_args -hold_margin $::env(GRT_RESIZER_HOLD_SLACK_MARGIN) +lappend hold_args -max_buffer_percent $::env(GRT_RESIZER_HOLD_MAX_BUFFER_PCT) if { $::env(GRT_RESIZER_ALLOW_SETUP_VIOS) == 1 } { - lappend arg_list -allow_setup_violations + lappend hold_args -allow_setup_violations +} + +if { $::env(GRT_RESIZER_FIX_HOLD_FIRST) == 1 } { + repair_timing {*}$hold_args + repair_timing {*}$setup_args +} else { + repair_timing {*}$setup_args + repair_timing {*}$hold_args } -repair_timing {*}$arg_list # Re-DPL and GRT source $::env(SCRIPTS_DIR)/openroad/common/dpl.tcl diff --git a/openlane/scripts/pyosys/synthesize.py b/openlane/scripts/pyosys/synthesize.py index d0ed67f2d..600dffed1 100644 --- a/openlane/scripts/pyosys/synthesize.py +++ b/openlane/scripts/pyosys/synthesize.py @@ -12,7 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Parts of this file adapted from https://github.com/YosysHQ/yosys/blob/master/techlibs/common/synth.cc +# Parts of this file adapted from: +# +# * https://github.com/YosysHQ/yosys/blob/master/techlibs/common/synth.cc +# * https://github.com/YosysHQ/yosys/blob/master/passes/opt/opt.cc # # Copyright (C) 2012 Claire Xenia Wolf # @@ -57,7 +60,91 @@ def openlane_proc(d: ys.Design, report_dir: str): d.run_pass("opt_expr") # Optimize expressions -def openlane_synth(d, top, flatten, report_dir, *, booth=False, abc_dff=False): +def openlane_opt( + d, + fast=False, + mux_undef=False, + mux_bool=False, + undriven=False, + fine=False, + purge=False, + noclkinv=False, + keepdc=False, + share_all=False, + nodffe=False, + nosdff=False, + sat=False, + opt_share=False, + noff=False, +): + expr_args = [] + merge_args = [] + reduce_args = [] + clean_args = [] + dff_args = [] + if purge: + clean_args.append("-purge") + if mux_undef: + expr_args.append("-mux_undef") + if mux_bool: + expr_args.append("-mux_bool") + if undriven: + expr_args.append("-undriven") + if fine: + expr_args.append("-fine") + reduce_args.append("-fine") + if noclkinv: + expr_args.append("-noclkinv") + if keepdc: + expr_args.append("-keepdc") + dff_args.append("-keepdc") + merge_args.append("-keepdc") + if nodffe: + dff_args.append("-nodffe") + if nosdff: + dff_args.append("-nosdff") + if sat: + dff_args.append("-sat") + if share_all: + merge_args.append("-share_all") + if fast: + while True: + d.run_pass("opt_expr", *expr_args) + d.run_pass("opt_merge", *merge_args) + d.scratchpad_unset("opt.did_something") + if not noff: + d.run_pass("opt_dff", *dff_args) + if not d.scratchpad_get_bool("opt.did_something"): + break + d.run_pass("opt_clean", *clean_args) + ys.log_header(d, "Rerunning OPT passes (Removed registers in this run.)") + d.run_pass("opt_clean", *clean_args) + else: + d.run_pass("opt_expr", *expr_args) + d.run_pass("opt_merge", "-nomux", *merge_args) + while True: + d.scratchpad_unset("opt.did_something") + d.run_pass("opt_muxtree") + d.run_pass("opt_reduce", *reduce_args) + d.run_pass("opt_merge", *merge_args) + if opt_share: + d.run_pass("opt_share") + if not noff: + d.run_pass("opt_dff", *dff_args) + d.run_pass("opt_clean", *clean_args) + d.run_pass("opt_expr", *expr_args) + if not d.scratchpad_get_bool("opt.did_something"): + break + ys.log_header(d, "Rerunning OPT passes. (Maybe there is more to do…)") + + d.optimize() + d.sort() + d.check() + + +def openlane_synth( + d, top, flatten, report_dir, *, booth=False, abc_dff=False, undriven=True +): d.run_pass("hierarchy", "-check", "-top", top, "-nokeep_prints", "-nokeep_asserts") openlane_proc(d, report_dir) @@ -69,9 +156,9 @@ def openlane_synth(d, top, flatten, report_dir, *, booth=False, abc_dff=False): d.run_pass("opt_clean") # Clean up after optimization # Perform various logic optimization passes - d.run_pass("opt", "-nodffe", "-nosdff") # Optimize logic excluding flip-flops + openlane_opt(d, nodffe=True, nosdff=True) d.run_pass("fsm") # Identify and optimize finite state machines - d.run_pass("opt") # Additional logic optimization + openlane_opt(d) d.run_pass("wreduce") # Reduce logic using algebraic rewriting d.run_pass("peepopt") # Perform local peephole optimization d.run_pass("opt_clean") # Clean up after optimization @@ -79,21 +166,38 @@ def openlane_synth(d, top, flatten, report_dir, *, booth=False, abc_dff=False): d.run_pass("booth") d.run_pass("alumacc") # Optimize arithmetic logic unitsb d.run_pass("share") # Share logic across the design - d.run_pass("opt") # More logic optimization + openlane_opt(d) # Memory optimization d.run_pass("memory", "-nomap") # Analyze memories but don't map them yet d.run_pass("opt_clean") # Clean up after memory analysis # Perform more aggressive optimization with faster runtime - d.run_pass("opt", "-fast", "-full") # Fast and comprehensive optimization + openlane_opt( + d, + fast=True, + opt_share=True, # affects opt_share + mux_undef=True, # affects opt_expr + mux_bool=True, # affects opt_expr + undriven=undriven, # affects opt_expr + fine=True, # affects opt_expr, opt_reduce + ) # Technology mapping d.run_pass("memory_map") # Map memories to standard cells - d.run_pass("opt", "-full") # More optimization after memory mapping - d.run_pass("techmap") # Map logic to standard cells from the technology library - d.run_pass("opt", "-fast") # Fast optimization after technology mapping - d.run_pass("opt", "-fast") # More fast optimization + openlane_opt( + d, + opt_share=True, # affects opt_share + mux_undef=True, # affects opt_expr + mux_bool=True, # affects opt_expr + undriven=undriven, # affects opt_expr + fine=True, # affects opt_expr, opt_reduce + ) + d.run_pass("techmap") + + # Couple more fast opt iterations + openlane_opt(d, fast=True) + openlane_opt(d, fast=True) d.run_pass( "abc", "-fast", *(["-dff"] if abc_dff else []) @@ -111,11 +215,13 @@ def openlane_synth(d, top, flatten, report_dir, *, booth=False, abc_dff=False): @click.option("--config-in", type=click.Path(exists=True), required=True) @click.option("--extra-in", type=click.Path(exists=True), required=True) @click.option("--lighter-dff-map", type=click.Path(exists=True), required=False) +@click.argument("inputs", nargs=-1) def synthesize( output, config_in, extra_in, lighter_dff_map, + inputs, ): config = json.load(open(config_in)) extra = json.load(open(extra_in)) @@ -150,7 +256,17 @@ def synthesize( ys.log(f"[INFO] Using SDC file '{sdc_path}' for ABC…") - if verilog_files := config.get("VERILOG_FILES"): + if len(inputs): + d.read_verilog_files( + inputs, + top=config["DESIGN_NAME"], + synth_parameters=[], + includes=includes, + defines=defines, + use_synlig=False, + synlig_defer=False, + ) + elif verilog_files := config.get("VERILOG_FILES"): d.read_verilog_files( verilog_files, top=config["DESIGN_NAME"], @@ -203,10 +319,13 @@ def synthesize( d.tee("stat", "-json", *lib_arguments, o=os.path.join(report_dir, "stat.json")) d.tee("stat", *lib_arguments, o=os.path.join(report_dir, "stat.rpt")) + noattr_flag = [] + if config["SYNTH_WRITE_NOATTR"]: + noattr_flag.append("-noattr") + d.run_pass( "write_verilog", - "-noattr", - "-noexpr", + *noattr_flag, "-nohex", "-nodec", "-defparam", @@ -232,10 +351,11 @@ def synthesize( openlane_synth( d, config["DESIGN_NAME"], - not config["SYNTH_NO_FLAT"], + config["SYNTH_HIERARCHY_MODE"] == "flatten", report_dir, booth=config["SYNTH_MUL_BOOTH"], abc_dff=config["SYNTH_ABC_DFF"], + undriven=config.get("SYNTH_TIE_UNDEFINED") is not None, ) d.run_pass("delete", "t:$print") @@ -305,7 +425,10 @@ def run_strategy(d): *(["-dff"] if config["SYNTH_ABC_DFF"] else []), ) - d.run_pass("setundef", "-zero") + if value := config.get("SYNTH_TIE_UNDEFINED"): + flag = "-zero" if value == "low" else "-one" + d.run_pass("setundef", flag) + d.run_pass( "hilomap", "-hicell", @@ -336,9 +459,13 @@ def run_strategy(d): # sc_mcu7t5v0__and3_1_A3_Z_gf180mcu_fd_sc_mcu7t5v0__buf_1_I_Z d.run_pass("autoname") + noattr_flag = [] + if config["SYNTH_WRITE_NOATTR"]: + noattr_flag.append("-noattr") + d.run_pass( "write_verilog", - "-noattr", + *noattr_flag, "-noexpr", "-nohex", "-nodec", @@ -349,7 +476,7 @@ def run_strategy(d): run_strategy(d) - if config["SYNTH_NO_FLAT"]: + if config["SYNTH_HIERARCHY_MODE"] == "deferred_flatten": # Resynthesize, flattening d_flat = ys.Design() d_flat.add_blackbox_models(blackbox_models, includes=includes, defines=defines) diff --git a/openlane/steps/openroad.py b/openlane/steps/openroad.py index 11cd6efe3..6e687cc2a 100644 --- a/openlane/steps/openroad.py +++ b/openlane/steps/openroad.py @@ -1287,6 +1287,11 @@ class GlobalPlacement(_GlobalPlacement): "Specifies whether the placer should use routability driven placement.", default=True, ), + Variable( + "PL_ROUTABILITY_OVERFLOW_THRESHOLD", + Optional[Decimal], + "Sets overflow threshold for routability mode.", + ), ] @@ -2288,6 +2293,12 @@ class ResizerTimingPostCTS(ResizerStep): "Enables gate cloning when attempting to fix setup violations", default=True, ), + Variable( + "PL_RESIZER_FIX_HOLD_FIRST", + bool, + "Experimental: attempt to fix hold violations before setup violations, which may lead to better timing results.", + default=False, + ), ] def get_script_path(self): @@ -2361,6 +2372,12 @@ class ResizerTimingPostGRT(ResizerStep): "Gates running global routing after resizer steps. May be useful to disable for designs where global routing takes non-trivial time.", default=True, ), + Variable( + "GRT_RESIZER_FIX_HOLD_FIRST", + bool, + "Experimental: attempt to fix hold violations before setup violations, which may lead to better timing results.", + default=False, + ), ] def get_script_path(self): diff --git a/openlane/steps/pyosys.py b/openlane/steps/pyosys.py index 8f76ddc21..353fbab1f 100644 --- a/openlane/steps/pyosys.py +++ b/openlane/steps/pyosys.py @@ -412,10 +412,16 @@ class SynthesisCommon(VerilogStep): default=False, ), Variable( - "SYNTH_NO_FLAT", - bool, - "A flag that disables flattening the hierarchy during synthesis, only flattening it after synthesis, mapping and optimizations.", - default=False, + "SYNTH_HIERARCHY_MODE", + Literal["flatten", "deferred_flatten", "keep"], + "Affects how hierarchy is maintained throughout and after synthesis. 'flatten' flattens it during and after synthesis. 'deferred_flatten' flattens it after synthesis. 'keep' never flattens it.", + default="flatten", + deprecated_names=[ + ( + "SYNTH_NO_FLAT", + lambda x: "deferred_flatten" if x else "flatten", + ) + ], ), Variable( "SYNTH_SHARE_RESOURCES", @@ -453,6 +459,18 @@ class SynthesisCommon(VerilogStep): "Runs the booth pass as part of synthesis: See https://yosyshq.readthedocs.io/projects/yosys/en/latest/cmd/booth.html", default=False, ), + Variable( + "SYNTH_TIE_UNDEFINED", + Optional[Literal["high", "low"]], + "Whether to tie undefined values low or high. Explicitly provide null if you wish to simply leave them undriven.", + default="low", + ), + Variable( + "SYNTH_WRITE_NOATTR", + bool, + "If true, Verilog-2001 attributes are omitted from output netlists. Some utilities do not support attributes.", + default=True, + ), # Variable( # "SYNTH_SDC_FILE", # Optional[Path], @@ -547,6 +565,30 @@ class Synthesis(SynthesisCommon): config_vars = SynthesisCommon.config_vars + verilog_rtl_cfg_vars +@Step.factory.register() +class Resynthesis(SynthesisCommon): + """ + Like ``Synthesis``, but operates on the input netlist instead of RTL files. + Useful to process/elaborate on netlists generated by tools other than Yosys. + + Some metrics will also be extracted and updated, namely: + + * ``design__instance__count`` + * ``design__instance_unmapped__count`` + * ``design__instance__area`` + """ + + id = "Yosys.Resynthesis" + name = "Resynthesis" + + config_vars = SynthesisCommon.config_vars + + inputs = [DesignFormat.NETLIST] + + def get_command(self, state_in): + return super().get_command(state_in) + [state_in[DesignFormat.NETLIST]] + + @Step.factory.register() class VHDLSynthesis(SynthesisCommon): """ diff --git a/pyproject.toml b/pyproject.toml index 4ce86e2be..6eaf08819 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openlane" -version = "2.2.9" +version = "2.3.0" description = "An infrastructure for implementing chip design flows" authors = ["Efabless Corporation and Contributors "] readme = "Readme.md" @@ -24,7 +24,7 @@ psutil = ">=5.9.0" httpx = ">=0.22.0,<0.28" klayout = ">=0.29.0,<0.30.0" rapidfuzz = ">=3.9.0,<4" -ioplace-parser = "0.3.0" +ioplace-parser = ">=0.3.0,<0.5.0" yamlcore = "^0.0.2"