Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# Copyright 2023 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
import os
import re
import uuid
import shutil
import tempfile
import subprocess
from enum import IntEnum
from decimal import Decimal
from functools import lru_cache
from typing import (
Any,
Callable,
Dict,
FrozenSet,
Iterable,
Literal,
Mapping,
Optional,
Sequence,
Tuple,
List,
Union,
)
import libparse
from deprecated.sphinx import deprecated
from .misc import mkdirp
from .types import Path
from .metrics import aggregate_metrics
from .generic_dict import GenericImmutableDict, is_string
from ..state import DesignFormat
from ..common import Filter
from ..logging import debug, warn, err
class Toolbox(object):
"""
An assisting object shared by a Flow and all its constituent Steps.
The toolbox may create artifacts that are cached to avoid constant re-creation
between steps.
"""
def __init__(self, tmp_dir: str) -> None:
# Only create before use, otherwise users will end up with
# "openlane_run/tmp" created in their PWD because of the global toolbox
self.tmp_dir = tmp_dir
self.remove_cells_from_lib = lru_cache(16, True)(self.remove_cells_from_lib) # type: ignore
self.create_blackbox_model = lru_cache(16, True)(self.create_blackbox_model) # type: ignore
@deprecated(
version="2.0.0b1",
reason="Use 'aggregate_metrics' from 'openlane.common'",
action="once",
)
def aggregate_metrics(
self,
input: Dict[str, Any],
aggregator_by_metric: Dict[str, Tuple[Any, Callable[[Iterable], Any]]],
) -> Dict[str, Any]:
return aggregate_metrics(input, aggregator_by_metric)
def filter_views(
self,
config: Mapping[str, Any],
views_by_corner: Mapping[str, Union[Path, Iterable[Path]]],
timing_corner: Optional[str] = None,
) -> List[Path]:
"""
Given a mapping from (wildcards of) corner names to views, this function
enumerates all views matching either the default timing corner or
an explicitly-provided override.
:param config: The configuration. Used solely to extract the default
corner.
:param views_by_corner: The mapping from (wild cards) of corner names to
views.
:param corner: An explicit override for the default corner. Must be a
fully qualified IPVT corner.
:returns: The created list
"""
timing_corner = timing_corner or config["DEFAULT_CORNER"]
result: List[Path] = []
for key in Filter(views_by_corner).get_matching_wildcards(timing_corner):
value = views_by_corner[key]
if is_string(value):
result += [value] # type: ignore
else:
result += list(value) # type: ignore
return result
def get_macro_views(
self,
config: Mapping[str, Any],
view: DesignFormat,
timing_corner: Optional[str] = None,
unless_exist: Union[None, DesignFormat, Sequence[DesignFormat]] = None,
) -> List[Path]:
"""
For :class:`Config` objects (or similar Mappings) that have Macro
information, this function gets all Macro views matching a certain
:class:`DesignFormat` for either the default timing corner or an
explicitly-provided override.
:param config: The configuration.
:param view: The design format to return views of.
:param timing_corner: An explicit override for the default corner set
by the configuration.
:param corner: An explicit override for the default corner. Must be a
fully qualified IPVT corner.
:param unless_exist: If a Macro also has a view for these
``DesignFormat``\\s, do not return a result for the requested
``DesignFormat``\\.
Useful for if you want to return say, Netlists if reliable LIB files
do not exist.
:returns: A list of the Macro views matched by the process described
above.
"""
from ..config import Macro
timing_corner = timing_corner or config["DEFAULT_CORNER"]
macros = config["MACROS"]
result: List[Path] = []
if macros is None:
return result
unless_exist = unless_exist or []
if isinstance(unless_exist, DesignFormat):
unless_exist = [unless_exist]
for module, macro in macros.items():
if not isinstance(macro, Macro):
raise TypeError(
f"Misconstructed configuration: macro definition for key {module} is not of type 'Macro'."
)
views = macro.view_by_df(view)
if views is None:
continue
alt_views: List[Path] = []
for alternate_format in unless_exist:
entry = macro.view_by_df(alternate_format)
if entry is not None:
current = entry
if isinstance(current, dict):
current = self.filter_views(config, current, timing_corner)
elif not isinstance(current, list):
current = [current]
alt_views += current
if len(alt_views) != 0:
continue
if isinstance(views, dict):
views_filtered = self.filter_views(config, views, timing_corner)
result += views_filtered
elif isinstance(views, list):
result += views
elif views is not None:
result += [Path(views)]
return [element for element in result if str(element) != Path._dummy_path]
def get_macro_views_by_priority(
self,
config: Mapping[str, Any],
design_formats: Sequence[DesignFormat],
timing_corner: Optional[str] = None,
) -> List[Tuple[Path, DesignFormat]]:
result: List[Tuple[Path, DesignFormat]] = []
formats_so_far: List[DesignFormat] = []
for format in design_formats:
views = self.get_macro_views(
config,
format,
unless_exist=formats_so_far,
timing_corner=timing_corner,
)
for view in views:
result.append((view, format))
formats_so_far.append(format)
return result
def get_timing_files_categorized(
self,
config: Mapping[str, Any],
timing_corner: Optional[str] = None,
prioritize_nl: bool = False,
) -> Tuple[str, List[Path], List[Path], List[Tuple[str, Path]]]:
"""
Returns the lib files for a given configuration and timing corner.
:param config: A configuration object or a similar mapping.
:param timing_corner:
A fully qualified IPVT corner to get SCL libs for.
If not specified, the value for ``DEFAULT_CORNER`` from the SCL will
be used.
:param prioritize_nl:
Do not return lib files for macros that have gate-Level Netlists and
SPEF views.
If set to ``false``\\, only lib files are returned.
:returns: A tuple of:
* The name of the timing corner
* A list of lib files
* A list of netlists
* A list of tuples of instances and SPEFs
"""
from ..config import Macro
timing_corner = timing_corner or config["DEFAULT_CORNER"]
all_libs: List[Path] = self.filter_views(config, config["LIB"], timing_corner)
if len(all_libs) == 0:
warn(f"No SCL lib files found for {timing_corner}.")
all_netlists: List[Path] = []
all_spefs: List[Tuple[str, Path]] = []
macros = config["MACROS"]
if macros is None:
macros = {}
for module, macro in macros.items():
if not isinstance(macro, Macro):
raise TypeError(
f"Misconstructed configuration: macro definition for key {module} is not of type 'Macro'."
)
if prioritize_nl:
netlists = macro.nl
if isinstance(netlists, Path):
netlists = [netlists]
spefs = self.filter_views(
config,
macro.spef,
timing_corner,
)
if len(netlists) and not len(spefs):
warn(
f"Netlists found for macro {module}, but no parasitics extraction found at corner {timing_corner}. The netlist cannot be used for timing on this module."
)
elif len(spefs) and not len(netlists):
warn(
f"Parasitics extraction(s) found for macro {module} at corner {timing_corner}, but no netlist found. The parasitics cannot be used for timing on this module."
)
elif len(spefs) and len(netlists):
debug(f"Adding {[netlists + spefs]} to timing info…")
all_netlists += netlists
for spef in spefs:
for instance in macro.instances:
all_spefs.append((instance, spef))
continue
# NL/SPEF not prioritized or not found
libs = self.filter_views(
config,
macro.lib,
timing_corner,
)
if not len(libs):
warn(
f"No libs found for macro {module} at corner {timing_corner}. The module will be black-boxed."
)
continue
debug(f"Adding {libs} to timing info…")
all_libs += libs
return (timing_corner, all_libs, all_netlists, all_spefs)
def get_timing_files(
self,
config: Mapping[str, Any],
timing_corner: Optional[str] = None,
prioritize_nl: bool = False,
) -> Tuple[str, List[str]]:
"""
Returns the lib files for a given configuration and timing corner.
:param config: A configuration object or a similar mapping.
:param timing_corner:
A fully qualified IPVT corner to get SCL libs for.
If not specified, the value for ``DEFAULT_CORNER`` from the SCL will
be used.
:param prioritize_nl:
Do not return lib files for macros that have gate-Level Netlists and
SPEF views.
If set to ``false``\\, only lib files are returned.
:returns: A tuple of:
* The name of the timing corner
* A heterogeneous list of files composed of: Lib files are returned as-is,
Netlists are returned as-is, and SPEF files are returned in the
format ``{instance_name}@{spef_path}``\\.
It is left up to the step or tool to process this list as they see
fit.
"""
timing_corner, libs, netlists, spefs = self.get_timing_files_categorized(
config=config,
timing_corner=timing_corner,
prioritize_nl=prioritize_nl,
)
results = [str(path) for path in libs + netlists]
for instance, spef in spefs:
results.append(f"{instance}@{spef}")
return (timing_corner, results)
def render_png(
self,
config: GenericImmutableDict[str, Any],
state_in: GenericImmutableDict[str, Any],
) -> Optional[bytes]: # pragma: no cover
try:
from ..steps import KLayout, StepError
from ..config import Config, InvalidConfig
from ..state import State
# I'm too damn tired to figure out a way to forward-declare those two,
# have fun if you want to
if not isinstance(config, Config):
raise TypeError("parameter config must be of type Config")
if not isinstance(state_in, State):
raise TypeError("parameter state_in must be of type State")
with tempfile.TemporaryDirectory(prefix="openlane_klayout_tmp_") as d:
render_step = KLayout.Render(config, state_in, _config_quiet=True)
render_step.start(self, d)
return open(os.path.join(d, "out.png"), "rb").read()
except InvalidConfig:
warn("PDK is incompatible with KLayout. Unable to generate preview.")
return None
except StepError as e:
warn(f"Failed to generate preview: {e}.")
return None
def remove_cells_from_lib(
self,
input_lib_files: FrozenSet[str],
excluded_cells: FrozenSet[str],
) -> List[str]:
"""
Creates a new lib file with some cells removed.
This function is memoized, i.e., results are cached for a specific set
of inputs.
:param input_lib_files: A `frozenset` of input lib files.
:param excluded_cells: A `frozenset` of wildcards of cells to remove
from the files.
:returns: A path to the lib file with the removed cells.
"""
mkdirp(self.tmp_dir)
class State(IntEnum):
initial = 0
cell = 10
excluded_cell = 11
cell_start_rx = re.compile(r"(\s*)cell\s*\(\"?(.*?)\"?\)\s*\{")
out_paths = []
excluded_cells_filter = Filter(excluded_cells)
for file in input_lib_files:
input_lib_stream = open(file)
out_filename = f"{uuid.uuid4().hex}.lib"
out_path = os.path.join(self.tmp_dir, out_filename)
state = State.initial
brace_count = 0
output_file_handle = open(out_path, "w")
write = lambda x: print(x, file=output_file_handle, end="")
for line in input_lib_stream:
if state == State.initial:
cell_m = cell_start_rx.search(line)
if cell_m is not None:
whitespace = cell_m[1]
cell_name = cell_m[2]
if excluded_cells_filter.match(cell_name):
state = State.excluded_cell
write(f"{whitespace}/* removed {cell_name} */\n")
else:
state = State.cell
write(line)
brace_count = 1
else:
write(line)
elif state in [State.cell, State.excluded_cell]:
if "{" in line:
brace_count += 1
if "}" in line:
brace_count -= 1
if state == State.cell:
write(line)
if brace_count == 0:
state = State.initial
output_file_handle.close()
out_paths.append(out_path)
return out_paths
def create_blackbox_model(
self,
input_models: Union[frozenset, Tuple[str, ...]],
defines: FrozenSet[str],
) -> str:
mkdirp(self.tmp_dir)
out_path = os.path.join(self.tmp_dir, f"{uuid.uuid4().hex}.bb.v")
debug(f"Creating cell models for {input_models} at '{out_path}'…")
bad_yosys_line = re.compile(r"^\s+(\w+|(\\\S+?))\s*\(.*\).*;")
stack: List[Literal["specify", "primitive"]] = []
with open(out_path, "w", encoding="utf8") as out:
for model in input_models:
try:
for line in open(model, "r", encoding="utf8"):
if len(stack) == 0:
if line.strip().startswith("specify"):
stack.append("specify")
elif line.strip().startswith("primitive"):
stack.append("primitive")
elif bad_yosys_line.search(line) is None:
print(line.strip("\n"), file=out)
else:
if line.strip().startswith("endspecify"):
current = stack.pop()
if current != "specify":
raise ValueError(
f"Invalid specify block in {model}"
)
print("/* removed specify */", file=out)
elif line.strip().startswith("endprimitive"):
current = stack.pop()
if current != "primitive":
raise ValueError(
f"Invalid primitive block in {model}"
)
print("/* removed primitive */", file=out)
print("", file=out)
except ValueError as e:
err(f"Failed to pre-process input models for linting: {e}")
yosys = shutil.which("yosys") or shutil.which("yowasp-yosys")
if yosys is None:
warn(
"yosys and yowasp-yosys not found in PATH. This may trigger issues with blackboxing."
)
return out_path
commands = ""
for define in list(defines):
commands += f"verilog_defines -D{define};\n"
commands += f"read_verilog -sv -lib {out_path};\n"
output_log_path = f"{out_path}_yosys.log"
output_log = open(output_log_path, "wb")
try:
subprocess.check_call(
[
yosys,
"-p",
f"""
{commands}
blackbox;
write_verilog -noattr -noexpr -nohex -nodec -defparam -blackboxes {out_path};
""",
],
stdout=output_log,
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
output_log.close()
err(f"Failed to pre-process input models for linting with Yosys: {e}")
err(open(output_log_path, "r", encoding="utf8").read())
err("Will attempt to load models into linter as-is.")
return out_path
def get_lib_voltage(
self,
input_lib: str,
) -> Optional[Decimal]:
"""
Extract the voltage from the default operating conditions of a liberty file.
Returns ``None`` if and only if the ``default_operating_conditions`` key
does not exist and the number of operating conditions enumerated is not
exactly 1 (one).
:param input_lib: The lib file in question
:returns: The voltage in question
"""
parser = libparse.LibertyParser(open(input_lib, encoding="utf8"))
ast = parser.ast
default_operating_conditions_id = None
operating_conditions_raw = {}
for child in ast.children:
if child.id == "default_operating_conditions":
default_operating_conditions_id = child.value
if child.id == "operating_conditions":
operating_conditions_raw[child.args[0]] = child
if default_operating_conditions_id is None:
if len(operating_conditions_raw) > 1:
warn(
f"No default operating condition defined in lib file '{input_lib}', and the lib file has multiple operating conditions."
)
return None
elif len(operating_conditions_raw) < 1:
warn(f"Lib file '{input_lib}' has no operating conditions set.")
return None
default_operating_conditions_id = list(operating_conditions_raw.keys())[0]
operating_conditions = operating_conditions_raw[default_operating_conditions_id]
operating_condition_dict = {}
for child in operating_conditions.children:
operating_condition_dict[child.id] = child.value
return Decimal(operating_condition_dict["voltage"])
8c7cd1d33This file is pinned to the selected release. Download the original to open it in your design tool.