From 788ebb906979e3907e4f4b99b792ef70588d33ee Mon Sep 17 00:00:00 2001 From: Harrison Liew Date: Sat, 30 Sep 2023 13:14:33 -0700 Subject: [PATCH] units overhaul - get time & cap units from the tech library --- doc/Technology/Tech-json.rst | 1 - hammer/config/config_src.py | 3 +- hammer/par/innovus/__init__.py | 7 +-- hammer/tech/__init__.py | 42 +++++++++++--- hammer/technology/asap7/asap7.tech.json | 1 - .../technology/nangate45/nangate45.tech.json | 1 - .../sky130-tech-gen-files/beginning.json | 3 +- .../sky130-tech-gen-files/beginning_nda.json | 1 - hammer/technology/sky130/sky130.tech.json | 3 +- hammer/utils/__init__.py | 1 + hammer/utils/lib_utils.py | 57 +++++++++++++++++++ hammer/vlsi/driver.py | 1 + hammer/vlsi/hammer_tool.py | 11 +++- hammer/vlsi/hammer_vlsi_impl.py | 13 +++-- hammer/vlsi/submit_command.py | 6 +- tests/test_sdc.py | 2 + tests/utils/tool.py | 1 - 17 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 hammer/utils/lib_utils.py diff --git a/doc/Technology/Tech-json.rst b/doc/Technology/Tech-json.rst index ae7b69291..c0b72523c 100644 --- a/doc/Technology/Tech-json.rst +++ b/doc/Technology/Tech-json.rst @@ -16,7 +16,6 @@ Below is an example of the installs and tarballs from the ASAP7 plugin. "name": "ASAP7 Library", "grid_unit": "0.001", - "time_unit": "1 ps", "installs": [ { "id": "$PDK", diff --git a/hammer/config/config_src.py b/hammer/config/config_src.py index 00af47c55..f5340e950 100644 --- a/hammer/config/config_src.py +++ b/hammer/config/config_src.py @@ -941,7 +941,8 @@ def check_setting(self, key: str, cfg: Optional[dict] = None) -> bool: if cfg is None: cfg = self.get_config() if key not in self.get_config_types(): - self.logger.warning(f"Key {key} is not associated with a type") + #TODO: compile this at the beginning instead of emitting every instance + #self.logger.warning(f"Key {key} is not associated with a type") return True try: exp_value_type = parse_setting_type(self.get_config_types()[key]) diff --git a/hammer/par/innovus/__init__.py b/hammer/par/innovus/__init__.py index 24c59ccad..ab5fe64be 100644 --- a/hammer/par/innovus/__init__.py +++ b/hammer/par/innovus/__init__.py @@ -260,8 +260,6 @@ def init_design(self) -> bool: verbose_append("set_db timing_analysis_cppr both") # Use OCV mode for timing analysis by default verbose_append("set_db timing_analysis_type ocv") - # Match SDC time units to timing libraries - verbose_append("set_library_unit -time 1{}".format(self.get_time_unit().value_prefix + self.get_time_unit().unit)) # Read LEF layouts. lef_files = self.technology.read_libs([ @@ -559,8 +557,9 @@ def add_fillers(self) -> bool: else: decap_cells = decaps[0].name decap_caps = [] # type: List[float] + cap_unit = self.get_cap_unit().value_prefix + self.get_cap_unit().unit if decaps[0].size is not None: - decap_caps = list(map(lambda x: CapacitanceValue(x).value_in_units("fF"), decaps[0].size)) + decap_caps = list(map(lambda x: CapacitanceValue(x).value_in_units(cap_unit), decaps[0].size)) if len(decap_cells) != len(decap_caps): self.logger.error("Each decap cell in the name list must have a corresponding decapacitance value in the size list.") decap_consts = list(filter(lambda x: x.target=="capacitance", self.get_decap_constraints())) @@ -580,7 +579,7 @@ def add_fillers(self) -> bool: assert isinstance(const.height, Decimal) area_str = " ".join(("-area", str(const.x), str(const.y), str(const.x+const.width), str(const.y+const.height))) self.verbose_append("add_decaps -effort high -total_cap {CAP} {AREA}".format( - CAP=const.capacitance.value_in_units("fF"), AREA=area_str)) + CAP=const.capacitance.value_in_units(cap_unit), AREA=area_str)) if len(stdfillers) == 0: self.logger.warning( diff --git a/hammer/tech/__init__.py b/hammer/tech/__init__.py index 90f981805..610c9e9a8 100644 --- a/hammer/tech/__init__.py +++ b/hammer/tech/__init__.py @@ -21,9 +21,10 @@ from hammer.config import load_yaml, HammerJSONEncoder from hammer.logging import HammerVLSILoggingContext -from hammer.utils import (LEFUtils, add_lists, deeplist, get_or_else, +from hammer.utils import (LEFUtils, LIBUtils, add_lists, deeplist, get_or_else, in_place_unique, optional_map, reduce_list_str, reduce_named, coerce_to_grid) +from hammer.vlsi.units import TimeValue, CapacitanceValue if TYPE_CHECKING: from hammer.vlsi.hooks import HammerToolHookAction @@ -213,7 +214,6 @@ def from_setting(grid_unit: Decimal, d: Dict[str, Any]) -> "Site": class TechJSON(BaseModel): name: str grid_unit: Optional[str] - time_unit: Optional[str] shrink_factor: Optional[str] installs: Optional[List[PathPrefix]] libraries: Optional[List[Library]] @@ -350,6 +350,10 @@ def __init__(self): # Configuration self.config: TechJSON = None + # Units + self.time_unit: Optional[TimeValue] = None + self.cap_unit: Optional[CapacitanceValue] = None + @classmethod def load_from_module(cls, tech_module: str) -> Optional["HammerTechnology"]: """Load a technology from a given module. @@ -372,9 +376,31 @@ def load_from_module(cls, tech_module: str) -> Optional["HammerTechnology"]: elif tech_yaml.is_file(): tech.config = TechJSON.parse_raw(json.dumps(load_yaml(tech_yaml.read_text()))) return tech - else: + else: #TODO - from Pydantic model instance return None + def get_lib_units(self) -> None: + """ + Get time and capacitance units from the first LIB file + Must be called right after the tech module is loaded. + """ + libs = self.read_libs( + [filters.get_timing_lib_with_preference("NLDM")], + HammerTechnologyUtils.to_plain_item) + if len(libs) > 0: + tu = LIBUtils.get_time_unit(libs[0]) + cu = LIBUtils.get_cap_unit(libs[0]) + if tu is None: + self.logger.error("Error in parsing first NLDM Liberty file for time units.") + else: + self.time_unit = TimeValue(tu) + if cu is None: + self.logger.error("Error in parsing first NLDM Liberty file for capacitance units.") + else: + self.cap_unit = CapacitanceValue(cu) + else: + self.logger.error("No NLDM libs defined. Time/cap units will be defined by the tool or another technology.") + def set_database(self, database: hammer_config.HammerDatabase) -> None: """Set the settings database for use by the tool.""" self._database = database # type: hammer_config.HammerDatabase @@ -1262,19 +1288,19 @@ def get_timing_lib_with_preference(self, lib_pref: str = "NLDM") -> LibraryFilte Select ASCII .lib timing libraries. Prefers NLDM, then ECSM, then CCS if multiple are present for a single given .lib. """ - lib_pref = lib_pref.upper() + lib_pref = lib_pref.upper() def paths_func(lib: Library) -> List[str]: pref_list = ["NLDM", "ECSM", "CCS"] index = None - + try: index = pref_list.index(lib_pref) - except: + except: raise ValueError("Library preference must be one of NLDM, ECSM, or CCS.") pref_list.insert(0, pref_list.pop(index)) - for elem in pref_list: + for elem in pref_list: if elem == "NLDM": if lib.nldm_liberty_file is not None: return [lib.nldm_liberty_file] @@ -1286,7 +1312,7 @@ def paths_func(lib: Library) -> List[str]: return [lib.ccs_liberty_file] else: pass - + return [] return LibraryFilter( diff --git a/hammer/technology/asap7/asap7.tech.json b/hammer/technology/asap7/asap7.tech.json index 15b27bcfd..650df5d58 100644 --- a/hammer/technology/asap7/asap7.tech.json +++ b/hammer/technology/asap7/asap7.tech.json @@ -1,7 +1,6 @@ { "name": "ASAP7 Library", "grid_unit": "0.001", - "time_unit": "1 ps", "installs": [ { "id": "$PDK", diff --git a/hammer/technology/nangate45/nangate45.tech.json b/hammer/technology/nangate45/nangate45.tech.json index fbfab002f..43b71e414 100644 --- a/hammer/technology/nangate45/nangate45.tech.json +++ b/hammer/technology/nangate45/nangate45.tech.json @@ -1,7 +1,6 @@ { "name": "Nangate45 Library", "grid_unit": "0.005", - "time_unit": "1 ps", "installs": [{ "id": "nangate45", "path": "technology.nangate45.install_dir" diff --git a/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning.json b/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning.json index bbf6d0d6b..7fbc7cd3c 100644 --- a/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning.json +++ b/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning.json @@ -1,7 +1,6 @@ { "name": "Skywater 130nm Library", "grid_unit": "0.001", - "time_unit": "1 ns", "installs": [ { "id": "$SKY130A", @@ -19,4 +18,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning_nda.json b/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning_nda.json index 14dcab7be..2d248220c 100644 --- a/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning_nda.json +++ b/hammer/technology/sky130/extra/sky130-tech-gen-files/beginning_nda.json @@ -1,7 +1,6 @@ { "name": "Skywater 130nm Library", "grid_unit": "0.001", - "time_unit": "1 ns", "installs": [ { "id": "$SKY130_NDA", diff --git a/hammer/technology/sky130/sky130.tech.json b/hammer/technology/sky130/sky130.tech.json index 3841803b7..bdd9fa099 100644 --- a/hammer/technology/sky130/sky130.tech.json +++ b/hammer/technology/sky130/sky130.tech.json @@ -1,7 +1,6 @@ { "name": "Skywater 130nm Library", "grid_unit": "0.001", - "time_unit": "1 ns", "installs": [ { "id": "$SKY130_NDA", @@ -2023,4 +2022,4 @@ "y": 5.44 } ] -} \ No newline at end of file +} diff --git a/hammer/utils/__init__.py b/hammer/utils/__init__.py index 57c67260e..d9727a04d 100644 --- a/hammer/utils/__init__.py +++ b/hammer/utils/__init__.py @@ -16,6 +16,7 @@ from .verilog_utils import * from .lef_utils import * +from .lib_utils import * def deepdict(x: dict) -> dict: diff --git a/hammer/utils/lib_utils.py b/hammer/utils/lib_utils.py new file mode 100644 index 000000000..5a595d285 --- /dev/null +++ b/hammer/utils/lib_utils.py @@ -0,0 +1,57 @@ +# lib_utils.py +# Misc Liberty utilities +# +# See LICENSE for licence details. + +import re +import os +import gzip +from decimal import Decimal +from typing import List, Optional, Tuple +__all__ = ['LIBUtils'] + + +class LIBUtils: + @staticmethod + def get_time_unit(source: str) -> Optional[str]: + """ + Get the time unit from the given LIB source file. + """ + lines = LIBUtils.get_headers(source) + try: + match = next(line for line in lines if "time_unit" in line) + # attibute syntax: time_unit : ; + unit = re.split(" : | ; ", match)[1] + return unit + except StopIteration: # should not get here + return None + + @staticmethod + def get_cap_unit(source: str) -> Optional[str]: + """ + Get the load capacitance unit from the given LIB source file. + """ + lines = LIBUtils.get_headers(source) + try: + match = next(line for line in lines if "capacitive_load_unit" in line) + # attibute syntax: capacitive_load_unit(,); + # Also need to capitalize last f to F + split = re.split("\(|,|\)", match) + return split[1] + split[2].strip()[:-1] + split[2].strip()[-1].upper() + except StopIteration: # should not get here + return None + + @staticmethod + def get_headers(source: str) -> List[str]: + """ + Get the header lines with the major info + """ + nbytes = 10000 + if source.split('.')[-1] == "gz": + z = gzip.GzipFile(source) + lines = z.peek(nbytes).splitlines() + else: + # TODO: handle other compressed file types? Tools only seem to support gzip. + fd = os.open(source, os.O_RDONLY) + lines = os.pread(fd, nbytes, 0).splitlines() + return list(map(lambda l: l.decode('ascii', errors='ignore'), lines)) diff --git a/hammer/vlsi/driver.py b/hammer/vlsi/driver.py index 0273aa122..9051826d0 100644 --- a/hammer/vlsi/driver.py +++ b/hammer/vlsi/driver.py @@ -165,6 +165,7 @@ def load_technology(self, cache_dir: str = "") -> None: tech.set_database(self.database) tech.cache_dir = cache_dir tech.extract_technology_files() + tech.get_lib_units() self.tech = tech diff --git a/hammer/vlsi/hammer_tool.py b/hammer/vlsi/hammer_tool.py index 8274a9b66..1dfa15df8 100644 --- a/hammer/vlsi/hammer_tool.py +++ b/hammer/vlsi/hammer_tool.py @@ -25,7 +25,7 @@ from .hooks import (HammerStepFunction, HammerToolHookAction, HammerToolStep, HookLocation, HammerStartStopStep) from .submit_command import HammerSubmitCommand -from .units import TemperatureValue, TimeValue, VoltageValue +from .units import TemperatureValue, TimeValue, VoltageValue, CapacitanceValue __all__ = ['HammerTool'] @@ -1076,8 +1076,15 @@ def get_time_unit(self) -> TimeValue: """ Return the library time value. """ - return TimeValue(get_or_else(self.technology.config.time_unit, "1 ns")) + #TODO: support mixed technologies + return get_or_else(self.technology.time_unit, TimeValue("1 ns")) + def get_cap_unit(self) -> CapacitanceValue: + """ + Return the library capacitance value. + """ + #TODO: support mixed technologies + return get_or_else(self.technology.cap_unit, CapacitanceValue("1 pF")) def get_all_supplies(self, key: str) -> List[Supply]: supplies = self.get_setting(key) diff --git a/hammer/vlsi/hammer_vlsi_impl.py b/hammer/vlsi/hammer_vlsi_impl.py index 3bb7070aa..a41df49b5 100644 --- a/hammer/vlsi/hammer_vlsi_impl.py +++ b/hammer/vlsi/hammer_vlsi_impl.py @@ -2138,14 +2138,17 @@ def sdc_clock_constraints(self) -> str: clocks = self.get_clock_ports() time_unit = self.get_time_unit().value_prefix + self.get_time_unit().unit - output.append(f"set_units -time {time_unit}") for clock in clocks: - # TODO: FIXME This assumes that library units are always in ns!!! + # hports causes some tools to crash if get_or_else(clock.generated, False): + if any("get_db hports" in p for p in [get_or_else(clock.path, ""), get_or_else(clock.source_path, "")]): + self.logger.error("get_db hports will cause some tools to crash. Consider querying hpins instead.") output.append("create_generated_clock -name {n} -source {m_path} -divide_by {div} {path}". format(n=clock.name, m_path=clock.source_path, div=clock.divisor, path=clock.path)) elif clock.path is not None: + if "get_db hports" in clock.path: + self.logger.error("get_db hports will cause some tools to crash. Consider querying hpins instead.") output.append("create_clock {0} -name {1} -period {2}".format(clock.path, clock.name, clock.period.value_in_units(time_unit))) else: output.append("create_clock {0} -name {0} -period {1}".format(clock.name, clock.period.value_in_units(time_unit))) @@ -2172,9 +2175,9 @@ def sdc_pin_constraints(self) -> str: """Generate a fragment for I/O pin constraints.""" output = [] # type: List[str] - output.append("set_units -capacitance fF") + cap_unit = self.get_cap_unit().value_prefix + self.get_cap_unit().unit - default_output_load = CapacitanceValue(self.get_setting("vlsi.inputs.default_output_load")).value_in_units("fF", round_zeroes = True) + default_output_load = CapacitanceValue(self.get_setting("vlsi.inputs.default_output_load")).value_in_units(cap_unit) # Specify default load. output.append("set_load {load} [all_outputs]".format( @@ -2184,7 +2187,7 @@ def sdc_pin_constraints(self) -> str: # Also specify loads for specific pins. for load in self.get_output_load_constraints(): output.append("set_load {load} [get_port {name}]".format( - load=load.load.value_in_units("fF", round_zeroes = True), + load=load.load.value_in_units(cap_unit), name=load.name )) diff --git a/hammer/vlsi/submit_command.py b/hammer/vlsi/submit_command.py index 03bc892e3..ae7b4e65f 100644 --- a/hammer/vlsi/submit_command.py +++ b/hammer/vlsi/submit_command.py @@ -145,7 +145,7 @@ def submit(self, args: List[str], env: Dict[str, str], so = proc.stdout assert so is not None while True: - line = so.readline().decode("utf-8") + line = so.readline().decode("utf-8", errors="ignore") if line != '': subprocess_logger.debug(line.rstrip()) output_buf += line @@ -252,7 +252,7 @@ def submit(self, args: List[str], env: Dict[str, str], so = proc.stdout assert so is not None while True: - line = so.readline().decode("utf-8") + line = so.readline().decode("utf-8", errors="ignore") if line != '': subprocess_logger.debug(line.rstrip()) output_buf += line @@ -351,7 +351,7 @@ def submit(self, args: List[str], env: Dict[str, str], so = proc.stdout assert so is not None while True: - line = so.readline().decode("utf-8") + line = so.readline().decode("utf-8", errors="ignore") if line != '': subprocess_logger.debug(line.rstrip()) output_buf += line diff --git a/tests/test_sdc.py b/tests/test_sdc.py index 2de3ab647..9e5c5eca1 100644 --- a/tests/test_sdc.py +++ b/tests/test_sdc.py @@ -1,6 +1,7 @@ from typing import Optional from hammer import vlsi as hammer_vlsi, config as hammer_config +from hammer import technology as hammer_techs from utils.tool import DummyTool @@ -25,6 +26,7 @@ def test_custom_sdc_constraints(self): } tool = SDCDummyTool() + tool.technology = hammer_techs.nop.NopTechnology() database = hammer_config.HammerDatabase() hammer_vlsi.HammerVLSISettings.load_builtins_and_core(database) database.update_project([inputs]) diff --git a/tests/utils/tool.py b/tests/utils/tool.py index 1a4985ba4..1e0bd792b 100644 --- a/tests/utils/tool.py +++ b/tests/utils/tool.py @@ -86,7 +86,6 @@ def write_tech_json( tech_json = { "name": tech_name, "grid_unit": "0.001", - "time_unit": "1 ns", "installs": [], "libraries": [ {"milkyway_techfile": "soy"},