Source code for fabulous.fabric_cad.timing_model.tools.sta_tools.opensta
"""OpenSTA Tool Interface.
Provides an interface to run OpenSTA for static timing analysis on a given Verilog
netlist.
"""
import subprocess
from pathlib import Path
from loguru import logger
from fabulous.fabric_cad.timing_model.tools.specification import StaTool
[docs]
class OpenStaTool(StaTool):
"""OpenSTA is an open-source static timing analysis tool.
Initializes the OpenSTATool with the given parameters.
This class provides an interface to run OpenSTA on a given netlist,
and to retrieve the generated SDF file after analysis.
Parameters
----------
sta_executable : Path | str
The path to the OpenSTA executable.
liberty_files : list[Path] | Path | None
The Liberty timing model file(s) to use for analysis. Can be a
single Path or a list of Paths.
top_name : str | None
The name of the top-level design to analyze.
verilog_netlist : Path | None
The path to the Verilog gate-level netlist to analyze. If None, it
must be set before calling analyze().
spef_files : list[Path] | Path | None
The SPEF RC extraction file(s) to use for analysis. Can be a single
Path or a list of Paths.
debug : bool
Flag to enable debug mode, which will print additional information
during analysis. Default is False.
"""
def __init__(
self,
sta_executable: Path | str,
liberty_files: list[Path] | Path | None = None,
top_name: str | None = None,
verilog_netlist: Path | None = None,
spef_files: list[Path] | Path | None = None,
debug: bool = False,
) -> None:
self.verilog_netlist: Path | None = verilog_netlist
self.lib_files: list[Path] | Path | None = liberty_files
self.top_name: str | None = top_name
self.sta_executable: Path | str = sta_executable
self.spef_files: list[Path] | Path | None = spef_files
self.debug: bool = debug
self.sdf_path: Path | None = None
[docs]
def sta_analyze(self) -> None:
"""Generate an temporary SDF file from the Verilog gate-level netlist.
Uses OpenSTA. The SDF file is created in a temporary location
and deleted after use.
Raises
------
RuntimeError
If the SDF file cannot be generated or is empty after running OpenSTA.
"""
self._check_errors()
sta_tcl_script: str = ""
if isinstance(self.lib_files, Path):
sta_tcl_script += f"read_liberty {self.lib_files}\n"
else:
for lib in self.lib_files:
sta_tcl_script += f"read_liberty {lib}\n"
sta_tcl_script += f"read_verilog {self.verilog_netlist}\n"
sta_tcl_script += f"link_design {self.top_name}\n"
if self.spef_files is not None:
if isinstance(self.spef_files, Path):
sta_tcl_script += f"read_spef {self.spef_files}\n"
elif isinstance(self.spef_files, list):
for spef in self.spef_files:
sta_tcl_script += f"read_spef {spef}\n"
sta_tcl_script += "write_sdf {}\n".format("{sdf_path}")
sta_tcl_script += "exit\n"
path: Path = Path.home() / ".fabulous" / "tmp" / f"sta_{self.top_name}_tmp.sdf"
path.parent.mkdir(parents=True, exist_ok=True)
logger.debug(f"Generating SDF file at temporary path: {path}")
self._call_external(
self.sta_executable,
stdin_data=sta_tcl_script.format(sdf_path=path),
debug=self.debug,
)
content: str = path.read_text()
if not content:
path.unlink()
raise RuntimeError(
"Failed to generate SDF file using OpenSTA. No content in SDF file."
)
self.sdf_path = path
@property
[docs]
def sta_sdf_file(self) -> Path:
"""Return the path to the generated SDF file after analysis.
Returns
-------
Path
The path to the generated SDF file.
Raises
------
RuntimeError
If the SDF file has not been generated yet.
"""
if self.sdf_path is None:
raise RuntimeError(
"SDF file has not been generated yet. Call analyze() first."
)
return self.sdf_path
@property
[docs]
def sta_netlist_file(self) -> Path:
"""Return the path to the netlist file used for STA analysis.
Returns
-------
Path
The path to the netlist file used for STA analysis.
"""
return self.verilog_netlist
@sta_netlist_file.setter
def sta_netlist_file(self, netl: Path) -> None:
"""Set the path to the netlist file used for STA analysis.
Parameters
----------
netl : Path
The path to the netlist file.
"""
self.verilog_netlist = netl
@property
[docs]
def sta_design_name(self) -> str:
"""Return the name of the design being analyzed.
Returns
-------
str
The name of the design being analyzed.
"""
return self.top_name
@sta_design_name.setter
def sta_design_name(self, name: str) -> None:
"""Set the name of the design being analyzed.
Parameters
----------
name : str
The name of the design being analyzed.
"""
self.top_name = name
@property
[docs]
def sta_liberty_files(self) -> list[Path] | Path | None:
"""Return the list of Liberty files used for STA analysis.
Returns
-------
list[Path] | Path | None
The list of Liberty files used for STA analysis.
"""
return self.lib_files
@sta_liberty_files.setter
def sta_liberty_files(self, files: list[Path] | Path | None) -> None:
"""Set the list of Liberty files used for STA analysis.
Parameters
----------
files : list[Path] | Path | None
The list of Liberty files used for STA analysis.
"""
self.lib_files = files
@property
[docs]
def sta_rc_files(self) -> list[Path] | Path | None:
"""Return the list of RC files used for STA analysis.
Returns
-------
list[Path] | Path | None
The list of RC files used for STA analysis.
"""
return self.spef_files
@sta_rc_files.setter
def sta_rc_files(self, files: list[Path] | Path | None) -> None:
"""Set the list of RC files used for STA analysis.
Parameters
----------
files : list[Path] | Path | None
The list of RC files used for STA analysis.
"""
self.spef_files = files
[docs]
def sta_clean_up(self) -> None:
"""Clean up any temporary files generated during STA analysis.
This includes the SDF file.
"""
if self.sdf_path is not None and self.sdf_path.exists():
logger.debug(f"Cleaning up temporary SDF file at: {self.sdf_path}")
self.sdf_path.unlink()
self.sdf_path = None
def _call_external(
self,
executable: str,
args: list[str] | None = None,
stdin_data: str = "",
debug: bool = False,
) -> subprocess.CompletedProcess:
"""Call an external executable with given arguments and stdin data.
Captures the output and checks for errors.
Parameters
----------
executable : str
The path to the executable to run.
args : list[str] | None
List of arguments to pass to the executable.
stdin_data : str
Data to send to the executable's stdin.
debug : bool
Flag to enable debug mode, which will print additional information.
Returns
-------
subprocess.CompletedProcess
The result of the subprocess call.
Raises
------
RuntimeError
If the external command fails.
"""
if args is None:
args = []
if debug:
logger.debug("Debug mode enabled for external command.")
logger.debug(f"Calling external command: {executable} {' '.join(args)}")
logger.debug(f"With stdin data:\n{stdin_data}")
result = subprocess.run(
[executable, *args],
input=stdin_data,
text=True,
)
else:
result = subprocess.run(
[executable, *args],
input=stdin_data,
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(
f"Command '{' '.join([executable, *args])}' "
f"failed with error: {result.stderr}"
)
return result
def _check_errors(self) -> None:
"""Check for errors in the provided configuration parameters.
Raises
------
TypeError
If any parameter is of incorrect type.
FileNotFoundError
If any specified file does not exist.
ValueError
If any specified file is empty.
"""
if not isinstance(self.verilog_netlist, Path):
raise TypeError("verilog_netlist must be a pathlib.Path object.")
if not self.verilog_netlist.exists():
raise FileNotFoundError(
f"Verilog netlist file not found: {self.verilog_netlist}"
)
if self.verilog_netlist.stat().st_size == 0:
raise ValueError(f"Verilog netlist file is empty: {self.verilog_netlist}")
if not isinstance(self.lib_files, list | Path):
raise TypeError(
"liberty_files must be a list of pathlib.Path objects or a "
"single pathlib.Path object."
)
if isinstance(self.lib_files, list):
for lib in self.lib_files:
if not isinstance(lib, Path):
raise TypeError(
"Each item in liberty_files list must be a pathlib.Path object."
)
if not lib.exists():
raise FileNotFoundError(f"Liberty file not found: {lib}")
if lib.stat().st_size == 0:
raise ValueError(f"Liberty file is empty: {lib}")
else:
if not self.lib_files.exists():
raise FileNotFoundError(f"Liberty file not found: {self.lib_files}")
if self.lib_files.stat().st_size == 0:
raise ValueError(f"Liberty file is empty: {self.lib_files}")
if not isinstance(self.top_name, str):
raise TypeError("top_name must be a string.")
if not isinstance(self.sta_executable, Path | str):
raise TypeError("sta_executable must be a string or a pathlib.Path object.")
if self.spef_files is not None and not isinstance(self.spef_files, list | Path):
raise TypeError(
"spef_files must be a list of pathlib.Path objects or a single "
"pathlib.Path object or None."
)
if isinstance(self.spef_files, list):
for spef in self.spef_files:
if not isinstance(spef, Path):
raise TypeError(
"Each item in spef_files list must be a pathlib.Path object."
)
if not spef.exists():
raise FileNotFoundError(f"SPEF file not found: {spef}")
if spef.stat().st_size == 0:
raise ValueError(f"SPEF file is empty: {spef}")
elif isinstance(self.spef_files, Path):
if not self.spef_files.exists():
raise FileNotFoundError(f"SPEF file not found: {self.spef_files}")
if self.spef_files.stat().st_size == 0:
raise ValueError(f"SPEF file is empty: {self.spef_files}")
if not isinstance(self.debug, bool):
raise TypeError("debug must be a boolean.")