diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64630ac0..44499f8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: # run: uv run pyright chipcompiler - name: Pytest - run: uv run pytest test/ --ignore=test/test_harden.py --ignore=test/test_rcx.py --cov=chipcompiler --cov-report= + run: uv run pytest test/ --ignore=test/test_harden.py --ignore=test/test_rcx.py --ignore=test/examples/test_soc.py --cov=chipcompiler --cov-report= - name: Publish coverage summary if: always() diff --git a/chipcompiler/data/pdk.py b/chipcompiler/data/pdk.py index 2213e2b0..839002ad 100644 --- a/chipcompiler/data/pdk.py +++ b/chipcompiler/data/pdk.py @@ -90,47 +90,32 @@ def PDK_ICS55(pdk_root: str = "") -> PDK: "{}/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_ss_rcworst_1p08_125_nldm.lib".format(stdcell_dir), "{}/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_ss_rcworst_1p08_125_nldm.lib".format(stdcell_dir) ] - mapping_file = "{}/corners/ICsprout_55LLULP_1P6M_5lc_V1p1_cell.map".format(resolved_root) + mapping_file = "" corners = [ { "name" : "TYPICAL", "temperature" : [25], - "ecc_tf" : "{}/corners/TYP.json".format(resolved_root), - "itf_file" : "{}/corners/TYP.itf".format(resolved_root), - "captab_file" : "{}/corners/TYP.captab".format(resolved_root), - "spef_file" : "{}/corners/TYP.spef".format(resolved_root) + "spef_file" : "./TYP.spef" }, { "name" : "RCbest", "temperature" : [-40, 125], - "ecc_tf" : "{}/corners/RCbest.json".format(resolved_root), - "itf_file" : "{}/corners/RCbest.itf".format(resolved_root), - "captab_file" : "{}/corners/RCbest.captab".format(resolved_root), - "spef_file" : "{}/corners/RCbest.spef".format(resolved_root) + "spef_file" : "./RCbest.spef" }, { "name" : "RCworst", "temperature" : [-40, 125], - "ecc_tf" : "{}/corners/RCworst.json".format(resolved_root), - "itf_file" : "{}/corners/RCworst.itf".format(resolved_root), - "captab_file" : "{}/corners/RCworst.captab".format(resolved_root), - "spef_file" : "{}/corners/RCworst.spef".format(resolved_root) + "spef_file" : "./RCworst.spef" }, { "name" : "Cbest", "temperature" : [-40, 125], - "ecc_tf" : "{}/corners/Cbest.json".format(resolved_root), - "itf_file" : "{}/corners/Cbest.itf".format(resolved_root), - "captab_file" : "{}/corners/Cbest.captab".format(resolved_root), - "spef_file" : "{}/corners/Cbest.spef".format(resolved_root) + "spef_file" : "./Cbest.spef" }, { "name" : "Cworst", "temperature" : [-40, 125], - "ecc_tf" : "{}/corners/Cworst.json".format(resolved_root), - "itf_file" : "{}/corners/Cworst.itf".format(resolved_root), - "captab_file" : "{}/corners/Cworst.captab".format(resolved_root), - "spef_file" : "{}/corners/Cworst.spef".format(resolved_root) + "spef_file" : "./Cworst.spef" } ] diff --git a/chipcompiler/data/workspace.py b/chipcompiler/data/workspace.py index 76673652..36cb0078 100644 --- a/chipcompiler/data/workspace.py +++ b/chipcompiler/data/workspace.py @@ -257,12 +257,24 @@ def init_workspace_config(workspace: Workspace) -> None: router["RT"]["-top_routing_layer"] = workspace.parameters.data.get("Top layer", "") json_write(workspace.config[f"{StepEnum.ROUTING.value}"], router) - rcx = json_read(workspace.config[f"{StepEnum.RCX.value}"]) - rcx["pdk"] = "ics55" if workspace.pdk.name == "ics55" else "" - rcx["mapping_file"] = workspace.pdk.mapping_file - corners = deepcopy(workspace.pdk.corners) - rcx["corners"] = corners - json_write(workspace.config[f"{StepEnum.RCX.value}"], rcx) + # rcx = json_read(workspace.config[f"{StepEnum.RCX.value}"]) + # rcx["pdk"] = "ics55" if workspace.pdk.name == "ics55" else "" + # rcx["mapping_file"] = workspace.pdk.mapping_file + # corners = deepcopy(workspace.pdk.corners) + # rcx["corners"] = corners + # json_write(workspace.config[f"{StepEnum.RCX.value}"], rcx) + + sta = json_read(workspace.config[f"{StepEnum.STA.value}"]) + pdk_root = workspace.pdk.root.rstrip(os.sep) + for liberty in sta.get("liberty", []): + liberty["path"] = [ + path + if path == pdk_root or path.startswith(f"{pdk_root}{os.sep}") + else os.path.join(workspace.pdk.root, path.lstrip(os.sep)) + for path in liberty.get("path", []) + ] + + json_write(workspace.config[f"{StepEnum.STA.value}"], sta) dreamplace = json_read(workspace.config["dreamplace"]) dreamplace["lef_input"] = [workspace.pdk.tech, *workspace.pdk.lefs] @@ -613,6 +625,15 @@ def load_workspace(directory : str) -> Workspace: if len(spef_path) > 0: pdk.spef = spef_path[0] + # update lef and lib paths based on config + from chipcompiler.utility import json_read + db_json = json_read(workspace.config.get("db", "")) + if db_json.get("INPUT", {}).get("tech_lef_path", "") != "": + pdk.tech = db_json.get("INPUT", {}).get("tech_lef_path", "") + if db_json.get("INPUT", {}).get("lef_paths", []) != []: + pdk.lefs = db_json.get("INPUT", {}).get("lef_paths", []) + if db_json.get("INPUT", {}).get("lib_path", []) != []: + pdk.libs = db_json.get("INPUT", {}).get("lib_path", []) workspace.pdk = pdk #update config diff --git a/chipcompiler/engine/flow.py b/chipcompiler/engine/flow.py index 6e61cdf5..b0a40c9f 100644 --- a/chipcompiler/engine/flow.py +++ b/chipcompiler/engine/flow.py @@ -5,16 +5,35 @@ import time import logging import traceback -from multiprocessing import Process -from threading import Thread +from threading import Event, Thread from chipcompiler.data import Workspace, WorkspaceStep, StateEnum, StepEnum, log_flow from chipcompiler.engine import EngineDB -from chipcompiler.utility import track_process_memory from chipcompiler.utility.log import redirect_stdio_to_file logger = logging.getLogger(__name__) +def get_process_rss_mb(pid : int) -> float: + peak_memory = 0 + try: + with open(f"/proc/{pid}/status", 'r') as f: + for line in f: + if line.startswith("VmRSS:"): + rss_kb = int(line.split()[1]) + peak_memory = rss_kb / 1024 + break + except Exception: + pass + return peak_memory + +def track_current_process_memory(pid : int, + stop_event : Event, + peak_memory : list[float]): + while not stop_event.is_set(): + peak_memory[0] = max(peak_memory[0], get_process_rss_mb(pid)) + stop_event.wait(0.1) + peak_memory[0] = max(peak_memory[0], get_process_rss_mb(pid)) + class EngineFlow: def __init__(self, workspace : Workspace, engine_db : EngineDB = None): self.workspace = workspace @@ -321,7 +340,15 @@ def run_step(self, step_tag = f"{workspace_step.name}({workspace_step.tool})" self.workspace.logger.info(f"[STEP] {step_tag} pid={os.getpid()} started") - + + pid = os.getpid() + start_memory_mb = get_process_rss_mb(pid) + peak_memory = [start_memory_mb] + stop_memory_monitor = Event() + memory_monitor = Thread(target=track_current_process_memory, + args=(pid, stop_memory_monitor, peak_memory), + daemon=True) + memory_monitor.start() try: from chipcompiler.tools import run_step as run_tool_step result = run_tool_step(workspace=self.workspace, step=workspace_step, ecc_module=self.engine_db.engine) @@ -329,9 +356,13 @@ def run_step(self, except Exception: self.workspace.logger.error(f"[STEP] {step_tag} failed with exception") traceback.print_exc() + finally: + stop_memory_monitor.set() + memory_monitor.join() # compute metrics - peak_memory_mb = 0 + peak_memory_mb = peak_memory[0] - start_memory_mb + peak_memory_mb = 0 if peak_memory_mb < 0 else round(peak_memory_mb, 3) elapsed = time.time() - start_time runtime = f"{int(elapsed // 3600)}:{int((elapsed % 3600) // 60)}:{int(elapsed % 60)}" diff --git a/chipcompiler/thirdparty/ecc-dreamplace b/chipcompiler/thirdparty/ecc-dreamplace index 23f48c06..6c638fa0 160000 --- a/chipcompiler/thirdparty/ecc-dreamplace +++ b/chipcompiler/thirdparty/ecc-dreamplace @@ -1 +1 @@ -Subproject commit 23f48c063f8dc65c9b0b2e2627cee689ddbb2dc2 +Subproject commit 6c638fa0c369dcb53b6a851df5a65abe44c7a061 diff --git a/chipcompiler/thirdparty/ecc-tools b/chipcompiler/thirdparty/ecc-tools index e6050a57..30b5161b 160000 --- a/chipcompiler/thirdparty/ecc-tools +++ b/chipcompiler/thirdparty/ecc-tools @@ -1 +1 @@ -Subproject commit e6050a57e48f7c4567552d6ba2f86958d63d8d51 +Subproject commit 30b5161bdb71eb43f26c2664d1e9785c241a1c59 diff --git a/chipcompiler/tools/ecc/configs/rcx.json b/chipcompiler/tools/ecc/configs/rcx.json index 930b972a..65092994 100644 --- a/chipcompiler/tools/ecc/configs/rcx.json +++ b/chipcompiler/tools/ecc/configs/rcx.json @@ -1,62 +1,4 @@ { - "pdk": "", "thread_num": 64, - "output": "/RCX_ecc/output", - "mapping_file": "/corners/ICsprout_55LLULP_1P6M_5lc_V1p1_cell.map", - "corners": [ - { - "name": "TYPICAL", - "temperature": [25], - "ecc_tf": "/corners/TYP.json", - "itf_file": "/corners/TYP.itf", - "captab_file": "/corners/TYP.captab", - "spef_file": [ - { "25" : "/RCX_ecc/output/gcd_TYPICAL_25C.spef" } - ] - }, - { - "name": "RCbest", - "temperature": [-40, 125], - "ecc_tf": "/corners/RCbest.json", - "itf_file": "/corners/RCbest.itf", - "captab_file": "/corners/RCbest.captab", - "spef_file": [ - { "-40" : "/RCX_ecc/output/gcd_RCbest_m40C.spef" }, - { "125" : "/RCX_ecc/output/gcd_RCbest_125C.spef" } - ] - }, - { - "name": "RCworst", - "temperature": [-40, 125], - "ecc_tf": "/corners/RCworst.json", - "itf_file": "/corners/RCworst.itf", - "captab_file": "/corners/RCworst.captab", - "spef_file": [ - { "-40" : "/RCX_ecc/output/gcd_RCworst_m40C.spef" }, - { "125" : "/RCX_ecc/output/gcd_RCworst_125C.spef" } - ] - }, - { - "name": "Cbest", - "temperature": [-40, 125], - "ecc_tf": "/corners/Cbest.json", - "itf_file": "/corners/Cbest.itf", - "captab_file": "/corners/Cbest.captab", - "spef_file": [ - { "-40" : "/RCX_ecc/output/gcd_Cbest_m40C.spef" }, - { "125" : "/RCX_ecc/output/gcd_Cbest_125C.spef" } - ] - }, - { - "name": "Cworst", - "temperature": [-40, 125], - "ecc_tf": "/corners/Cworst.json", - "itf_file": "/corners/Cworst.itf", - "captab_file": "/corners/Cworst.captab", - "spef_file": [ - { "-40" : "/RCX_ecc/output/gcd_Cworst_m40C.spef" }, - { "125" : "/RCX_ecc/output/gcd_Cworst_125C.spef" } - ] - } - ] + "output": "/RCX_ecc/output" } diff --git a/chipcompiler/tools/ecc/configs/sta.json b/chipcompiler/tools/ecc/configs/sta.json index 2a734ab0..0293a927 100644 --- a/chipcompiler/tools/ecc/configs/sta.json +++ b/chipcompiler/tools/ecc/configs/sta.json @@ -5,7 +5,7 @@ "temperature": 125, "path" : [ "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_ss_rcworst_1p08_125_nldm.lib", - "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CR_ss_rcworst_1p08_125_nldm.lib" + "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_ss_rcworst_1p08_125_nldm.lib" ] }, { @@ -13,7 +13,7 @@ "temperature": -40, "path" : [ "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_ss_cworst_1p08_m40_nldm.lib", - "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CR_ss_cworst_1p08_m40_nldm.lib" + "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_ss_cworst_1p08_m40_nldm.lib" ] }, { @@ -21,7 +21,7 @@ "temperature": 25, "path" : [ "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_typ_tt_1p2_25_nldm.lib", - "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CR_typ_tt_1p2_25_nldm.lib" + "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_typ_tt_1p2_25_nldm.lib" ] }, { @@ -29,7 +29,7 @@ "temperature": -40, "path" : [ "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_ff_rcbest_1p32_m40_nldm.lib", - "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CR_ff_rcbest_1p32_m40_nldm.lib" + "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_ff_rcbest_1p32_m40_nldm.lib" ] }, { @@ -37,7 +37,7 @@ "temperature": 125, "path" : [ "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_ff_cbest_1p32_125_nldm.lib", - "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CR_ff_cbest_1p32_125_nldm.lib" + "/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_ff_cbest_1p32_125_nldm.lib" ] } ], diff --git a/chipcompiler/tools/ecc/module.py b/chipcompiler/tools/ecc/module.py index 8bae313c..347d0fff 100644 --- a/chipcompiler/tools/ecc/module.py +++ b/chipcompiler/tools/ecc/module.py @@ -890,7 +890,7 @@ def is_rt_timing_enable(self, config : str): ######################################################################## # RCX api ######################################################################## - def init_rcx(self, config: str, pdk: str = ""): + def init_rcx(self, config: str, pdk: str = "ics55"): if pdk: return self.ecc.init_rcx(config=config, pdk=pdk) return self.ecc.init_rcx(config=config) @@ -901,9 +901,6 @@ def run_rcx(self): def report_rcx(self): return self.ecc.report_rcx() - def rcx_json_to_itf(self, json_path: str, itf_path: str): - rcx_extractor =self.RcxExtraction(json_path, itf_path) - rcx_extractor.transfer() ######################################################################## # STA api @@ -1293,861 +1290,4 @@ def update_and_get_all_pin_timings( pin_net_delay, cell_arc_delays, net_timing_details, - ) - - class RcxExtraction: - SAFE_TOKEN_RE = re.compile(r"^[^\s{}=#]+$") - - PROCESS_FIELD_MAP = { - "name": "TECHNOLOGY", - "temperature": "GLOBAL_TEMPERATURE", - "half_node_scale_factor": "HALF_NODE_SCALE_FACTOR", - } - - FIELD_MAP = { - "epsilon_ratio": "ER", - "min_width": "WMIN", - "min_spacing": "SMIN", - "btm_layer": "FROM", - "top_layer": "TO", - "res_per_via": "RPV", - "area": "AREA", - "TCR1": "CRT1", - "TCR2": "CRT2", - "layer_type": "LAYER_TYPE", - "temp_reference": "T0", - "measured_from": "MEASURED_FROM", - "top_thickness": "TW_T", - "side_thickness": "SW_T", - "damage_thickness": "DAMAGE_THICKNESS", - "damage_er": "DAMAGE_ER", - "gate_to_contact_smin": "GATE_TO_CONTACT_SMIN", - "side_tangent": "SIDE_TANGENT", - "dielectric_layer": "DIELECTRIC_LAYER", - "number_of_tables": "NUMBER_OF_TABLES", - "contact_to_contact_spacings": "CONTACT_TO_CONTACT_SPACINGS", - "gate_to_contact_spacings": "GATE_TO_CONTACT_SPACINGS", - "caps_per_micron": "CAPS_PER_MICRON", - "thickness_changes": "THICKNESS_CHANGES", - "lengths": "LENGTHS", - "widths": "WIDTHS", - "spacings": "SPACINGS", - "values": "VALUES", - "density_polynomial_orders": "DENSITY_POLYNOMIAL_ORDERS", - "width_polynomial_orders": "WIDTH_POLYNOMIAL_ORDERS", - "width_ranges": "WIDTH_RANGES", - "polynomial_coefficients": "POLYNOMIAL_COEFFICIENTS", - } - - SCALAR_ETCH_FIELDS = { - "etch_shrink": "ETCH", - "etch_shrink_c": "CAPACITIVE_ONLY_ETCH", - "etch_shrink_r": "RESISTIVE_ONLY_ETCH", - } - - CONDUCTOR_SPECIAL_BLOCKS = { - "rho", - "wire_edge_enlargement", - "wire_edge_enlargement_c", - "wire_edge_enlargement_r", - "width_dependent_tc", - "wire_thickness_ratio", - "polynomial_based_thickness_variation", - "gate_to_diffusion_cap", - "ild_vs_width_and_spacing", - } - - VIA_SPECIAL_BLOCKS = { - "contact_resistance", - "crt_vs_area", - "etch_vs_width_and_length", - "etch_vs_contact_and_gate_spacings", - } - - RESERVED_KEYS = { - "name", - "btm_height", - "top_height", - "entries", - } - - def __init__(self, input_path : str, output_path : str) -> None: - self.input_path = input_path - self.output_path = output_path - - def transfer(self): - data = self.load_json() - output = self.json_to_itf(data) - - Path(self.output_path).write_text(output) - - - @dataclass(frozen=True) - class JsonNumber: - text: str - decimal: Decimal - - - @dataclass(frozen=True) - class EntryLayout: - type_width: int - name_width: int - - - def parse_json_number(self, text: str) -> JsonNumber: - return self.JsonNumber(text, Decimal(text)) - - - def as_decimal(self,value: Any, default: Decimal = Decimal("0")) -> Decimal: - if value is None: - return default - if isinstance(value, self.JsonNumber): - return value.decimal - if isinstance(value, Decimal): - return value - if isinstance(value, int): - return Decimal(value) - if isinstance(value, float): - return Decimal(str(value)) - text = str(value).strip() - if not text: - return default - try: - return Decimal(text) - except InvalidOperation: - return default - - - def decimal_to_text(self, value: Decimal) -> str: - text = format(value, "f") - if "." in text: - text = text.rstrip("0").rstrip(".") - if text in {"", "-0", "-0.0"}: - return "0" - return text - - - def quote_token(self, value: str) -> str: - if self.SAFE_TOKEN_RE.match(value) and not value.startswith("//"): - return value - return json.dumps(value, ensure_ascii=False) - - - def format_scalar(self, value: Any) -> str: - if isinstance(value, self.JsonNumber): - return value.text - if isinstance(value, Decimal): - return self.decimal_to_text(value) - if isinstance(value, bool): - return "1" if value else "0" - if isinstance(value, int): - return str(value) - if isinstance(value, float): - if not math.isfinite(value): - raise ValueError(f"non-finite float is not valid ITF value: {value!r}") - return format(value, ".15g") - if value is None: - raise ValueError("null is not a valid ITF value") - if isinstance(value, str): - return self.quote_token(value) - return self.quote_token(str(value)) - - - def format_freeform_cell(self, value: Any) -> str: - if isinstance(value, str): - return value.strip() - return self.format_scalar(value) - - - def format_row(self, value: Any) -> str: - if isinstance(value, str): - return value.strip() - if isinstance(value, list): - return " ".join(self.format_freeform_cell(cell) for cell in value) - return self.format_freeform_cell(value) - - - def format_parenthesized_entry(self, entry: Any) -> str: - if isinstance(entry, str): - text = entry.strip() - return text if text.startswith("(") else f"({text})" - if isinstance(entry, list): - return "(" + ", ".join(self.format_freeform_cell(cell) for cell in entry) + ")" - raise ValueError(f"unsupported parenthesized entry payload: {type(entry)!r}") - - - def is_scalar_sequence(self, value: Any) -> bool: - return isinstance(value, list) and all(not isinstance(item, (list, dict)) for item in value) - - - def is_matrix_like(self, value: Any) -> bool: - return isinstance(value, list) and value and all(isinstance(item, (list, str)) for item in value) - - - def looks_like_series_text(self, value: str) -> bool: - text = value.strip() - return any(token in text for token in (" ", "\t", "\n", "(", ")")) - - - def normalized_statement_name(self, name: str) -> str: - return self.FIELD_MAP.get(name, name.upper()) - - - def emit_assignment(self, key: str, value: Any, indent: str = "") -> str: - return f"{indent}{self.normalized_statement_name(key)} = {self.format_scalar(value)}" - - - def format_assignment_pair(self, key: str, value: Any) -> str: - return f"{self.normalized_statement_name(key)} = {self.format_scalar(value)}" - - - def entry_prefix(self, kind: str, name: str, layout: EntryLayout) -> str: - return f"{kind:<{layout.type_width}} {name:<{layout.name_width}} " - - - def emit_inline_entry(self, kind: str, name: str, pairs: list[str], layout: EntryLayout) -> str: - prefix = self.entry_prefix(kind, name, layout) - body = " ".join(pairs) - return f"{prefix}{{ {body} }}" - - - def value_requires_block(self, value: Any) -> bool: - if isinstance(value, dict): - return True - if isinstance(value, list): - return True - if isinstance(value, str) and self.looks_like_series_text(value): - return True - return False - - - def emit_braced_value(self, label: str, value: Any, indent: str = "") -> list[str]: - statement = self.normalized_statement_name(label) - if isinstance(value, str): - return [f"{indent}{statement} {{ {value.strip()} }}"] - if self.is_scalar_sequence(value): - body = " ".join(self.format_freeform_cell(item) for item in value) - return [f"{indent}{statement} {{ {body} }}"] - if self.is_matrix_like(value): - lines = [f"{indent}{statement} {{"] - child_indent = indent + " " - for row in value: - lines.append(f"{child_indent}{self.format_row(row)}") - lines.append(f"{indent}}}") - return lines - return [f"{indent}{statement} {{ {self.format_row(value)} }}"] - - - def emit_parenthesized_block(self, statement: str, value: Any, indent: str = "") -> list[str]: - if isinstance(value, str): - return [f"{indent}{statement} {{ {value.strip()} }}"] - - entries: list[Any] - if isinstance(value, list): - entries = value - else: - raise ValueError(f"{statement} requires a string or entry list") - - lines = [f"{indent}{statement} {{"] - child_indent = indent + " " - for entry in entries: - lines.append(f"{child_indent}{self.format_parenthesized_entry(entry)}") - lines.append(f"{indent}}}") - return lines - - - def emit_generic_block(self, statement: str, payload: Any, indent: str = "") -> list[str]: - if isinstance(payload, list): - if not payload: - return [f"{indent}{statement} {{}}"] - if all(isinstance(item, dict) for item in payload): - lines: list[str] = [] - for item in payload: - lines.extend(self.emit_generic_block(statement, item, indent)) - return lines - return self.emit_braced_value(statement, payload, indent) - - if not isinstance(payload, dict): - return self.emit_braced_value(statement, payload, indent) - - lines = [f"{indent}{statement} {{"] - child_indent = indent + " " - for key, value in payload.items(): - if key == "entries": - continue - if isinstance(value, dict): - nested_statement = self.normalized_statement_name(key) - lines.extend(self.emit_generic_block(nested_statement, value, child_indent)) - continue - if isinstance(value, list) and value and all(isinstance(item, dict) for item in value): - nested_statement = self.normalized_statement_name(key) - for item in value: - lines.extend(self.emit_generic_block(nested_statement, item, child_indent)) - continue - if isinstance(value, list): - lines.extend(self.emit_braced_value(key, value, child_indent)) - continue - if isinstance(value, str): - if self.looks_like_series_text(value): - lines.extend(self.emit_braced_value(key, value, child_indent)) - else: - lines.append(self.emit_assignment(key, value, child_indent)) - continue - lines.append(self.emit_assignment(key, value, child_indent)) - lines.append(f"{indent}}}") - return lines - - - def emit_rho_block(self, field_name: str, payload: Any, indent: str = "") -> list[str]: - if not isinstance(payload, dict): - raise ValueError(f"{field_name} must be a scalar or object") - - if "silicon_width" in payload or "silicon_thickness" in payload: - lines = [f"{indent}RHO_VS_SI_WIDTH_AND_THICKNESS {{"] - child_indent = indent + " " - if "silicon_width" in payload: - lines.extend(self.emit_braced_value("WIDTH", payload["silicon_width"], child_indent)) - if "silicon_thickness" in payload: - lines.extend(self.emit_braced_value("THICKNESS", payload["silicon_thickness"], child_indent)) - if "values" in payload: - lines.extend(self.emit_braced_value("VALUES", payload["values"], child_indent)) - lines.append(f"{indent}}}") - return lines - - if "draw_width" in payload or "draw_spacing" in payload: - lines = [f"{indent}RHO_VS_WIDTH_AND_SPACING {{"] - child_indent = indent + " " - if "draw_spacing" in payload: - lines.extend(self.emit_braced_value("SPACINGS", payload["draw_spacing"], child_indent)) - if "draw_width" in payload: - lines.extend(self.emit_braced_value("WIDTHS", payload["draw_width"], child_indent)) - if "values" in payload: - lines.extend(self.emit_braced_value("VALUES", payload["values"], child_indent)) - lines.append(f"{indent}}}") - return lines - - raise ValueError("unsupported rho object shape") - - - def emit_wire_edge_enlargement(self, field_name: str, payload: Any, indent: str = "") -> list[str]: - qualifier = "" - if field_name == "wire_edge_enlargement_c": - qualifier = " CAPACITIVE_ONLY" - elif field_name == "wire_edge_enlargement_r": - qualifier = " RESISTIVE_ONLY" - - def emit_one(table: dict[str, Any]) -> list[str]: - lines = [f"{indent}ETCH_VS_WIDTH_AND_SPACING{qualifier} {{"] - child_indent = indent + " " - if "wee_spacings" in table: - lines.extend(self.emit_braced_value("SPACINGS", table["wee_spacings"], child_indent)) - if "wee_widths" in table: - lines.extend(self.emit_braced_value("WIDTHS", table["wee_widths"], child_indent)) - if "wee_adjustments" in table: - lines.extend(self.emit_braced_value("VALUES", table["wee_adjustments"], child_indent)) - lines.append(f"{indent}}}") - return lines - - if isinstance(payload, list): - lines: list[str] = [] - for item in payload: - if not isinstance(item, dict): - raise ValueError(f"{field_name} repeated payload must be a dict list") - lines.extend(emit_one(item)) - return lines - - if not isinstance(payload, dict): - raise ValueError(f"{field_name} must be a dict or dict list") - return emit_one(payload) - - - def emit_width_dependent_tc(self, payload: Any, indent: str = "") -> list[str]: - if not isinstance(payload, dict): - raise ValueError("width_dependent_tc must be an object") - widths = payload.get("widths", []) - tcr1 = payload.get("TCR1", []) - tcr2 = payload.get("TCR2", []) - if not isinstance(widths, list) or not isinstance(tcr1, list) or not isinstance(tcr2, list): - raise ValueError("width_dependent_tc widths/TCR1/TCR2 must be lists") - if len(widths) != len(tcr1) or len(widths) != len(tcr2): - raise ValueError("width_dependent_tc list lengths do not match") - - lines = [f"{indent}CRT_VS_SI_WIDTH {{"] - child_indent = indent + " " - for width, coeff1, coeff2 in zip(widths, tcr1, tcr2): - lines.append( - f"{child_indent}({self.format_freeform_cell(width)}, {self.format_freeform_cell(coeff1)}, {self.format_freeform_cell(coeff2)})" - ) - lines.append(f"{indent}}}") - return lines - - - def emit_wire_thickness_ratio(self, payload: Any, indent: str = "") -> list[str]: - if not isinstance(payload, dict): - raise ValueError("wire_thickness_ratio must be an object") - densities = payload.get("densities", []) - deltas = payload.get("thickness_deltas", []) - if not isinstance(densities, list) or not isinstance(deltas, list): - raise ValueError("wire_thickness_ratio densities/thickness_deltas must be lists") - if len(densities) != len(deltas): - raise ValueError("wire_thickness_ratio list lengths do not match") - - qualifiers = payload.get("qualifiers") - qualifier_text = "" - if isinstance(qualifiers, list) and qualifiers: - qualifier_text = " " + " ".join(str(item).upper() for item in qualifiers) - - lines = [f"{indent}THICKNESS_VS_DENSITY{qualifier_text} {{"] - child_indent = indent + " " - for density, delta in zip(densities, deltas): - lines.append(f"{child_indent}({self.format_freeform_cell(density)}, {self.format_freeform_cell(delta)})") - lines.append(f"{indent}}}") - return lines - - - def normalize_polynomial_tables(payload: dict[str, Any]) -> list[list[Any]]: - coefficients = payload.get("polynomial_coefficients") - if not isinstance(coefficients, list): - raise ValueError("polynomial_coefficients must be a list") - if not coefficients: - return [] - - if all(isinstance(row, list) and all(not isinstance(cell, list) for cell in row) for row in coefficients): - return [coefficients] - - if all(isinstance(table, list) for table in coefficients): - return coefficients - - raise ValueError("unsupported polynomial_coefficients payload") - - - def emit_polynomial_based_thickness_variation(self, payload: Any, indent: str = "") -> list[str]: - if not isinstance(payload, dict): - raise ValueError("polynomial_based_thickness_variation must be an object") - lines = [f"{indent}POLYNOMIAL_BASED_THICKNESS_VARIATION {{"] - child_indent = indent + " " - if "density_polynomial_orders" in payload: - lines.extend( - self.emit_braced_value("DENSITY_POLYNOMIAL_ORDERS", payload["density_polynomial_orders"], child_indent) - ) - if "width_polynomial_orders" in payload: - lines.extend( - self.emit_braced_value("WIDTH_POLYNOMIAL_ORDERS", payload["width_polynomial_orders"], child_indent) - ) - if "width_ranges" in payload: - lines.extend(self.emit_braced_value("WIDTH_RANGES", payload["width_ranges"], child_indent)) - - for table in self.normalize_polynomial_tables(payload): - lines.append(f"{child_indent}POLYNOMIAL_COEFFICIENTS {{") - row_indent = child_indent + " " - for row in table: - lines.append(f"{row_indent}{self.format_row(row)}") - lines.append(f"{child_indent}}}") - - lines.append(f"{indent}}}") - return lines - - - def emit_ild_vs_width_and_spacing(self, payload: Any, indent: str = "") -> list[str]: - if not isinstance(payload, dict): - raise ValueError("ild_vs_width_and_spacing must be an object") - lines = [f"{indent}ILD_VS_WIDTH_AND_SPACING {{"] - child_indent = indent + " " - if "dielectric_layer" in payload: - lines.append(self.emit_assignment("dielectric_layer", payload["dielectric_layer"], child_indent)) - if "widths" in payload: - lines.extend(self.emit_braced_value("WIDTHS", payload["widths"], child_indent)) - if "spacings" in payload: - lines.extend(self.emit_braced_value("SPACINGS", payload["spacings"], child_indent)) - if "thickness_changes" in payload: - lines.extend(self.emit_braced_value("THICKNESS_CHANGES", payload["thickness_changes"], child_indent)) - lines.append(f"{indent}}}") - return lines - - - def infer_qualifier(self, entry: dict[str, Any]) -> str: - has_c = "etch_shrink_c" in entry - has_r = "etch_shrink_r" in entry - has_rc = "etch_shrink" in entry - if has_c and not has_r and not has_rc: - return " CAPACITIVE_ONLY" - if has_r and not has_c and not has_rc: - return " RESISTIVE_ONLY" - return "" - - - def emit_etch_vs_width_and_length(self, payload: Any, entry: dict[str, Any], indent: str = "") -> list[str]: - if not isinstance(payload, dict): - raise ValueError("etch_vs_width_and_length must be an object") - qualifier = self.infer_qualifier(entry) - lines = [f"{indent}ETCH_VS_WIDTH_AND_LENGTH{qualifier} {{"] - child_indent = indent + " " - if "lengths" in payload: - lines.extend(self.emit_braced_value("LENGTHS", payload["lengths"], child_indent)) - if "widths" in payload: - lines.extend(self.emit_braced_value("WIDTHS", payload["widths"], child_indent)) - if "entries" in payload: - lines.extend(self.emit_parenthesized_block("VALUES", payload["entries"], child_indent)) - elif "values" in payload: - lines.extend(self.emit_braced_value("VALUES", payload["values"], child_indent)) - lines.append(f"{indent}}}") - return lines - - - def emit_etch_vs_contact_and_gate_spacings(self, payload: Any, entry: dict[str, Any], indent: str = "") -> list[str]: - statement = "ETCH_VS_CONTACT_AND_GATE_SPACINGS" + self.infer_qualifier(entry) - return self.emit_generic_block(statement, payload, indent) - - - def emit_special_conductor_block(self, field_name: str, payload: Any, entry: dict[str, Any], indent: str = "") -> list[str]: - if field_name == "rho": - return self.emit_rho_block(field_name, payload, indent) - if field_name in {"wire_edge_enlargement", "wire_edge_enlargement_c", "wire_edge_enlargement_r"}: - return self.emit_wire_edge_enlargement(field_name, payload, indent) - if field_name == "width_dependent_tc": - return self.emit_width_dependent_tc(payload, indent) - if field_name == "wire_thickness_ratio": - return self.emit_wire_thickness_ratio(payload, indent) - if field_name == "polynomial_based_thickness_variation": - return self.emit_polynomial_based_thickness_variation(payload, indent) - if field_name == "gate_to_diffusion_cap": - return self.emit_generic_block("GATE_TO_DIFFUSION_CAP", payload, indent) - if field_name == "ild_vs_width_and_spacing": - return self.emit_ild_vs_width_and_spacing(payload, indent) - raise ValueError(f"unsupported conductor block field: {field_name}") - - - def emit_special_via_block(self, field_name: str, payload: Any, entry: dict[str, Any], indent: str = "") -> list[str]: - if field_name == "contact_resistance": - return self.emit_parenthesized_block("RPV_VS_AREA", payload, indent) - if field_name == "crt_vs_area": - return self.emit_parenthesized_block("CRT_VS_AREA", payload, indent) - if field_name == "etch_vs_width_and_length": - return self.emit_etch_vs_width_and_length(payload, entry, indent) - if field_name == "etch_vs_contact_and_gate_spacings": - return self.emit_etch_vs_contact_and_gate_spacings(payload, entry, indent) - raise ValueError(f"unsupported via block field: {field_name}") - - - def conductor_thickness(self, entry: dict[str, Any]) -> Decimal: - return self.as_decimal(entry.get("top_height")) - self.as_decimal(entry.get("btm_height")) - - - def dielectric_thickness(self, entry: dict[str, Any], layers_by_name: dict[str, dict[str, Any]]) -> Decimal: - del layers_by_name - return self.as_decimal(entry.get("top_height")) - self.as_decimal(entry.get("btm_height")) - - - def emit_conductor(self, entry: dict[str, Any], layout: EntryLayout) -> str: - name = self.format_scalar(entry["name"]) - lines = [f"{self.entry_prefix('CONDUCTOR', name, layout)}{{"] - indent = " " - consumed = set(self.RESERVED_KEYS) - inline_pairs: list[str] = [] - has_nested_blocks = False - - inline_pairs.append(self.format_assignment_pair("THICKNESS", self.conductor_thickness(entry))) - - scalar_order = [ - "layer_type", - "min_width", - "min_spacing", - "gate_to_contact_smin", - "side_tangent", - "rho", - "rpsq", - "TCR1", - "TCR2", - "temp_reference", - "etch_shrink", - "etch_shrink_c", - "etch_shrink_r", - "damage_thickness", - "damage_er", - ] - - for key in scalar_order: - if key not in entry: - continue - value = entry[key] - if key == "rho" and isinstance(value, dict): - continue - if key == "rpsq" and isinstance(value, dict): - raise ValueError("rpsq table/object forms are not supported in json->itf") - if key in self.SCALAR_ETCH_FIELDS: - inline_pairs.append(self.format_assignment_pair(self.SCALAR_ETCH_FIELDS[key], -self.as_decimal(value))) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - consumed.add(key) - - block_order = [ - "polynomial_based_thickness_variation", - "rho", - "wire_edge_enlargement", - "wire_edge_enlargement_c", - "wire_edge_enlargement_r", - "ild_vs_width_and_spacing", - "width_dependent_tc", - "wire_thickness_ratio", - "gate_to_diffusion_cap", - ] - - for key in block_order: - if key not in entry: - continue - has_nested_blocks = True - lines.extend(self.emit_special_conductor_block(key, entry[key], entry, indent)) - consumed.add(key) - - for key, value in entry.items(): - if key in consumed: - continue - if isinstance(value, dict) or (isinstance(value, list) and value and all(isinstance(item, dict) for item in value)): - has_nested_blocks = True - lines.extend(self.emit_generic_block(self.normalized_statement_name(key), value, indent)) - elif isinstance(value, list): - has_nested_blocks = True - lines.extend(self.emit_braced_value(key, value, indent)) - elif isinstance(value, str): - if self.looks_like_series_text(value): - has_nested_blocks = True - lines.extend(self.emit_braced_value(key, value, indent)) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - - if not has_nested_blocks: - return self.emit_inline_entry("CONDUCTOR", name, inline_pairs, layout) - - if inline_pairs: - for pair in reversed(inline_pairs): - key, value = pair.split(" = ", 1) - lines.insert(1, f"{indent}{key} = {value}") - lines.append("}") - return "\n".join(lines) - - - def emit_dielectric( - self, - entry: dict[str, Any], - layers_by_name: dict[str, dict[str, Any]], - layout: EntryLayout, - ) -> str: - name = self.format_scalar(entry["name"]) - lines = [f"{self.entry_prefix('DIELECTRIC', name, layout)}{{"] - indent = " " - consumed = set(self.RESERVED_KEYS) - inline_pairs: list[str] = [] - has_nested_blocks = False - - inline_pairs.append(self.format_assignment_pair("THICKNESS", self.dielectric_thickness(entry, layers_by_name))) - consumed.add("measured_from") - - scalar_order = [ - "epsilon_ratio", - "damage_thickness", - "damage_er", - "top_thickness", - "side_thickness", - "measured_from", - "temp_reference", - ] - for key in scalar_order: - if key not in entry: - continue - inline_pairs.append(self.format_assignment_pair(key, entry[key])) - consumed.add(key) - - for key, value in entry.items(): - if key in consumed: - continue - if isinstance(value, dict) or (isinstance(value, list) and value and all(isinstance(item, dict) for item in value)): - has_nested_blocks = True - lines.extend(self.emit_generic_block(self.normalized_statement_name(key), value, indent)) - elif isinstance(value, list): - has_nested_blocks = True - lines.extend(self.emit_braced_value(key, value, indent)) - elif isinstance(value, str): - if self.looks_like_series_text(value): - has_nested_blocks = True - lines.extend(self.emit_braced_value(key, value, indent)) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - - if not has_nested_blocks: - return self.emit_inline_entry("DIELECTRIC", name, inline_pairs, layout) - - if inline_pairs: - for pair in reversed(inline_pairs): - key, value = pair.split(" = ", 1) - lines.insert(1, f"{indent}{key} = {value}") - lines.append("}") - return "\n".join(lines) - - - def emit_via(self, entry: dict[str, Any], layout: EntryLayout) -> str: - name = self.format_scalar(entry["name"]) - lines = [f"{self.entry_prefix('VIA', name, layout)}{{"] - indent = " " - consumed = set(self.RESERVED_KEYS) - inline_pairs: list[str] = [] - has_nested_blocks = False - - scalar_order = [ - "btm_layer", - "top_layer", - "area", - "res_per_via", - "rho", - "TCR1", - "TCR2", - "temp_reference", - "etch_shrink", - "etch_shrink_c", - "etch_shrink_r", - ] - for key in scalar_order: - if key not in entry: - continue - if key in self.SCALAR_ETCH_FIELDS: - inline_pairs.append(self.format_assignment_pair(self.SCALAR_ETCH_FIELDS[key], -self.as_decimal(entry[key]))) - else: - inline_pairs.append(self.format_assignment_pair(key, entry[key])) - consumed.add(key) - - block_order = [ - "contact_resistance", - "crt_vs_area", - "etch_vs_width_and_length", - "etch_vs_contact_and_gate_spacings", - ] - for key in block_order: - if key not in entry: - continue - has_nested_blocks = True - lines.extend(self.emit_special_via_block(key, entry[key], entry, indent)) - consumed.add(key) - - for key, value in entry.items(): - if key in consumed: - continue - if isinstance(value, dict) or (isinstance(value, list) and value and all(isinstance(item, dict) for item in value)): - has_nested_blocks = True - lines.extend(self.emit_generic_block(self.normalized_statement_name(key), value, indent)) - elif isinstance(value, list): - has_nested_blocks = True - lines.extend(self.emit_braced_value(key, value, indent)) - elif isinstance(value, str): - if self.looks_like_series_text(value): - has_nested_blocks = True - lines.extend(self.emit_braced_value(key, value, indent)) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - else: - inline_pairs.append(self.format_assignment_pair(key, value)) - - if not has_nested_blocks: - return self.emit_inline_entry("VIA", name, inline_pairs, layout) - - if inline_pairs: - for pair in reversed(inline_pairs): - key, value = pair.split(" = ", 1) - lines.insert(1, f"{indent}{key} = {value}") - lines.append("}") - return "\n".join(lines) - - - def emit_process(self, process: dict[str, Any]) -> list[str]: - lines: list[str] = [] - consumed: set[str] = set() - for key in ("name", "temperature", "half_node_scale_factor"): - if key in process: - lines.append(self.emit_assignment(self.PROCESS_FIELD_MAP[key], process[key])) - consumed.add(key) - for key, value in process.items(): - if key in consumed: - continue - lines.append(self.emit_assignment(key, value)) - return lines - - - def build_entry_layout(self, entries: list[dict[str, Any]], kinds: list[str]) -> EntryLayout: - type_width = max(len(kind) for kind in kinds) - names = [self.format_scalar(entry["name"]) for entry in entries if "name" in entry] - name_width = max([len(name) for name in names] + [1]) - return self.EntryLayout(type_width=type_width, name_width=name_width) - - - def sort_layers(self, conductors: list[dict[str, Any]], dielectrics: list[dict[str, Any]]) -> list[tuple[str, dict[str, Any]]]: - ordered: list[tuple[Decimal, int, int, str, dict[str, Any]]] = [] - for index, entry in enumerate(dielectrics): - ordered.append((self.as_decimal(entry.get("btm_height")), 0, index, "DIELECTRIC", entry)) - for index, entry in enumerate(conductors): - ordered.append((self.as_decimal(entry.get("btm_height")), 1, index, "CONDUCTOR", entry)) - ordered.sort(key=lambda item: (-item[0], item[1], item[2])) - return [(kind, entry) for _, _, _, kind, entry in ordered] - - - def json_to_itf(self, data: dict[str, Any]) -> str: - if not isinstance(data, dict): - raise ValueError("top-level JSON must be an object") - - process = data.get("process", {}) - conductors = data.get("conductors", []) - dielectrics = data.get("dielectrics", []) - vias = data.get("vias", []) - - if not isinstance(process, dict): - raise ValueError("process must be an object") - if not isinstance(conductors, list) or not all(isinstance(item, dict) for item in conductors): - raise ValueError("conductors must be an object list") - if not isinstance(dielectrics, list) or not all(isinstance(item, dict) for item in dielectrics): - raise ValueError("dielectrics must be an object list") - if not isinstance(vias, list) or not all(isinstance(item, dict) for item in vias): - raise ValueError("vias must be an object list") - - layers_by_name = {entry["name"]: entry for entry in conductors + dielectrics if "name" in entry} - layer_layout = self.build_entry_layout(conductors + dielectrics, ["CONDUCTOR", "DIELECTRIC"]) - via_layout = self.build_entry_layout(vias, ["VIA"]) - - lines: list[str] = [] - process_lines = self.emit_process(process) - lines.extend(process_lines) - - rendered_blocks: list[tuple[str, bool]] = [] - - for kind, entry in self.sort_layers(conductors, dielectrics): - if kind == "DIELECTRIC": - block = self.emit_dielectric(entry, layers_by_name, layer_layout) - else: - block = self.emit_conductor(entry, layer_layout) - rendered_blocks.append((block, "\n" in block)) - - for entry in vias: - block = self.emit_via(entry, via_layout) - rendered_blocks.append((block, "\n" in block)) - - if process_lines and rendered_blocks: - lines.append("") - - previous_multiline = False - for index, (block, is_multiline) in enumerate(rendered_blocks): - if index > 0 and (is_multiline or previous_multiline): - lines.append("") - lines.extend(block.splitlines()) - previous_multiline = is_multiline - - return "\n".join(lines) + "\n" - - - def load_json(self, path: Path = None) -> dict[str, Any]: - if path is None: - path = Path(self.input_path) - return json.loads( - path.read_text(), - parse_int=self.parse_json_number, - parse_float=self.parse_json_number, - ) + ) \ No newline at end of file diff --git a/chipcompiler/tools/ecc/runner.py b/chipcompiler/tools/ecc/runner.py index e863530f..56dfd0c9 100644 --- a/chipcompiler/tools/ecc/runner.py +++ b/chipcompiler/tools/ecc/runner.py @@ -2,7 +2,6 @@ # -*- encoding: utf-8 -*- import sys import os -import re from chipcompiler.data import WorkspaceStep, Workspace, StateEnum, StepEnum from chipcompiler.tools.ecc.module import ECCToolsModule @@ -17,7 +16,7 @@ def create_db_engine(workspace: Workspace, step: WorkspaceStep) -> ECCToolsModule: """""" - def load_data(): + def load_data(): ecc_module = ECCToolsModule() ecc_module.init_config(flow_config=workspace.config.get("flow"), @@ -109,13 +108,13 @@ def save_data(workspace: Workspace, step: WorkspaceStep, ecc_module : ECCToolsModule, feature_step : bool = True, - report_timing : bool = True) -> bool: + report_timing : bool = False) -> bool: """ module is ecc module from db engine, eda instacnce may initialize data from this module if module has been set """ if ecc_module is None: - return FALSE + return False ecc_module.def_save(def_path=step.output.get("def", "")) ecc_module.verilog_save(output_verilog=step.output.get("verilog", "")) @@ -152,6 +151,11 @@ def save_data(workspace: Workspace, margin = workspace.parameters.data.get("Core", {}).get("Margin", [0, 0]) + core_usage = db_json.get("Design Layout", {}).get("core_usage", 0) + + aspect_ratio = die_bounding_width / die_bounding_height if die_bounding_height > 0 else 1 + + update_param = { "Die": { "Size": [die_bounding_width, die_bounding_height], @@ -165,7 +169,9 @@ def save_data(workspace: Workspace, margin[1], core_bounding_width + margin[0], core_bounding_height + margin[1] - ) + ), + "Utilitization": core_usage, + "Aspect ratio": aspect_ratio } } @@ -307,7 +313,7 @@ def run_placement(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.run_placement.value, state=StateEnum.Success) - reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -437,6 +443,7 @@ def run_routing(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.load_data.value, state=StateEnum.Success) if ecc_module.is_rt_timing_enable(config=workspace.config.get(f"{StepEnum.ROUTING.value}", "")): + ecc_module.release_sta() ecc_module.init_sta(output_dir=step.data.get(f"{StepEnum.ROUTING.value}", ""), top_module=workspace.design.top_module, lib_paths=workspace.pdk.libs, @@ -446,7 +453,7 @@ def run_routing(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.run_routing.value, state=StateEnum.Success) - reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -481,7 +488,7 @@ def run_drc(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.run_DRC.value, state=StateEnum.Success) - reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False) ecc_module.save_drc(feature_path=step.feature.get("step", "")) @@ -517,7 +524,7 @@ def run_legalization(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.run_legalization.value, state=StateEnum.Success) - reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -548,7 +555,7 @@ def run_filler(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.run_filler.value, state=StateEnum.Success) - reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -576,28 +583,39 @@ def run_floorplan(workspace: Workspace, state=StateEnum.Success) # init floorplan - # init by core utilization + die_area=workspace.parameters.data.get("Die", {}).get("Size", []) + core_area=workspace.parameters.data.get("Core", {}).get("Size", []) util = workspace.parameters.data.get("Core", {}).get("Utilitization", 0.3) margin = workspace.parameters.data.get("Core", {}).get("Margin", [0, 0]) aspect_ratio = workspace.parameters.data.get("Core", {}).get("Aspect ratio", 1) - ecc_module.init_floorplan_by_core_utilization( - core_site=workspace.pdk.site_core, - io_site=workspace.pdk.site_io, - corner_site=workspace.pdk.site_corner, - core_util=util, - x_margin=margin[0], - y_margin=margin[1], - aspect_ratio=aspect_ratio, - ) - - # init by die and core area - # die_area=workspace.parameters.data.get("Die", {}).get("Bounding box", "") - # core_area=workspace.parameters.data.get("Core", {}).get("Bounding box", "") - # ecc_module.init_floorplan_by_area(die_area=die_area, - # core_area=core_area, - # core_site=workspace.pdk.site_core, - # io_site=workspace.pdk.site_io, - # corner_site=workspace.pdk.site_corner) + if len(die_area) == 0: + # init by core utilization + ecc_module.init_floorplan_by_core_utilization( + core_site=workspace.pdk.site_core, + io_site=workspace.pdk.site_io, + corner_site=workspace.pdk.site_corner, + core_util=util, + x_margin=margin[0], + y_margin=margin[1], + aspect_ratio=aspect_ratio, + ) + else: + # init by die and core area + if len(die_area) != 2 or len(margin) != 2: + return reslut + + str_die = f"0, 0, {die_area[0]}, {die_area[1]}" + + if len(core_area) == 2: + str_core = f"{margin[0]}, {margin[1]}, {core_area[0]+margin[0]}, {core_area[1]+margin[1]}" + else: + str_core = f"{margin[0]}, {margin[1]}, {die_area[0]-margin[0]}, {die_area[1]-margin[1]}" + + ecc_module.init_floorplan_by_area(die_area=str_die, + core_area=str_core, + core_site=workspace.pdk.site_core, + io_site=workspace.pdk.site_io, + corner_site=workspace.pdk.site_corner) sub_flow.update_step(step_name=EccSubFlowEnum.init_floorplan.value, state=StateEnum.Success) @@ -654,10 +672,11 @@ def run_floorplan(workspace: Workspace, # auto place io pins json_iopin_place = json_floorplan.get("Auto place pin", {}) - ecc_module.auto_place_pins(layer=json_iopin_place.get("layer", ""), - width=json_iopin_place.get("width", 0), - height=json_iopin_place.get("height", 0), - sides=json_iopin_place.get("sides", [])) + if len(json_iopin_place) > 0: + ecc_module.auto_place_pins(layer=json_iopin_place.get("layer", ""), + width=json_iopin_place.get("width", 0), + height=json_iopin_place.get("height", 0), + sides=json_iopin_place.get("sides", [])) sub_flow.update_step(step_name=EccSubFlowEnum.place_io_pins.value, state=StateEnum.Success) @@ -715,7 +734,7 @@ def run_floorplan(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.set_clock_net.value, state=StateEnum.Success) - reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, feature_step=False) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module, feature_step=False, report_timing=False) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -740,6 +759,7 @@ def run_harden(workspace: Workspace, ecc_module = ecc_module) if ecc_module is not None: + ecc_module.release_sta() ecc_module.init_sta(output_dir=step.data["sta"], top_module=workspace.design.top_module, lib_paths=workspace.pdk.libs, @@ -763,19 +783,6 @@ def run_rcx(workspace: Workspace, """ run rcx """ - def run_jsons_to_itf(ecc_module : ECCToolsModule) -> bool: - config=json_read(workspace.config.get(StepEnum.RCX.value, "")) - corners_dict = config.get("corners", []) - for item in corners_dict: - json_file = item.get("ecc_tf", "") - itf_file = item.get("itf_file", "") - - if not os.path.exists(json_file): - return False - - ecc_module.rcx_json_to_itf(json_path=json_file, itf_path=itf_file) - return True - result = False sub_flow = EccSubFlow(workspace=workspace, @@ -787,24 +794,19 @@ def run_jsons_to_itf(ecc_module : ECCToolsModule) -> bool: if ecc_module is not None: sub_flow.update_step(step_name=EccSubFlowEnum.load_data.value, state=StateEnum.Success) - if not run_jsons_to_itf(ecc_module): - sub_flow.update_step(step_name=EccSubFlowEnum.run_rcx.value, state=StateEnum.Imcomplete) - result = False - else: - rcx_config_path = workspace.config.get(StepEnum.RCX.value, "") - rcx_config = json_read(rcx_config_path) - rcx_pdk = str(rcx_config.get("pdk", "") or "").strip() - ecc_module.init_rcx(config=rcx_config_path, pdk=rcx_pdk) - ecc_module.run_rcx() - ecc_module.report_rcx() - sub_flow.update_step(step_name=EccSubFlowEnum.run_rcx.value, state=StateEnum.Success) - - save_data(workspace=workspace, step=step, ecc_module=ecc_module, feature_step=False) - - sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, - state=StateEnum.Success) - result = True + ecc_module.init_rcx(config=workspace.config.get(StepEnum.RCX.value, ""), + pdk=workspace.pdk.name) + ecc_module.run_rcx() + ecc_module.report_rcx() + sub_flow.update_step(step_name=EccSubFlowEnum.run_rcx.value, state=StateEnum.Success) + + save_data(workspace=workspace, step=step, ecc_module=ecc_module, feature_step=False, report_timing=False) + + sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, + state=StateEnum.Success) + + result = True return result @@ -822,97 +824,34 @@ def safe_dir_name(name: str) -> str: ) return value or "spef" - def temperature_key(temperature) -> str: + def temperature_token(temperature) -> str: try: numeric = float(temperature) if numeric.is_integer(): - return str(int(numeric)) + temperature = int(numeric) except (TypeError, ValueError): pass - return str(temperature) - - def temperature_token(temperature) -> str: - return temperature_key(temperature).replace("-", "m").replace(".", "p") - - def resolve_config_path(path: str) -> str: - if path.startswith("/"): - relative = path[1:] - workspace_prefixes = ( - "CTS_ecc", "Floorplan_ecc", "fixFanout_ecc", "place_ecc", - "legalization_ecc", "route_ecc", "drc_ecc", "filler_ecc", - "RCX_ecc", "rcx_ecc", "sta_ecc", "harden_ecc", "config", - "origin", "home", - ) - pdk_prefixes = ("IP", "prtech", "corners") - prefix = relative.split("/", 1)[0] - if prefix in workspace_prefixes: - return os.path.join(workspace.directory, relative) - if prefix in pdk_prefixes: - return os.path.join(workspace.pdk.root, relative) - return path - - def normalize_spef_path(spef_file: str) -> str: - if not spef_file or os.path.exists(spef_file): - return spef_file - - dirname = os.path.dirname(spef_file) - tail = os.path.basename(spef_file) - parts = tail.rsplit("_M", 1) - if len(parts) == 2 and parts[1].endswith("C.spef"): - normalized = os.path.join(dirname, f"{parts[0]}_m{parts[1]}") - if os.path.exists(normalized): - return normalized - return spef_file - - def normalize_liberty_path(liberty_file: str) -> str: - if not liberty_file or os.path.exists(liberty_file): - return liberty_file - - liberty_dir = os.path.dirname(liberty_file) - cell_dir = os.path.basename(os.path.dirname(liberty_dir)) - tail = os.path.basename(liberty_file) - match = re.match(r"^(ics55_LLSC_H7C[0-9A-Za-z]+)(_.+)$", tail) - if match: - normalized = os.path.join(liberty_dir, f"{cell_dir}{match.group(2)}") - if os.path.exists(normalized): - return normalized - return liberty_file - - def find_liberty_corner(sta_data: dict, corner_name: str) -> dict | None: - for liberty in sta_data.get("liberty", []): - if liberty.get("corner") == corner_name: - return liberty - return None - - def find_rcx_corner(rcx_data: dict, rcx_corner_name: str) -> dict | None: - for corner in rcx_data.get("corners", []): - if corner.get("name") == rcx_corner_name: - return corner - return None - - def find_spef_for_temp(rcx_corner: dict, temperature) -> str: - spef_file = rcx_corner.get("spef_file", "") - if isinstance(spef_file, str): - return spef_file - - temp_key = temperature_key(temperature) - for spef_item in spef_file: - if not isinstance(spef_item, dict): - continue - for spef_temperature, spef_path in spef_item.items(): - if temperature_key(spef_temperature) == temp_key: - return spef_path - return "" + return str(temperature).replace("-", "m").replace(".", "p") def collect_signoff_items() -> list[dict]: sta_config = workspace.config.get(StepEnum.STA.value, "") sta_data = json_read(sta_config) rcx_data = json_read(workspace.config.get(StepEnum.RCX.value, "")) + rcx_output_dir = str(rcx_data.get("output", "") or "") + if rcx_output_dir.startswith("/"): + relative_output_dir = rcx_output_dir[1:] + if relative_output_dir.split("/", 1)[0] in ("RCX_ecc", "rcx_ecc"): + rcx_output_dir = os.path.join(workspace.directory, relative_output_dir) + + liberty_by_corner = { + liberty.get("corner"): liberty + for liberty in sta_data.get("liberty", []) + } items = [] for signoff_group in sta_data.get("signoff", []): for corner_name, rcx_corner_names in signoff_group.items(): - liberty = find_liberty_corner(sta_data, corner_name) + liberty = liberty_by_corner.get(corner_name) if liberty is None: workspace.logger.error("No liberty corner '%s' found in %s", corner_name, @@ -920,28 +859,19 @@ def collect_signoff_items() -> list[dict]: return [] temperature = liberty.get("temperature") - liberty_files = [ - normalize_liberty_path(resolve_config_path(path)) - for path in liberty.get("path", []) - ] + liberty_files = liberty.get("path", []) for rcx_corner_name in rcx_corner_names: - rcx_corner = find_rcx_corner(rcx_data, rcx_corner_name) - if rcx_corner is None: - workspace.logger.error("No RCX corner '%s' found in %s", - rcx_corner_name, - workspace.config.get(StepEnum.RCX.value, "")) - return [] - - spef_file = normalize_spef_path( - resolve_config_path(find_spef_for_temp(rcx_corner, temperature)) - ) items.append({ "corner": corner_name, "temperature": temperature, "rcx_corner": rcx_corner_name, "liberty_files": liberty_files, - "spef_file": spef_file, + "spef_file": os.path.join( + rcx_output_dir, + f"{workspace.design.name}_{rcx_corner_name}_" + f"{temperature_token(temperature)}C.spef", + ), }) return items @@ -955,94 +885,101 @@ def collect_signoff_items() -> list[dict]: step=step, ecc_module = ecc_module) - if ecc_module is not None: - sub_flow.update_step(step_name=EccSubFlowEnum.load_data.value, state=StateEnum.Success) - - signoff_items = collect_signoff_items() - if len(signoff_items) <= 0: - workspace.logger.error("No signoff STA items found") + if ecc_module is None: + return result + + sub_flow.update_step(step_name=EccSubFlowEnum.load_data.value, state=StateEnum.Success) + + signoff_items = collect_signoff_items() + if not signoff_items: + workspace.logger.error("No signoff STA items found") + sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, + state=StateEnum.Imcomplete) + return False + + if not os.path.exists(workspace.pdk.sdc): + workspace.logger.error("STA SDC does not exist: %s", + workspace.pdk.sdc) + sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, + state=StateEnum.Imcomplete) + return False + + for signoff_item in signoff_items: + corner_name = signoff_item["corner"] + temperature = signoff_item["temperature"] + rcx_corner_name = signoff_item["rcx_corner"] + liberty_files = signoff_item["liberty_files"] + spef_file = signoff_item["spef_file"] + + if not os.path.exists(spef_file): + workspace.logger.error( + "STA SPEF does not exist for %s/%s at %sC: %s", + corner_name, + rcx_corner_name, + temperature, + spef_file, + ) sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, state=StateEnum.Imcomplete) return False - if not os.path.exists(workspace.pdk.sdc): - workspace.logger.error("STA SDC does not exist: %s", - workspace.pdk.sdc) + missing_liberty_files = [ + lib_path for lib_path in liberty_files + if not os.path.exists(lib_path) + ] + if len(liberty_files) <= 0 or missing_liberty_files: + workspace.logger.error( + "STA liberty does not exist for %s: %s; missing: %s", + corner_name, + liberty_files, + missing_liberty_files, + ) sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, state=StateEnum.Imcomplete) return False - for signoff_item in signoff_items: - corner_name = signoff_item["corner"] - temperature = signoff_item["temperature"] - rcx_corner_name = signoff_item["rcx_corner"] - liberty_files = signoff_item["liberty_files"] - spef_file = signoff_item["spef_file"] - - if not os.path.exists(spef_file): - workspace.logger.error( - "STA SPEF does not exist for %s/%s at %sC: %s", - corner_name, - rcx_corner_name, - temperature, - spef_file, - ) - sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, - state=StateEnum.Imcomplete) - return False - - if len(liberty_files) <= 0 or any(not os.path.exists(lib_path) for lib_path in liberty_files): - workspace.logger.error( - "STA liberty does not exist for %s: %s", - corner_name, - liberty_files, - ) - sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, - state=StateEnum.Imcomplete) - return False - - report_corner_dir = f"{corner_name}_{temperature_token(temperature)}" - report_dir = os.path.join( - step.output.get("dir", ""), - safe_dir_name(report_corner_dir), - safe_dir_name(rcx_corner_name), - ) - os.makedirs(report_dir, exist_ok=True) - - try: - ecc_module.update_sta_data_config( - db_config=workspace.config.get("db", ""), - output_dir=step.output.get("dir", ""), - lib_paths=liberty_files, - sdc_path=workspace.pdk.sdc, - ) - ecc_module.release_sta() - ecc_module.init_sta(output_dir=report_dir, - top_module=workspace.design.top_module, - lib_paths=liberty_files, - sdc_path=workspace.pdk.sdc) - ecc_module.read_spef(file_name=spef_file) - ecc_module.report_timing() - finally: - ecc_module.release_sta() + report_corner_dir = f"{corner_name}_{temperature_token(temperature)}" + report_dir = os.path.join( + step.output.get("dir", ""), + safe_dir_name(report_corner_dir), + safe_dir_name(rcx_corner_name), + ) + os.makedirs(report_dir, exist_ok=True) - workspace.logger.info( - "STA report for %s/%s at %sC saved to %s", - corner_name, - rcx_corner_name, - temperature, - report_dir, + try: + ecc_module.update_sta_data_config( + db_config=workspace.config.get("db", ""), + output_dir=step.output.get("dir", ""), + lib_paths=liberty_files, + sdc_path=workspace.pdk.sdc, ) + ecc_module.release_sta() + ecc_module.init_sta(output_dir=report_dir, + top_module=workspace.design.top_module, + lib_paths=liberty_files, + sdc_path=workspace.pdk.sdc) + ecc_module.read_spef(file_name=spef_file) + ecc_module.report_timing() + finally: + ecc_module.release_sta() + + workspace.logger.info( + "STA report for %s/%s at %sC saved to %s", + corner_name, + rcx_corner_name, + temperature, + report_dir, + ) - sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, state=StateEnum.Success) - - result = save_data(workspace=workspace, - step=step, - ecc_module=ecc_module, - feature_step=False, - report_timing=False) - - sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, - state=StateEnum.Success) + sub_flow.update_step(step_name=EccSubFlowEnum.run_sta.value, state=StateEnum.Success) + + result = save_data(workspace=workspace, + step=step, + ecc_module=ecc_module, + feature_step=False, + report_timing=False) + + sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, + state=StateEnum.Success) return result diff --git a/chipcompiler/tools/yosys/builder.py b/chipcompiler/tools/yosys/builder.py index 5cdd6b1e..98c331b8 100644 --- a/chipcompiler/tools/yosys/builder.py +++ b/chipcompiler/tools/yosys/builder.py @@ -3,6 +3,7 @@ import os import stat from chipcompiler.data import WorkspaceStep, Workspace +from chipcompiler.utility import json_read def _tcl_quote(value: str) -> str: @@ -22,19 +23,33 @@ def _abspath(path: str) -> str: return os.path.abspath(path) -def _split_stdcell_macro_libs(lib_files: list[str]) -> tuple[list[str], list[str]]: - """Split known ICS55 stdcell libraries from macro libraries.""" - stdcell_libs = [] - macro_libs = [] +def _existing_unique_paths(paths: list[str]) -> list[str]: + """Return existing paths in input order without duplicates.""" + unique_paths = [] + seen = set() - for lib in lib_files: - name = os.path.basename(lib) - if name.startswith("ICS55_"): - macro_libs.append(lib) - else: - stdcell_libs.append(lib) + for path in paths: + if not path or not os.path.isfile(path): + continue + abs_path = os.path.abspath(path) + if abs_path in seen: + continue + unique_paths.append(abs_path) + seen.add(abs_path) - return stdcell_libs, macro_libs + return unique_paths + + +def _workspace_libs(workspace: Workspace) -> list[str]: + """Read extra liberty files from the workspace DB config.""" + db_config = json_read(workspace.config.get("db", "")) + lib_paths = db_config.get("INPUT", {}).get("lib_path", []) + if isinstance(lib_paths, str): + lib_paths = [lib_paths] + if not isinstance(lib_paths, list): + return [] + + return _existing_unique_paths(lib_paths) def generate_global_var_tcl(workspace: Workspace, @@ -84,9 +99,9 @@ def generate_global_var_tcl(workspace: Workspace, abc_driver_cell = pdk.abc_driver_cell if pdk.abc_driver_cell else "BUFX4H7L" abc_load = pdk.abc_load if pdk.abc_load else 0.015 - lib_files = workspace.pdk.libs if workspace.pdk.libs else [] - lib_stdcell, lib_macro = _split_stdcell_macro_libs(lib_files) - lib_files_all = " ".join(lib_stdcell + lib_macro) if lib_files else "" + lib_stdcell = _existing_unique_paths(workspace.pdk.libs if workspace.pdk.libs else []) + lib_files = _existing_unique_paths(lib_stdcell + _workspace_libs(workspace)) + lib_files_all = " ".join(lib_files) if lib_files else "" lib_stdcell_all = " ".join(lib_stdcell) if lib_stdcell else "" filelist = workspace.design.input_filelist if workspace.design.input_filelist else workspace.parameters.data.get("File list", "") diff --git a/test/test_ecc_tools_module.py b/test/test_ecc_tools_module.py index 7748a784..52387646 100644 --- a/test/test_ecc_tools_module.py +++ b/test/test_ecc_tools_module.py @@ -27,10 +27,19 @@ def test_init_rcx_passes_pdk_when_configured(): assert module.ecc.calls == [{"config": "/tmp/rcx.json", "pdk": "ics55"}] -def test_init_rcx_omits_empty_pdk_for_backward_compatibility(): +def test_init_rcx_defaults_to_ics55_pdk(): module = ECCToolsModule.__new__(ECCToolsModule) module.ecc = FakeEcc() assert module.init_rcx(config="/tmp/rcx.json") is True + assert module.ecc.calls == [{"config": "/tmp/rcx.json", "pdk": "ics55"}] + + +def test_init_rcx_omits_explicit_empty_pdk_for_backward_compatibility(): + module = ECCToolsModule.__new__(ECCToolsModule) + module.ecc = FakeEcc() + + assert module.init_rcx(config="/tmp/rcx.json", pdk="") is True + assert module.ecc.calls == [{"config": "/tmp/rcx.json"}]