From f489b6107dca4f6a890dfdefdbba0f75b9e953db Mon Sep 17 00:00:00 2001 From: Yell-walkalone <12112088@qq.com> Date: Thu, 18 Jun 2026 09:20:04 +0800 Subject: [PATCH 01/10] update checklist --- chipcompiler/data/checklist.py | 54 +- chipcompiler/data/home.py | 5 +- chipcompiler/thirdparty/ecc-dreamplace | 2 +- chipcompiler/thirdparty/ecc-tools | 2 +- chipcompiler/tools/ecc/builder.py | 2 +- chipcompiler/tools/ecc/checklist.py | 1231 ++++++++++++++++- chipcompiler/tools/ecc/runner.py | 49 +- chipcompiler/tools/ecc_dreamplace/builder.py | 3 + .../tools/ecc_dreamplace/checklist.py | 432 ++++++ chipcompiler/tools/ecc_dreamplace/runner.py | 20 +- chipcompiler/tools/yosys/checklist.py | 215 ++- 11 files changed, 1939 insertions(+), 76 deletions(-) create mode 100644 chipcompiler/tools/ecc_dreamplace/checklist.py diff --git a/chipcompiler/data/checklist.py b/chipcompiler/data/checklist.py index fc651510..c7c26002 100644 --- a/chipcompiler/data/checklist.py +++ b/chipcompiler/data/checklist.py @@ -7,7 +7,7 @@ class CheckState(Enum): """checklist state""" Unstart = "Unstart" # checked unstart - Success = "Success" # checked success + Passed = "Passed" # checked passed Failed = "Failed" # checked Failed Warning = "Warning" # checked Warning @@ -17,7 +17,7 @@ class Checklist: """ def __init__(self, path : str): self.path : str = path # checklist file path - self.header = ["step", "type", "item", "state"] + self.header = ["step", "type", "item", "state", "info"] self.data : dict = {} # checklist data if os.path.exists(self.path): @@ -30,15 +30,50 @@ def __init__(self, path : str): if len(self.data) == 0: self.data["path"] = path self.data["checklist"] = [] + else: + for check_item in self.data.get("checklist", []): + check_item.setdefault("info", "") def save(self): json_write(self.path, self.data) + + def state_value(self, + state : str | CheckState) -> str: + return state.value if isinstance(state, CheckState) else state + + def check_info(self, + state : str | CheckState, + item : str, + info : str = "") -> str: + state_value = self.state_value(state) + if info or state_value not in (CheckState.Failed.value, CheckState.Warning.value): + return info + + return f"{state_value}: {item} check needs attention" + + def state_statistics(self) -> dict: + statistics = { + state.value: 0 + for state in CheckState + } + checklist = self.data.get("checklist", []) + + for check_item in checklist: + state = check_item.get("state", "") + if state in statistics: + statistics[state] += 1 + + return { + "total": len(checklist), + **statistics, + } def add(self, step : str, type : str, item : str, - state : str): + state : str, + info : str = ""): # check if exist for check_item in self.data.get("checklist", []): if check_item["step"] == step and check_item["type"] == type and check_item["item"] == item: @@ -49,7 +84,8 @@ def add(self, "step" : step, "type" : type, "item" : item, - "state" : state + "state" : self.state_value(state), + "info" : self.check_info(state=state, item=item, info=info) } self.data["checklist"].append(check_item) @@ -59,11 +95,13 @@ def update(self, step : str, type : str, item : str, - state : str | CheckState): + state : str | CheckState, + info : str = ""): # check if exist for check_item in self.data.get("checklist", []): if check_item["step"] == step and check_item["type"] == type and check_item["item"] == item: - check_item["state"] = state.value if isinstance(state, CheckState) else state + check_item["state"] = self.state_value(state) + check_item["info"] = self.check_info(state=state, item=item, info=info) self.save() return @@ -71,5 +109,5 @@ def update(self, self.add(step=step, type=type, item=item, - state=state) - \ No newline at end of file + state=self.state_value(state), + info=info) diff --git a/chipcompiler/data/home.py b/chipcompiler/data/home.py index 4d2b4b65..bb85069b 100644 --- a/chipcompiler/data/home.py +++ b/chipcompiler/data/home.py @@ -255,6 +255,7 @@ def update_checklist(self, step : str, type : str, item : str, - state : str): + state : str, + info : str = ""): checklist = Checklist(path=self.data.get("checklist", "")) - checklist.update(step=step, type=type, item=item, state=state) + checklist.update(step=step, type=type, item=item, state=state, info=info) diff --git a/chipcompiler/thirdparty/ecc-dreamplace b/chipcompiler/thirdparty/ecc-dreamplace index 6c638fa0..d934bb86 160000 --- a/chipcompiler/thirdparty/ecc-dreamplace +++ b/chipcompiler/thirdparty/ecc-dreamplace @@ -1 +1 @@ -Subproject commit 6c638fa0c369dcb53b6a851df5a65abe44c7a061 +Subproject commit d934bb8612c9a5246ba6ba7f5e6fc3fc6b4ddd75 diff --git a/chipcompiler/thirdparty/ecc-tools b/chipcompiler/thirdparty/ecc-tools index 6c1163e7..c21b8d26 160000 --- a/chipcompiler/thirdparty/ecc-tools +++ b/chipcompiler/thirdparty/ecc-tools @@ -1 +1 @@ -Subproject commit 6c1163e7af0db697cfb4fb4e947ec9378f8da103 +Subproject commit c21b8d26cdb28a2af80b3f0bb623d1e7730d012d diff --git a/chipcompiler/tools/ecc/builder.py b/chipcompiler/tools/ecc/builder.py index fcab6e48..7fdfd2ad 100644 --- a/chipcompiler/tools/ecc/builder.py +++ b/chipcompiler/tools/ecc/builder.py @@ -43,7 +43,7 @@ def build_step(workspace: Workspace, if output_def is None: output_def = f"{step.directory}/output/{workspace.design.name}_{step.name}.def.gz" if output_verilog is None: - output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v" + output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v.gz" if output_gds is None: output_gds = f"{step.directory}/output/{workspace.design.name}_{step.name}.gds" output_db = f"{step.directory}/output/{workspace.design.name}_{step.name}_db" diff --git a/chipcompiler/tools/ecc/checklist.py b/chipcompiler/tools/ecc/checklist.py index 48ff1d03..30f70fe4 100644 --- a/chipcompiler/tools/ecc/checklist.py +++ b/chipcompiler/tools/ecc/checklist.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- +import glob +import os + from chipcompiler.data import ( Workspace, WorkspaceStep, @@ -7,49 +10,172 @@ StepEnum, CheckState ) +from chipcompiler.utility import json_read class EccChecklist: - def __init__(self, workspace : Workspace, workspace_step: WorkspaceStep): + CHECKLIST_ITEMS = { + StepEnum.FLOORPLAN: [ + ("Area", "check DIE area"), + ("Area", "check core area"), + ("Area", "check core utilization"), + ("Rows/Tracks", "check placement rows and sites"), + ("Rows/Tracks", "check routing tracks"), + ("Pins/Macros", "check IO pin placement"), + ("Pins/Macros", "check macro placement"), + ("PDN", "check tap and endcap insertion"), + ("PDN", "check PDN IO and global connect"), + ("PDN", "check PDN grid and stripes"), + ("Clock", "check clock net type"), + ], + StepEnum.NETLIST_OPT: [ + ("Fanout", "check max fanout constraint"), + ("Fanout", "check high fanout nets"), + ("Buffer", "check inserted buffer type"), + ("Tie", "check tie cell usage"), + ("Netlist", "check netlist and DEF consistency"), + ], + StepEnum.PLACEMENT: [ + ("Density", "check target density"), + ("Density", "check placement overflow"), + ("Wirelength", "check HPWL"), + ("Legality", "check cell overlap"), + ("Congestion", "check placement congestion"), + ], + StepEnum.CTS: [ + ("Clock", "check clock net"), + ("Buffer", "check CTS buffers"), + ("Timing", "check clock skew"), + ("Timing", "check clock transition"), + ("Timing", "check clock capacitance"), + ("Tree", "check clock sink coverage"), + ], + StepEnum.TIMING_OPT_DRV: [ + ("Timing", "check max transition"), + ("Timing", "check max capacitance"), + ("Timing", "check max fanout"), + ("Buffer", "check DRV inserted buffers"), + ], + StepEnum.TIMING_OPT_HOLD: [ + ("Timing", "check hold WNS/TNS"), + ("Buffer", "check hold inserted buffers"), + ("Netlist", "check hold ECO consistency"), + ], + StepEnum.TIMING_OPT_SETUP: [ + ("Timing", "check setup WNS/TNS"), + ("Buffer", "check setup inserted buffers"), + ("Netlist", "check setup ECO consistency"), + ], + StepEnum.LEGALIZATION: [ + ("Legality", "check cell overlap"), + ("Legality", "check off-row placement"), + ("Legality", "check site alignment"), + ("Movement", "check legalization movement"), + ("Fixed", "check fixed instances"), + ], + StepEnum.ROUTING: [ + ("Layer", "check routing layer range"), + ("Route", "check unrouted nets"), + ("Route", "check shorts and opens"), + ("Route", "check via count"), + ("Route", "check wire length"), + ("Timing", "check post-route timing"), + ], + StepEnum.DRC: [ + ("DRC", "check DRC violation count"), + ("DRC", "check DRC violation distribution"), + ("DRC", "check DRC waiver list"), + ("Signoff", "check final DRC requirement"), + ], + StepEnum.FILLER: [ + ("Filler", "check filler cell list"), + ("Filler", "check filler coverage"), + ("Legality", "check filler overlap"), + ("Signoff", "check post-filler DRC requirement"), + ], + StepEnum.RCX: [ + ("RCX", "check RCX corners"), + ("RCX", "check SPEF files"), + ("RCX", "check SPEF net names"), + ("STA", "check RCX and STA corner mapping"), + ], + StepEnum.STA: [ + ("STA", "check STA signoff matrix"), + ("Timing", "check setup timing"), + ("Timing", "check hold timing"), + ("Timing", "check frequency requirement"), + ("Timing", "check timing exceptions"), + ("DRV", "check STA DRV violations"), + ], + StepEnum.HARDEN: [ + ("Output", "check abstract LEF"), + ("Output", "check timing model LIB"), + ("Output", "check harden GDS"), + ("Output", "check hard macro deliverables"), + ], + } + + def __init__(self, + workspace : Workspace, + workspace_step: WorkspaceStep, + init_checklist : bool = True): self.workspace = workspace self.workspace_step = workspace_step - self.build_checklist() + if init_checklist: + self.build_checklist() + + def add_item(self, + checklist : Checklist, + step : str, + type : str, + item : str, + state : str, + info : str = ""): + checklist.add(step=step, + type=type, + item=item, + state=state, + info=info) + + # add to home page checklist + self.workspace.home.update_checklist(step=step, + type=type, + item=item, + state=state, + info=info) + + def add_items(self, + checklist : Checklist, + step : StepEnum): + for type, item in self.CHECKLIST_ITEMS.get(step, []): + self.add_item(checklist=checklist, + step=step.value, + type=type, + item=item, + state=CheckState.Unstart.value) + + def set_item_state(self, + step : str, + type : str, + item : str, + state : CheckState, + info : str = ""): + self.update_item(step=step, + type=type, + item=item, + state=state, + info=info) + self.workspace.home.update_checklist(step=step, + type=type, + item=item, + state=state.value, + info=info) def build_checklist(self) -> list: checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) step = StepEnum(self.workspace_step.name) - match step: - case StepEnum.FLOORPLAN: - checklist.add(step=step.value, - type="Area", - item="check DIE area", - state=CheckState.Unstart.value) - - # add to home page checklist - self.workspace.home.update_checklist(step=step.value, - type="Area", - item="check DIE area", - state=CheckState.Unstart.value) - case StepEnum.NETLIST_OPT: - pass - case StepEnum.PLACEMENT: - pass - case StepEnum.CTS: - pass - case StepEnum.TIMING_OPT_DRV: - pass - case StepEnum.TIMING_OPT_HOLD: - pass - case StepEnum.TIMING_OPT_SETUP: - pass - case StepEnum.LEGALIZATION: - pass - case StepEnum.ROUTING: - pass - case StepEnum.FILLER: - pass - case StepEnum.DRC: - pass + self.add_items(checklist=checklist, + step=step) self.workspace_step.checklist["checklist"] = checklist.data @@ -61,12 +187,1041 @@ def update_item(self, step : str, type : str, item : str, - state : str | CheckState): + state : str | CheckState, + info : str = ""): checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) checklist.update(step=step, type=type, item=item, - state=state) + state=state, + info=info) + + def check(self) -> bool: + step = StepEnum(self.workspace_step.name) + checker_class = { + StepEnum.FLOORPLAN: EccFloorplanChecklist, + StepEnum.NETLIST_OPT: EccNetlistOptChecklist, + StepEnum.CTS: EccCtsChecklist, + StepEnum.TIMING_OPT_DRV: EccTimingOptDrvChecklist, + StepEnum.TIMING_OPT_HOLD: EccTimingOptHoldChecklist, + StepEnum.TIMING_OPT_SETUP: EccTimingOptSetupChecklist, + StepEnum.ROUTING: EccRoutingChecklist, + StepEnum.DRC: EccDrcChecklist, + StepEnum.FILLER: EccFillerChecklist, + StepEnum.HARDEN: EccHardenChecklist, + StepEnum.RCX: EccRcxChecklist, + StepEnum.STA: EccStaChecklist, + }.get(step) + if checker_class is None: + return True + + return checker_class( + self.workspace, + self.workspace_step, + init_checklist=False, + ).check() - def check(self): - pass \ No newline at end of file + def check_file(self, + path : str, + text_tokens : list | None = None) -> bool: + if not path or not os.path.isfile(path) or os.path.getsize(path) <= 0: + return False + + if not text_tokens: + return True + + try: + with open(path, "r", encoding="utf-8", errors="ignore") as file: + content = file.read() + except OSError: + return False + + return all(token in content for token in text_tokens) + + def to_float(self, + value, + default : float | None = None) -> float | None: + try: + return float(value) + except (TypeError, ValueError): + return default + + +class EccFloorplanChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.FLOORPLAN.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + db = json_read(self.workspace_step.feature.get("db", "")) + subflow = json_read(self.workspace_step.subflow.get("path", "")) + + try: + with open(self.workspace_step.log.get("file", ""), + "r", encoding="utf-8", errors="ignore") as file: + log_text = file.read() + except OSError: + log_text = "" + + layout = db.get("Design Layout", {}) + statis = db.get("Design Statis", {}) + layers = db.get("Layers", {}) + nets = db.get("Nets", {}) + subflow_state = { + item.get("name"): item.get("state") + for item in subflow.get("steps", []) + } + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + + die_area = self.to_float( + metrics.get("Die area [μm^2]", layout.get("die_area")), 0.0) + die_width = self.to_float( + metrics.get("Die width [um]", layout.get("die_bounding_width")), 0.0) + die_height = self.to_float( + metrics.get("Die height [um]", layout.get("die_bounding_height")), 0.0) + core_area = self.to_float(layout.get("core_area"), 0.0) + core_width = self.to_float(layout.get("core_bounding_width"), 0.0) + core_height = self.to_float(layout.get("core_bounding_height"), 0.0) + core_util = self.to_float( + metrics.get("Core util", layout.get("core_usage")), 0.0) + num_iopins = self.to_float( + metrics.get("Total io pins", statis.get("num_iopins")), 0.0) + num_pdn = self.to_float(statis.get("num_pdn"), 0.0) + num_clock = self.to_float(nets.get("num_clock"), 0.0) + num_routing_layers = self.to_float( + layers.get("num_layers_routing", statis.get("num_layers_routing")), + 0.0, + ) + + checks = [ + ("Area", "check DIE area", + die_area > 0 and die_width > 0 and die_height > 0), + ("Area", "check core area", + core_area > 0 and core_width > 0 and core_height > 0), + ("Area", "check core utilization", + core_util is not None and 0 < core_util <= 1), + ("Rows/Tracks", "check placement rows and sites", + subflow_state.get("init floorplan") == "Success" + and "Write ROWS success" in log_text + and output_success), + ("Rows/Tracks", "check routing tracks", + subflow_state.get("create tracks") == "Success" + and num_routing_layers > 0 + and "Write Track Grid success" in log_text), + ("Pins/Macros", "check IO pin placement", + subflow_state.get("place io pins") == "Success" + and num_iopins > 0 + and "Write PINS success" in log_text), + ("Pins/Macros", "check macro placement", + "Macros" in db and "Macros Statis" in db and output_success), + ("PDN", "check tap and endcap insertion", + subflow_state.get("tap cell") == "Success" + and "Write COMPONENTS success" in log_text), + ("PDN", "check PDN IO and global connect", + subflow_state.get("PDN") == "Success" and num_pdn >= 2), + ("PDN", "check PDN grid and stripes", + subflow_state.get("PDN") == "Success" + and "Write SPECIALNETS success" in log_text), + ("Clock", "check clock net type", + subflow_state.get("set clock net") == "Success" and num_clock > 0), + ] + + warning_items = {"check macro placement"} + for type, item, success in checks: + warning = item in warning_items + state = CheckState.Passed + if not success: + state = CheckState.Warning if warning else CheckState.Failed + self.set_item_state( + step=step, + type=type, + item=item, + state=state, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success or item in warning_items for _, item, success in checks) + + +class EccNetlistOptChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.NETLIST_OPT.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + db = json_read(self.workspace_step.feature.get("db", "")) + config = json_read(self.workspace.config.get(StepEnum.NETLIST_OPT.value, "")) + + try: + with open(self.workspace_step.output.get("verilog", ""), + "r", encoding="utf-8", errors="ignore") as file: + netlist_text = file.read() + except OSError: + netlist_text = "" + + statis = db.get("Design Statis", {}) + pins = db.get("Pins", {}) + buffer_cells = getattr(self.workspace.pdk, "buffers", []) or [] + tie_high = getattr(self.workspace.pdk, "tie_high_cell", "") + tie_low = getattr(self.workspace.pdk, "tie_low_cell", "") + max_fanout_limit = self.to_float( + config.get("max_fanout", + metrics.get("Max fanout", + self.workspace.parameters.data.get("Max fanout"))), + 0.0, + ) + actual_max_fanout = self.to_float(pins.get("max_fanout")) + total_nets = self.to_float(metrics.get("Total nets"), 0.0) + db_nets = self.to_float(statis.get("num_nets"), 0.0) + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + + buffer_success = ( + len(buffer_cells) > 0 + and ( + any(buffer in netlist_text for buffer in buffer_cells) + or config.get("insert_buffer") in buffer_cells + ) + ) + tie_success = bool(tie_high and tie_low) + if tie_high in netlist_text or tie_low in netlist_text: + tie_success = True + + checks = [ + ("Fanout", "check max fanout constraint", max_fanout_limit > 0), + ("Fanout", "check high fanout nets", + actual_max_fanout is not None + and max_fanout_limit > 0 + and actual_max_fanout <= max_fanout_limit), + ("Buffer", "check inserted buffer type", buffer_success), + ("Tie", "check tie cell usage", tie_success), + ("Netlist", "check netlist and DEF consistency", + output_success and total_nets > 0 and db_nets > 0 + and int(total_nets) == int(db_nets)), + ] + + warning_items = {"check high fanout nets", "check tie cell usage"} + for type, item, success in checks: + warning = item in warning_items + state = CheckState.Passed + if not success: + state = CheckState.Warning if warning else CheckState.Failed + info = f"{item} check failed" + if item == "check high fanout nets": + info = ( + f"max fanout {actual_max_fanout} exceeds " + f"limit {max_fanout_limit}" + ) + elif item == "check tie cell usage": + info = "tie high/low cell definition or inserted tie cell is missing" + self.set_item_state( + step=step, + type=type, + item=item, + state=state, + info="" if success else info, + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success or item in warning_items for _, item, success in checks) + + +class EccCtsChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.CTS.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + db = json_read(self.workspace_step.feature.get("db", "")) + feature = json_read(self.workspace_step.feature.get("step", "")).get("CTS", {}) + config = json_read(self.workspace.config.get(StepEnum.CTS.value, "")) + + nets = db.get("Nets", {}) + instances = db.get("Instances", {}) + clock_instances = instances.get("clock", {}) or {} + buffer_cells = config.get("buffer_type", []) + if isinstance(buffer_cells, str): + buffer_cells = [buffer_cells] + + num_clock = self.to_float(nets.get("num_clock"), 0.0) + clock_sink_num = self.to_float(clock_instances.get("num"), 0.0) + buffer_num = self.to_float( + metrics.get("buffer_num", feature.get("buffer_num")), 0.0) + clock_path_max = self.to_float( + metrics.get("clock_path_max_buffer", + feature.get("clock_path_max_buffer")), 0.0) + clock_path_min = self.to_float( + metrics.get("clock_path_min_buffer", + feature.get("clock_path_min_buffer")), 0.0) + clock_wirelength = self.to_float( + metrics.get("total_clock_wirelength", + feature.get("total_clock_wirelength")), 0.0) + skew_bound = self.to_float(config.get("skew_bound"), 0.0) + max_transition = self.to_float( + config.get("max_buf_tran", config.get("max_sink_tran")), 0.0) + max_cap = self.to_float(config.get("max_cap"), 0.0) + + checks = [ + ("Clock", "check clock net", num_clock > 0), + ("Buffer", "check CTS buffers", + len(buffer_cells) > 0 and buffer_num > 0), + ("Timing", "check clock skew", + skew_bound > 0 and clock_path_max >= clock_path_min > 0), + ("Timing", "check clock transition", + max_transition > 0 and buffer_num >= 0), + ("Timing", "check clock capacitance", + max_cap > 0 and clock_wirelength > 0), + ("Tree", "check clock sink coverage", + clock_sink_num > 0 and clock_path_max > 0 and clock_path_min > 0), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) + + +class EccTimingOptDrvChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.TIMING_OPT_DRV.value + db = json_read(self.workspace_step.feature.get("db", "")) + config = json_read(self.workspace.config.get(StepEnum.TIMING_OPT_DRV.value, "")) + + try: + with open(self.workspace_step.log.get("file", ""), + "r", encoding="utf-8", errors="ignore") as file: + log_text = file.read().lower() + except OSError: + log_text = "" + + pins = db.get("Pins", {}) + buffer_cells = config.get("DRV_insert_buffers", []) + if isinstance(buffer_cells, str): + buffer_cells = [buffer_cells] + + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + log_success = not any( + token in log_text + for token in ["error:", "fatal", "traceback", "exception", "failed"] + ) + max_allowed_fanout = self.to_float( + config.get("max_allowed_buffering_fanout"), 0.0) + actual_max_fanout = self.to_float(pins.get("max_fanout")) + + checks = [ + ("Timing", "check max transition", + bool(config.get("optimize_drv")) and output_success and log_success), + ("Timing", "check max capacitance", + bool(config.get("optimize_drv")) and output_success and log_success), + ("Timing", "check max fanout", + max_allowed_fanout > 0 + and (actual_max_fanout is None + or actual_max_fanout <= max_allowed_fanout)), + ("Buffer", "check DRV inserted buffers", + len(buffer_cells) > 0 and output_success), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) + + +class EccTimingOptHoldChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.TIMING_OPT_HOLD.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + db = json_read(self.workspace_step.feature.get("db", "")) + config = json_read(self.workspace.config.get(StepEnum.TIMING_OPT_HOLD.value, "")) + + buffer_cells = config.get("hold_insert_buffers", []) + if isinstance(buffer_cells, str): + buffer_cells = [buffer_cells] + + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + min_wns = self.to_float(metrics.get("min_WNS")) + min_tns = self.to_float(metrics.get("min_TNS")) + if min_wns is None and min_tns is None: + timing_success = output_success and bool(config.get("optimize_hold")) + else: + timing_success = ( + min_wns is not None and min_tns is not None + and min_wns >= 0 and min_tns >= 0 + ) + statis = db.get("Design Statis", {}) + num_instances = self.to_float(statis.get("num_instances"), 0.0) + num_nets = self.to_float(statis.get("num_nets"), 0.0) + + checks = [ + ("Timing", "check hold WNS/TNS", timing_success), + ("Buffer", "check hold inserted buffers", + len(buffer_cells) > 0 and output_success), + ("Netlist", "check hold ECO consistency", + output_success and num_instances > 0 and num_nets > 0), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) + + +class EccTimingOptSetupChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.TIMING_OPT_SETUP.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + db = json_read(self.workspace_step.feature.get("db", "")) + config = json_read(self.workspace.config.get(StepEnum.TIMING_OPT_SETUP.value, "")) + + buffer_cells = config.get("setup_insert_buffers", []) + if isinstance(buffer_cells, str): + buffer_cells = [buffer_cells] + + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + max_wns = self.to_float(metrics.get("max_WNS")) + max_tns = self.to_float(metrics.get("max_TNS")) + if max_wns is None and max_tns is None: + timing_success = output_success and bool(config.get("optimize_setup")) + else: + timing_success = ( + max_wns is not None and max_tns is not None + and max_wns >= 0 and max_tns >= 0 + ) + statis = db.get("Design Statis", {}) + num_instances = self.to_float(statis.get("num_instances"), 0.0) + num_nets = self.to_float(statis.get("num_nets"), 0.0) + + checks = [ + ("Timing", "check setup WNS/TNS", timing_success), + ("Buffer", "check setup inserted buffers", + len(buffer_cells) > 0 and output_success), + ("Netlist", "check setup ECO consistency", + output_success and num_instances > 0 and num_nets > 0), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) + + +class EccRoutingChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.ROUTING.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + db = json_read(self.workspace_step.feature.get("db", "")) + feature = json_read(self.workspace_step.feature.get("step", "")).get( + StepEnum.ROUTING.value, {}) + config = json_read(self.workspace.config.get(StepEnum.ROUTING.value, "")) + + layers = db.get("Layers", {}) + nets = db.get("Nets", {}) + rt_config = config.get("RT", {}) + routing_layer_names = [ + layer.get("layer_name") + for layer in layers.get("routing_layers", []) + ] + bottom_layer = rt_config.get("-bottom_routing_layer") + top_layer = rt_config.get("-top_routing_layer") + dr_iterations = feature.get("DR", []) + final_dr = dr_iterations[-1] if dr_iterations else {} + final_violation_num = self.to_float( + final_dr.get("total_violation_num"), 0.0) + total_nets = self.to_float(nets.get("num_total"), 0.0) + wire_len = self.to_float( + metrics.get("wire_len", nets.get("wire_len")), 0.0) + via_num = self.to_float( + metrics.get("num_via", nets.get("num_via")), 0.0) + timing_enabled = str(rt_config.get("-enable_timing", "0")) == "1" + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + + checks = [ + ("Layer", "check routing layer range", + bottom_layer in routing_layer_names + and top_layer in routing_layer_names), + ("Route", "check unrouted nets", + output_success and total_nets > 0 and len(dr_iterations) > 0), + ("Route", "check shorts and opens", + final_violation_num == 0), + ("Route", "check via count", via_num > 0), + ("Route", "check wire length", wire_len > 0), + ("Timing", "check post-route timing", + (not timing_enabled) + or "Frequency [MHz]" in metrics + or "max_WNS" in metrics), + ] + + warning_items = {"check post-route timing"} + for type, item, success in checks: + warning = item in warning_items + state = CheckState.Passed + if not success: + state = CheckState.Warning if warning else CheckState.Failed + self.set_item_state( + step=step, + type=type, + item=item, + state=state, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success or item in warning_items for _, item, success in checks) + + +class EccDrcChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.DRC.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + feature = json_read(self.workspace_step.feature.get("step", "")).get("drc", {}) + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + + metric_drc_num = self.to_float(metrics.get("drc_num")) + feature_drc_num = self.to_float(feature.get("number")) + distribution = feature.get("distribution") + drc_clean = ( + metric_drc_num is not None + and feature_drc_num is not None + and metric_drc_num == 0 + and feature_drc_num == 0 + ) + + checks = [ + ("DRC", "check DRC violation count", drc_clean), + ("DRC", "check DRC violation distribution", + drc_clean or isinstance(distribution, dict)), + ("DRC", "check DRC waiver list", drc_clean), + ("Signoff", "check final DRC requirement", + output_success and drc_clean), + ] + + warning_items = { + "check DRC violation distribution", + "check DRC waiver list", + } + for type, item, success in checks: + warning = item in warning_items + state = CheckState.Passed + if not success: + state = CheckState.Warning if warning else CheckState.Failed + self.set_item_state( + step=step, + type=type, + item=item, + state=state, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success or item in warning_items for _, item, success in checks) + + +class EccFillerChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.FILLER.value + db = json_read(self.workspace_step.feature.get("db", "")) + subflow = json_read(self.workspace_step.subflow.get("path", "")) + config = json_read(self.workspace.config.get(StepEnum.PLACEMENT.value, "")) + + try: + with open(self.workspace_step.log.get("file", ""), + "r", encoding="utf-8", errors="ignore") as file: + log_text = file.read() + except OSError: + log_text = "" + + subflow_state = { + item.get("name"): item.get("state") + for item in subflow.get("steps", []) + } + filler_config = config.get("PL", {}).get("Filler", {}) + first_iter_fillers = filler_config.get("first_iter", []) + second_iter_fillers = filler_config.get("second_iter", []) + pdk_fillers = getattr(self.workspace.pdk, "fillers", []) or [] + output_success = all([ + self.check_file(self.workspace_step.output.get("def", "")), + self.check_file(self.workspace_step.output.get("verilog", "")), + self.check_file(self.workspace_step.output.get("gds", "")), + ]) + statis = db.get("Design Statis", {}) + num_instances = self.to_float(statis.get("num_instances"), 0.0) + log_lower = log_text.lower() + log_success = not any( + token in log_lower + for token in ["error:", "fatal", "traceback", "exception", "failed"] + ) + + checks = [ + ("Filler", "check filler cell list", + len(pdk_fillers) > 0 + or len(first_iter_fillers) > 0 + or len(second_iter_fillers) > 0), + ("Filler", "check filler coverage", + subflow_state.get("run filler") == "Success" + and output_success + and "insertFiller" in log_text), + ("Legality", "check filler overlap", + output_success and num_instances > 0 and log_success), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + drc_state = CheckState.Warning + self.set_item_state( + step=step, + type="Signoff", + item="check post-filler DRC requirement", + state=drc_state, + info="post-filler DRC is not run in current flow", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) + + +class EccHardenChecklist(EccChecklist): + def check(self) -> bool: + step = StepEnum.HARDEN.value + design_name = self.workspace.design.top_module \ + or self.workspace.design.name + + lef_tokens = ["MACRO", "END LIBRARY"] + lib_tokens = ["library", "cell"] + if design_name: + lef_tokens.append(f"MACRO {design_name}") + lib_tokens.append(f"cell ({design_name})") + + checks = [ + ( + "Output", + "check abstract LEF", + self.check_file(self.workspace_step.output.get("lef", ""), + lef_tokens), + ), + ( + "Output", + "check timing model LIB", + self.check_file(self.workspace_step.output.get("lib", ""), + lib_tokens), + ), + ( + "Output", + "check harden GDS", + self.check_file(self.workspace_step.output.get("gds", "")), + ), + ] + + deliverables_success = all(success for _, _, success in checks) + checks.append(( + "Output", + "check hard macro deliverables", + deliverables_success, + )) + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return deliverables_success + + +class EccRcxChecklist(EccChecklist): + def collect_rcx_spef_paths(self) -> list: + spef_paths = self.workspace_step.output.get("spef", []) + if isinstance(spef_paths, str): + spef_paths = [spef_paths] + + output_dir = self.workspace_step.output.get("dir", "") + if output_dir and os.path.isdir(output_dir): + spef_paths.extend(glob.glob(os.path.join(output_dir, "*.spef"))) + + return sorted({ + path + for path in spef_paths + if path + }) + + def expected_rcx_spef_paths(self) -> list: + rcx_config = self.workspace.config.get(StepEnum.RCX.value, "") + rcx_data = json_read(rcx_config) + expected_paths = [] + + for corner in rcx_data.get("corners", []): + spef_files = corner.get("spef_file", []) + if isinstance(spef_files, (str, dict)): + spef_files = [spef_files] + + for spef_item in spef_files: + if isinstance(spef_item, dict): + expected_paths.extend([ + path for path in spef_item.values() if path + ]) + elif spef_item: + expected_paths.append(spef_item) + + return sorted(set(expected_paths)) + + def spef_corner_name(self, + spef_path : str) -> str: + design_name = self.workspace.design.name \ + or self.workspace.design.top_module + name = os.path.basename(spef_path) + if name.endswith(".spef"): + name = name[:-5] + + prefix = f"{design_name}_" if design_name else "" + if prefix and name.startswith(prefix): + name = name[len(prefix):] + + if "_" in name: + name = name.rsplit("_", 1)[0] + + return name + + def sta_required_rcx_corners(self) -> set: + sta_config = self.workspace.config.get(StepEnum.STA.value, "") + sta_data = json_read(sta_config) + corners = set() + + for signoff_group in sta_data.get("signoff", []): + for rcx_corner_names in signoff_group.values(): + corners.update(rcx_corner_names) + + return corners + + def check_spef_file(self, + spef_path : str) -> bool: + design_name = self.workspace.design.name \ + or self.workspace.design.top_module + tokens = ["*SPEF", "*DESIGN", "*NAME_MAP"] + if design_name: + tokens.append(f"*DESIGN \"{design_name}\"") + + return self.check_file(spef_path, tokens) + + def check(self) -> bool: + step = StepEnum.RCX.value + spef_paths = self.collect_rcx_spef_paths() + expected_spef_paths = self.expected_rcx_spef_paths() + required_rcx_corners = self.sta_required_rcx_corners() + extracted_corners = { + self.spef_corner_name(path) + for path in spef_paths + } + + if expected_spef_paths: + spef_files_success = all( + os.path.isfile(path) and os.path.getsize(path) > 0 + for path in expected_spef_paths + ) + corners_success = spef_files_success + else: + spef_files_success = len(spef_paths) > 0 and all( + os.path.isfile(path) and os.path.getsize(path) > 0 + for path in spef_paths + ) + corners_success = len(extracted_corners) > 0 + + spef_net_names_success = len(spef_paths) > 0 and all( + self.check_spef_file(path) + for path in spef_paths + ) + + if required_rcx_corners: + mapping_success = required_rcx_corners.issubset(extracted_corners) + else: + mapping_success = len(extracted_corners) > 0 + + checks = [ + ("RCX", "check RCX corners", corners_success), + ("RCX", "check SPEF files", spef_files_success), + ("RCX", "check SPEF net names", spef_net_names_success), + ("STA", "check RCX and STA corner mapping", mapping_success), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) + + +class EccStaChecklist(EccChecklist): + def temperature_token(self, + temperature) -> str: + try: + numeric = float(temperature) + if numeric.is_integer(): + temperature = int(numeric) + except (TypeError, ValueError): + pass + return str(temperature).replace("-", "m").replace(".", "p") + + def collect_sta_report_paths(self) -> list: + output_dir = self.workspace_step.output.get("dir", "") + if not output_dir or not os.path.isdir(output_dir): + return [] + + return sorted(glob.glob( + os.path.join(output_dir, "**", "*.rpt.json"), + recursive=True, + )) + + def expected_sta_report_paths(self) -> list: + sta_config = self.workspace.config.get(StepEnum.STA.value, "") + sta_data = json_read(sta_config) + if len(sta_data) == 0: + return [] + + output_dir = self.workspace_step.output.get("dir", "") + top_module = self.workspace.design.top_module \ + or self.workspace.design.name + liberty_by_corner = { + liberty.get("corner"): liberty + for liberty in sta_data.get("liberty", []) + } + expected_paths = [] + + for signoff_group in sta_data.get("signoff", []): + for corner_name, rcx_corner_names in signoff_group.items(): + liberty = liberty_by_corner.get(corner_name) + if liberty is None: + continue + + report_corner_dir = "{}_{}".format( + corner_name, + self.temperature_token(liberty.get("temperature")), + ) + for rcx_corner_name in rcx_corner_names: + expected_paths.append(os.path.join( + output_dir, + report_corner_dir, + rcx_corner_name, + f"{top_module}.rpt.json", + )) + + return expected_paths + + def load_sta_reports(self) -> list: + reports = [] + for path in self.collect_sta_report_paths(): + data = json_read(path) + if len(data) > 0: + reports.append((path, data)) + + return reports + + def sta_report_has_violation(self, + data : dict, + delay_type : str) -> bool: + for slack_item in data.get("slack", []): + if slack_item.get("delay_type", "") != delay_type: + continue + + tns = self.to_float(slack_item.get("TNS"), 0.0) + wns = self.to_float(slack_item.get("WNS"), 0.0) + if tns is None or wns is None or tns < 0 or wns < 0: + return True + + return False + + def sta_report_has_delay_type(self, + data : dict, + delay_type : str) -> bool: + return any( + slack_item.get("delay_type", "") == delay_type + for slack_item in data.get("slack", []) + ) + + def sta_report_frequency(self, + data : dict, + target_frequency : float) -> float | None: + max_wns = None + for slack_item in data.get("slack", []): + if slack_item.get("delay_type", "") == "max": + max_wns = self.to_float(slack_item.get("WNS")) + break + + if target_frequency <= 0 or max_wns is None: + return None + + clk_period = 1000.0 / target_frequency + if clk_period - max_wns <= 0: + return None + + return 1000.0 / (clk_period - max_wns) + + def check(self) -> bool: + step = StepEnum.STA.value + reports = self.load_sta_reports() + report_paths = [path for path, _ in reports] + expected_paths = self.expected_sta_report_paths() + target_frequency = self.to_float( + self.workspace.parameters.data.get("Frequency max [MHz]", 0), + 0.0, + ) + + if expected_paths: + signoff_success = all( + os.path.isfile(path) and os.path.getsize(path) > 0 + for path in expected_paths + ) + else: + signoff_success = len(reports) > 0 + + setup_success = len(reports) > 0 and all( + self.sta_report_has_delay_type(data, "max") + and not self.sta_report_has_violation(data, "max") + for _, data in reports + ) + hold_success = len(reports) > 0 and all( + self.sta_report_has_delay_type(data, "min") + and not self.sta_report_has_violation(data, "min") + for _, data in reports + ) + + frequencies = [ + self.sta_report_frequency(data, target_frequency) + for _, data in reports + ] + frequency_success = ( + len(reports) > 0 + and target_frequency > 0 + and all(freq is not None and freq >= target_frequency + for freq in frequencies) + ) + + reports_parse_success = len(report_paths) == len(reports) and len(reports) > 0 + checks = [ + ("STA", "check STA signoff matrix", signoff_success), + ("Timing", "check setup timing", setup_success), + ("Timing", "check hold timing", hold_success), + ("Timing", "check frequency requirement", frequency_success), + ("Timing", "check timing exceptions", reports_parse_success), + ("DRV", "check STA DRV violations", reports_parse_success), + ] + + warning_items = { + "check timing exceptions", + "check STA DRV violations", + } + for type, item, success in checks: + warning = item in warning_items + state = CheckState.Passed + if not success: + state = CheckState.Warning if warning else CheckState.Failed + self.set_item_state( + step=step, + type=type, + item=item, + state=state, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success or item in warning_items for _, item, success in checks) diff --git a/chipcompiler/tools/ecc/runner.py b/chipcompiler/tools/ecc/runner.py index 4100ff0e..efa0bf23 100644 --- a/chipcompiler/tools/ecc/runner.py +++ b/chipcompiler/tools/ecc/runner.py @@ -16,6 +16,20 @@ def create_db_engine(workspace: Workspace, step: WorkspaceStep) -> ECCToolsModule: """""" + def input_path_exists(path: str) -> str | None: + if not path: + return None + + gzip_path = path if path.endswith(".gz") else f"{path}.gz" + plain_path = path[:-3] if path.endswith(".gz") else path + + if os.path.exists(gzip_path): + return gzip_path + if os.path.exists(plain_path): + return plain_path + + return None + def load_data(): ecc_module = ECCToolsModule() @@ -33,6 +47,12 @@ def load_data(): return None def load_design(): + def def_exist() -> str | None: + return input_path_exists(step.input.get("def", "")) + + def verilog_exist() -> str | None: + return input_path_exists(step.input.get("verilog", "")) + ecc_module = ECCToolsModule() ecc_module.init_config(flow_config=workspace.config.get("flow"), @@ -44,12 +64,15 @@ def load_design(): ecc_module.init_lefs(workspace.pdk.lefs) # if db def exist, read db def - if os.path.exists(step.input.get("def", "")): - ecc_module.read_def(step.input.get("def", "")) + def_path = def_exist() + + if def_path is not None: + ecc_module.read_def(def_path) else: #else, read step output verilog - if os.path.exists(step.input.get("verilog", "")): - ecc_module.read_verilog(verilog=step.input.get("verilog", ""), + verilog_path = verilog_exist() + if verilog_path: + ecc_module.read_verilog(verilog=verilog_path, top_module=workspace.design.top_module) else: return None @@ -60,13 +83,11 @@ def is_enable_setup(): # skip synthesis step if step.name == StepEnum.SYNTHESIS.value: return False - - # db_path = step.input.get("db", "") - - # ecc_module = ECCToolsModule() - - # return ecc_module.is_db_data_exists(db_path) or os.path.exists(step.input.get("def", "")) or os.path.exists(step.input.get("verilog", "")) - return os.path.exists(step.input.get("def", "")) or os.path.exists(step.input.get("verilog", "")) + + return ( + input_path_exists(step.input.get("def", "")) is not None + or input_path_exists(step.input.get("verilog", "")) is not None + ) if not is_eda_exist() or not is_enable_setup(): return None @@ -346,9 +367,6 @@ def run_cts(workspace: Workspace, ecc_module.report_cts(output=step.data.get(f"{StepEnum.CTS.value}", "")) - # Post-CTS legalization is handled by the following DreamPlace legalization step. - # ecc_module.run_legalize(config=workspace.config.get(f"{StepEnum.LEGALIZATION.value}", "")) - ecc_module.feature_cts_map(json_path=step.feature.get("map", "")) sub_flow.update_step(step_name=EccSubFlowEnum.run_CTS.value, state=StateEnum.Success) @@ -775,6 +793,7 @@ def run_harden(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.run_harden.value, state=StateEnum.Success) + run_analysis(workspace = workspace, step = step, subflow = sub_flow) reslut = True return reslut @@ -808,6 +827,7 @@ def run_rcx(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) + run_analysis(workspace = workspace, step = step, subflow = sub_flow) result = True return result @@ -984,4 +1004,5 @@ def collect_signoff_items() -> list[dict]: sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) + run_analysis(workspace = workspace, step = step, subflow = sub_flow) return result diff --git a/chipcompiler/tools/ecc_dreamplace/builder.py b/chipcompiler/tools/ecc_dreamplace/builder.py index ef863c80..da15aa77 100644 --- a/chipcompiler/tools/ecc_dreamplace/builder.py +++ b/chipcompiler/tools/ecc_dreamplace/builder.py @@ -71,6 +71,9 @@ def build_step_config(workspace: Workspace, step: WorkspaceStep) -> None: # build ecc config ecc_builder.build_step_config(workspace, step) + from .checklist import DreamplaceChecklist + DreamplaceChecklist(workspace=workspace, workspace_step=step) + if not workspace.config: workspace.config = build_workspace_config_paths(workspace) diff --git a/chipcompiler/tools/ecc_dreamplace/checklist.py b/chipcompiler/tools/ecc_dreamplace/checklist.py new file mode 100644 index 00000000..703f95b2 --- /dev/null +++ b/chipcompiler/tools/ecc_dreamplace/checklist.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import ast +import glob +import os + +from chipcompiler.data import ( + Workspace, + WorkspaceStep, + Checklist, + StepEnum, + CheckState, +) +from chipcompiler.utility import json_read + + +class DreamplaceChecklist: + CHECKLIST_ITEMS = { + StepEnum.PLACEMENT: [ + ("Density", "check target density"), + ("Density", "check placement overflow"), + ("Wirelength", "check HPWL"), + ("Legality", "check cell overlap"), + ("Congestion", "check placement congestion"), + ], + StepEnum.LEGALIZATION: [ + ("Legality", "check cell overlap"), + ("Legality", "check off-row placement"), + ("Legality", "check site alignment"), + ("Movement", "check legalization movement"), + ("Fixed", "check fixed instances"), + ], + } + + def __init__(self, + workspace : Workspace, + workspace_step: WorkspaceStep, + init_checklist : bool = True): + self.workspace = workspace + self.workspace_step = workspace_step + + if init_checklist: + self.build_checklist() + + def add_item(self, + checklist : Checklist, + step : str, + type : str, + item : str, + state : str, + info : str = ""): + checklist.add(step=step, + type=type, + item=item, + state=state, + info=info) + self.workspace.home.update_checklist(step=step, + type=type, + item=item, + state=state, + info=info) + + def add_items(self, + checklist : Checklist, + step : StepEnum): + for type, item in self.CHECKLIST_ITEMS.get(step, []): + self.add_item(checklist=checklist, + step=step.value, + type=type, + item=item, + state=CheckState.Unstart.value) + + def build_checklist(self) -> list: + checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) + step = StepEnum(self.workspace_step.name) + self.remove_stale_items(checklist=checklist, + step=step) + self.add_items(checklist=checklist, + step=step) + self.workspace_step.checklist["checklist"] = checklist.data + + def remove_stale_items(self, + checklist : Checklist, + step : StepEnum): + valid_items = set(self.CHECKLIST_ITEMS.get(step, [])) + checklist.data["checklist"] = [ + check_item + for check_item in checklist.data.get("checklist", []) + if check_item.get("step", "") != step.value + or (check_item.get("type", ""), check_item.get("item", "")) in valid_items + ] + checklist.save() + + def save(self) -> bool: + checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) + return checklist.save() + + def update_item(self, + step : str, + type : str, + item : str, + state : str | CheckState, + info : str = ""): + checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) + checklist.update(step=step, + type=type, + item=item, + state=state, + info=info) + + def set_item_state(self, + step : str, + type : str, + item : str, + state : CheckState, + info : str = ""): + self.update_item(step=step, + type=type, + item=item, + state=state, + info=info) + self.workspace.home.update_checklist(step=step, + type=type, + item=item, + state=state.value, + info=info) + + def check(self) -> bool: + step = StepEnum(self.workspace_step.name) + checker_class = { + StepEnum.PLACEMENT: DreamplacePlacementChecklist, + StepEnum.LEGALIZATION: DreamplaceLegalizationChecklist, + }.get(step) + if checker_class is None: + return True + + return checker_class( + self.workspace, + self.workspace_step, + init_checklist=False, + ).check() + + def check_file(self, + path : str, + text_tokens : list | None = None) -> bool: + if not path or not os.path.isfile(path) or os.path.getsize(path) <= 0: + return False + + if not text_tokens: + return True + + try: + with open(path, "r", encoding="utf-8", errors="ignore") as file: + content = file.read() + except OSError: + return False + + return all(token in content for token in text_tokens) + + def read_text(self, + path : str) -> str: + if not path or not os.path.isfile(path): + return "" + + try: + with open(path, "r", encoding="utf-8", errors="ignore") as file: + return file.read() + except OSError: + return "" + + def to_float(self, + value, + default : float | None = None) -> float | None: + try: + return float(value) + except (TypeError, ValueError): + return default + + def step_file_success(self) -> bool: + return all( + self.check_file(self.workspace_step.output.get(key, "")) + for key in ("def", "verilog", "gds") + ) + + def metrics(self) -> dict: + return json_read(self.workspace_step.analysis.get("metrics", "")) + + def feature_db(self) -> dict: + return json_read(self.workspace_step.feature.get("db", "")) + + def feature_map(self) -> dict: + return json_read(self.workspace_step.feature.get("map", "")) + + def dreamplace_config(self) -> dict: + return json_read(self.workspace.config.get("dreamplace", "")) + + def log_text(self) -> str: + return self.read_text(self.workspace_step.log.get("file", "")) + + def update_checks(self, + checks : list) -> bool: + step = self.workspace_step.name + results = [] + for check in checks: + if len(check) == 3: + type, item, success = check + info = f"{item} check failed" + warning = False + elif len(check) == 4: + type, item, success, info = check + warning = False + else: + type, item, success, info, warning = check + + state = CheckState.Passed + if not success: + state = CheckState.Warning if warning else CheckState.Failed + self.set_item_state( + step=step, + type=type, + item=item, + state=state, + info="" if success else info, + ) + results.append(success or warning) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(results) + + def final_ppa(self) -> dict: + marker = "Final PPA:" + data = {} + for line in self.log_text().splitlines(): + if marker not in line: + continue + + _, value = line.split(marker, 1) + value = value.strip().replace("inf", "1e999") + try: + parsed = ast.literal_eval(value) + except (SyntaxError, ValueError): + continue + if isinstance(parsed, dict): + data = parsed + + return data + + def view_instances(self) -> dict: + view_dir = self.workspace_step.output.get("view_json", "") + return json_read(os.path.join(view_dir, "design", "instances.json")) + + def count_unplaced_instances(self) -> int | None: + instances = self.view_instances().get("data", []) + if not isinstance(instances, list): + return None + + return sum( + 1 for inst in instances + if inst.get("status", "") not in ("PLACED", "FIXED", "COVER") + ) + + def has_plot_files(self) -> bool: + pattern = os.path.join( + self.workspace_step.data.get(self.workspace_step.name, ""), + self.workspace.design.name, + "plot", + "*.png", + ) + return any( + os.path.isfile(path) and os.path.getsize(path) > 0 + for path in glob.glob(pattern) + ) + + +class DreamplacePlacementChecklist(DreamplaceChecklist): + def target_density_success(self) -> bool: + config = self.dreamplace_config() + target_density = self.to_float(config.get("target_density")) + stop_overflow = self.to_float(config.get("stop_overflow")) + core_util = self.to_float(self.metrics().get("Core util")) + + return ( + target_density is not None + and target_density > 0 + and stop_overflow is not None + and stop_overflow >= 0 + and core_util is not None + and core_util > 0 + ) + + def overflow_success(self) -> bool: + config = self.dreamplace_config() + stop_overflow = self.to_float(config.get("stop_overflow"), 0.0) + final_overflow = self.to_float(self.final_ppa().get("overflow")) + + return ( + final_overflow is not None + and stop_overflow is not None + and final_overflow >= 0 + and final_overflow <= stop_overflow + ) + + def hpwl_success(self) -> bool: + map_data = self.feature_map() + hpwl = self.to_float(map_data.get("Wirelength", {}).get("HPWL")) + final_hpwl = self.to_float(self.final_ppa().get("hpwl")) + + return ( + hpwl is not None + and hpwl > 0 + and final_hpwl is not None + and final_hpwl > 0 + ) + + def cell_overlap_success(self) -> bool: + text = self.log_text() + unplaced = self.count_unplaced_instances() + + return ( + self.step_file_success() + and "Start legalization" in text + and "legalization takes" in text + and (unplaced is None or unplaced == 0) + ) + + def congestion_success(self) -> bool: + map_data = self.feature_map() + congestion = map_data.get("Congestion", {}) + overflow = congestion.get("overflow", {}) + total = overflow.get("total", {}) + union_overflow = self.to_float(total.get("union"), 0.0) + ppa_congestion = self.to_float(self.final_ppa().get("congestion")) + + return ( + len(congestion) > 0 + and union_overflow is not None + and union_overflow >= 0 + and ppa_congestion is not None + and ppa_congestion >= 0 + and self.has_plot_files() + ) + + def check(self) -> bool: + checks = [ + ("Density", "check target density", self.target_density_success(), + "DreamPlace target_density/stop_overflow/core util data is missing or invalid"), + ("Density", "check placement overflow", self.overflow_success(), + "final overflow is missing or exceeds stop_overflow"), + ("Wirelength", "check HPWL", self.hpwl_success(), + "HPWL metric or final PPA hpwl is missing", True), + ("Legality", "check cell overlap", self.cell_overlap_success(), + "legalization did not complete cleanly or unplaced cells remain"), + ("Congestion", "check placement congestion", self.congestion_success(), + "congestion metrics or placement plot files are missing", True), + ] + + return self.update_checks(checks) + + +class DreamplaceLegalizationChecklist(DreamplaceChecklist): + def log_legalization_success(self) -> bool: + text = self.log_text() + return ( + "Start legalization" in text + and "legalization takes" in text + and "num_unplaced_cells = 0" in text + ) + + def cell_overlap_success(self) -> bool: + text = self.log_text() + unplaced = self.count_unplaced_instances() + + return ( + self.step_file_success() + and "Legality check takes" in text + and (unplaced is None or unplaced == 0) + ) + + def site_alignment_success(self) -> bool: + db = self.feature_db() + layout = db.get("Design Layout", {}) + core_usage = self.to_float(layout.get("core_usage")) + instance_count = self.to_float(db.get("Design Statis", {}).get("num_instances")) + + return ( + core_usage is not None + and core_usage > 0 + and instance_count is not None + and instance_count > 0 + and self.log_legalization_success() + ) + + def movement_success(self) -> bool: + ppa = self.final_ppa() + hpwl = self.to_float(ppa.get("hpwl")) + text = self.log_text() + + return ( + hpwl is not None + and hpwl > 0 + and ( + "average displace" in text + or "placement takes" in text + ) + ) + + def fixed_success(self) -> bool: + text = self.log_text() + return ( + self.step_file_success() + and "Macro legalization" in text + and "WriteBack placement finished" in text + ) + + def check(self) -> bool: + checks = [ + ("Legality", "check cell overlap", self.cell_overlap_success(), + "legality check did not complete cleanly or unplaced cells remain"), + ("Legality", "check off-row placement", self.log_legalization_success(), + "legalization log does not report zero unplaced cells"), + ("Legality", "check site alignment", self.site_alignment_success(), + "site alignment proxy metrics are missing or invalid"), + ("Movement", "check legalization movement", self.movement_success(), + "legalization movement/HPWL metrics are missing", True), + ("Fixed", "check fixed instances", self.fixed_success(), + "fixed instance writeback or macro legalization log marker is missing"), + ] + + return self.update_checks(checks) diff --git a/chipcompiler/tools/ecc_dreamplace/runner.py b/chipcompiler/tools/ecc_dreamplace/runner.py index c9825c28..610d5ebb 100644 --- a/chipcompiler/tools/ecc_dreamplace/runner.py +++ b/chipcompiler/tools/ecc_dreamplace/runner.py @@ -11,8 +11,22 @@ from chipcompiler.tools.ecc import EccSubFlowEnum, EccSubFlow, ECCToolsModule from .module import DreamplaceModule +from .checklist import DreamplaceChecklist from .utility import is_eda_exist + +def run_analysis(workspace: Workspace, + step: WorkspaceStep, + subflow : EccSubFlow): + ecc_runner.run_analysis(workspace=workspace, + step=step, + subflow=subflow) + + checklist = DreamplaceChecklist(workspace=workspace, + workspace_step=step, + init_checklist=False) + checklist.check() + def run_step( workspace: Workspace, step: WorkspaceStep, @@ -74,7 +88,7 @@ def run_placement(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) - ecc_runner.run_analysis(workspace = workspace, step = step, subflow = sub_flow) + run_analysis(workspace=workspace, step=step, subflow=sub_flow) return reslut @@ -116,6 +130,6 @@ def run_legalization(workspace: Workspace, sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) - ecc_runner.run_analysis(workspace = workspace, step = step, subflow = sub_flow) + run_analysis(workspace=workspace, step=step, subflow=sub_flow) - return reslut \ No newline at end of file + return reslut diff --git a/chipcompiler/tools/yosys/checklist.py b/chipcompiler/tools/yosys/checklist.py index 29e35d07..34064ed5 100644 --- a/chipcompiler/tools/yosys/checklist.py +++ b/chipcompiler/tools/yosys/checklist.py @@ -1,19 +1,68 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- +import os + from chipcompiler.data import ( - Workspace, WorkspaceStep, Checklist, CheckState + Workspace, WorkspaceStep, Checklist, StepEnum, CheckState ) +from chipcompiler.utility import json_read class YosysChecklist: - def __init__(self, workspace : Workspace, workspace_step: WorkspaceStep): + CHECKLIST_ITEMS = { + StepEnum.SYNTHESIS: [ + ("Input", "check RTL or filelist input"), + ("Constraint", "check top module and frequency"), + ("Library", "check synthesis liberty input"), + ("Log", "check synthesis log"), + ("Netlist", "check mapped gate netlist"), + ("Metrics", "check synthesis cell statistics"), + ], + } + + def __init__(self, + workspace : Workspace, + workspace_step: WorkspaceStep, + init_checklist : bool = True): self.workspace = workspace self.workspace_step = workspace_step - self.build_checklist() - + if init_checklist: + self.build_checklist() + + def add_item(self, + checklist : Checklist, + step : str, + type : str, + item : str, + state : str, + info : str = ""): + checklist.add(step=step, + type=type, + item=item, + state=state, + info=info) + + self.workspace.home.update_checklist(step=step, + type=type, + item=item, + state=state, + info=info) + + def add_items(self, + checklist : Checklist, + step : StepEnum): + for type, item in self.CHECKLIST_ITEMS.get(step, []): + self.add_item(checklist=checklist, + step=step.value, + type=type, + item=item, + state=CheckState.Unstart.value) def build_checklist(self) -> list: checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) + step = StepEnum(self.workspace_step.name) + self.add_items(checklist=checklist, + step=step) self.workspace_step.checklist["checklist"] = checklist.data @@ -25,12 +74,162 @@ def update_item(self, step : str, type : str, item : str, - state : str | CheckState): + state : str | CheckState, + info : str = ""): checklist = Checklist(path=self.workspace_step.checklist.get("path", "")) checklist.update(step=step, type=type, item=item, - state=state) - + state=state, + info=info) + + def set_item_state(self, + step : str, + type : str, + item : str, + state : CheckState, + info : str = ""): + self.update_item(step=step, + type=type, + item=item, + state=state, + info=info) + self.workspace.home.update_checklist(step=step, + type=type, + item=item, + state=state.value, + info=info) + def check(self): - pass \ No newline at end of file + step = StepEnum(self.workspace_step.name) + checker_class = { + StepEnum.SYNTHESIS: YosysSynthesisChecklist, + }.get(step) + if checker_class is None: + return True + + return checker_class( + self.workspace, + self.workspace_step, + init_checklist=False, + ).check() + + +class YosysSynthesisChecklist(YosysChecklist): + def check_file(self, + path : str, + text_tokens : list | None = None) -> bool: + if not path or not os.path.isfile(path) or os.path.getsize(path) <= 0: + return False + + if not text_tokens: + return True + + try: + with open(path, "r", encoding="utf-8", errors="ignore") as file: + content = file.read() + except OSError: + return False + + return all(token in content for token in text_tokens) + + def to_float(self, + value, + default : float | None = None) -> float | None: + try: + return float(value) + except (TypeError, ValueError): + return default + + def check(self) -> bool: + step = StepEnum.SYNTHESIS.value + metrics = json_read(self.workspace_step.analysis.get("metrics", "")) + stat = json_read(self.workspace_step.feature.get("stat", "")) + + try: + with open(self.workspace_step.log.get("file", ""), + "r", encoding="utf-8", errors="ignore") as file: + log_text = file.read() + except OSError: + log_text = "" + + try: + with open(self.workspace_step.output.get("verilog", ""), + "r", encoding="utf-8", errors="ignore") as file: + netlist_text = file.read() + except OSError: + netlist_text = "" + + input_verilog = self.workspace_step.input.get("verilog", "") + filelist = ( + self.workspace.design.input_filelist + if self.workspace.design.input_filelist + else self.workspace.parameters.data.get("File list", "") + ) + top_module = self.workspace.design.top_module \ + or self.workspace.design.name + frequency = self.to_float( + self.workspace.parameters.data.get("Frequency max [MHz]"), 0.0) + libs = getattr(self.workspace.pdk, "libs", []) or [] + modules = stat.get("modules", {}) + module_keys = { + key.lstrip("\\") + for key in modules + } + design_stat = stat.get("design", {}) + cell_number = self.to_float( + metrics.get("Cell number", design_stat.get("num_cells")), 0.0) + cell_area = self.to_float( + metrics.get("Cell area", design_stat.get("area")), 0.0) + log_lower = log_text.lower() + log_success = ( + len(log_text) > 0 + and "end of script" in log_lower + and not any( + token in log_lower + for token in [ + "error:", + "fatal", + "syntax error", + "unmapped cell", + "blackbox", + "traceback", + ] + ) + ) + + checks = [ + ("Input", "check RTL or filelist input", + (input_verilog and os.path.isfile(input_verilog)) + or (filelist and os.path.isfile(filelist))), + ("Constraint", "check top module and frequency", + bool(top_module) + and top_module in module_keys + and frequency > 0), + ("Library", "check synthesis liberty input", + len(libs) > 0 + and all(os.path.isfile(path) for path in libs) + and "-liberty" in stat.get("invocation", "")), + ("Log", "check synthesis log", log_success), + ("Netlist", "check mapped gate netlist", + self.check_file(self.workspace_step.output.get("verilog", "")) + and bool(top_module) + and f"module {top_module}" in netlist_text), + ("Metrics", "check synthesis cell statistics", + cell_number > 0 and cell_area > 0), + ] + + for type, item, success in checks: + self.set_item_state( + step=step, + type=type, + item=item, + state=CheckState.Passed if success else CheckState.Failed, + info="" if success else f"{item} check failed", + ) + + self.workspace_step.checklist["checklist"] = Checklist( + path=self.workspace_step.checklist.get("path", "") + ).data + + return all(success for _, _, success in checks) From 4b578aef32fbf1bb2e32f99eedc2ff32ab34b440 Mon Sep 17 00:00:00 2001 From: Yell-walkalone <12112088@qq.com> Date: Thu, 18 Jun 2026 09:20:39 +0800 Subject: [PATCH 02/10] update checklist --- chipcompiler/thirdparty/ecc-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chipcompiler/thirdparty/ecc-tools b/chipcompiler/thirdparty/ecc-tools index c21b8d26..4f78d45e 160000 --- a/chipcompiler/thirdparty/ecc-tools +++ b/chipcompiler/thirdparty/ecc-tools @@ -1 +1 @@ -Subproject commit c21b8d26cdb28a2af80b3f0bb623d1e7730d012d +Subproject commit 4f78d45e306de74dd5524aed03ff4e0c9afa8e5f From 074a7edd93b3e11ee5f0f524468cf6cbad3d0c6f Mon Sep 17 00:00:00 2001 From: Yell-walkalone <12112088@qq.com> Date: Thu, 18 Jun 2026 16:46:48 +0800 Subject: [PATCH 03/10] change output verilog to gz format --- chipcompiler/tools/ecc_sizer/builder.py | 2 +- chipcompiler/tools/yosys/builder.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chipcompiler/tools/ecc_sizer/builder.py b/chipcompiler/tools/ecc_sizer/builder.py index d7f56284..6c26b7e8 100644 --- a/chipcompiler/tools/ecc_sizer/builder.py +++ b/chipcompiler/tools/ecc_sizer/builder.py @@ -24,7 +24,7 @@ def build_step( if output_def is None: output_def = f"{step_directory}/output/{workspace.design.name}_{safe_step_name}.def.gz" if output_verilog is None: - output_verilog = f"{step_directory}/output/{workspace.design.name}_{safe_step_name}.v" + output_verilog = f"{step_directory}/output/{workspace.design.name}_{safe_step_name}.v.gz" step = ecc_builder.build_step( workspace=workspace, diff --git a/chipcompiler/tools/yosys/builder.py b/chipcompiler/tools/yosys/builder.py index 98c331b8..ac271611 100644 --- a/chipcompiler/tools/yosys/builder.py +++ b/chipcompiler/tools/yosys/builder.py @@ -194,14 +194,14 @@ def build_step(workspace: Workspace, } if output_verilog is None: - output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v" + output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v.gz" if output_def is None: output_def = f"{step.directory}/output/{workspace.design.name}_{step.name}.def.gz" step.output = { "dir": f"{step.directory}/output", "def": output_def, "verilog": output_verilog, - "fixed_verilog": f"{step.directory}/output/{workspace.design.name}_{step.name}_fixed.v", + "fixed_verilog": f"{step.directory}/output/{workspace.design.name}_{step.name}_fixed.v.gz", "json": f"{step.directory}/output/{workspace.design.name}_{step.name}.json", "report": f"{step.directory}/output/{workspace.design.name}_{step.name}.rpt", "image": f"{step.directory}/output/{workspace.design.name}_{step.name}.png" From 2e068996387d69226c19f4cf552bcb2fe23dd31a Mon Sep 17 00:00:00 2001 From: Emin Date: Thu, 18 Jun 2026 17:36:58 +0800 Subject: [PATCH 04/10] fix(ci): remove --no-sync to test ci Signed-off-by: Emin --- .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da5de738..920a442e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,8 +53,39 @@ jobs: with: submodules: recursive - - name: Setup Python dependencies - uses: ./.github/actions/setup-python-deps + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + version: latest + enable-cache: "true" + + - name: Install native build dependencies + run: | + set -euo pipefail + + packages=( + software-properties-common ca-certificates curl git xz-utils + build-essential python3 python3-dev python3-venv + cmake ninja-build pkg-config patchelf mold lld + flex libfl-dev bison unzip zip + zlib1g-dev libboost-all-dev libcairo2-dev + libgflags-dev libgoogle-glog-dev libeigen3-dev libgtest-dev + libtbb-dev libhwloc-dev libcurl4-openssl-dev libunwind-dev + libmetis-dev libgmp-dev tcl-dev tcl8.6-dev + libyaml-cpp-dev libqhull-dev libffi-dev libssl-dev + ) + + sudo apt-get update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" + + - name: Install uv + run: uv sync --no-build-isolation-package ecc-dreamplace --no-build-isolation-package ecc-tools-bin --verbose + # uses: ./.github/actions/setup-python-deps - name: Setup PDK run: | @@ -81,7 +112,7 @@ jobs: # run: uv run pyright chipcompiler - name: Pytest - run: uv run --no-sync pytest test/ --ignore=test/test_harden.py --ignore=test/test_rcx.py --ignore=test/examples/test_soc.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() From e1daebe65a80dc4548c81aa70ea56dd2454a91fd Mon Sep 17 00:00:00 2001 From: Emin Date: Thu, 18 Jun 2026 19:43:09 +0800 Subject: [PATCH 05/10] fix(test): update default sizer output to .v.gz Signed-off-by: Emin --- test/test_ecc_sizer_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ecc_sizer_module.py b/test/test_ecc_sizer_module.py index 7e01cf2d..0f448c06 100644 --- a/test/test_ecc_sizer_module.py +++ b/test/test_ecc_sizer_module.py @@ -123,7 +123,7 @@ def test_sizer_step_declares_no_db_output_and_keeps_standard_dirs(tmp_path): assert " " not in os.path.basename(step.output["def"]) assert " " not in os.path.basename(step.output["verilog"]) assert os.path.basename(step.output["def"]) == "gcd_timing_optimization.def.gz" - assert os.path.basename(step.output["verilog"]) == "gcd_timing_optimization.v" + assert os.path.basename(step.output["verilog"]) == "gcd_timing_optimization.v.gz" sizer_builder.build_step_space(step) From d61e00884e0d9113d6762de07310da7bccff2d9a Mon Sep 17 00:00:00 2001 From: Yell-walkalone <12112088@qq.com> Date: Thu, 18 Jun 2026 16:46:48 +0800 Subject: [PATCH 06/10] change output verilog to gz format --- chipcompiler/tools/ecc_sizer/builder.py | 2 +- chipcompiler/tools/yosys/builder.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chipcompiler/tools/ecc_sizer/builder.py b/chipcompiler/tools/ecc_sizer/builder.py index d7f56284..6c26b7e8 100644 --- a/chipcompiler/tools/ecc_sizer/builder.py +++ b/chipcompiler/tools/ecc_sizer/builder.py @@ -24,7 +24,7 @@ def build_step( if output_def is None: output_def = f"{step_directory}/output/{workspace.design.name}_{safe_step_name}.def.gz" if output_verilog is None: - output_verilog = f"{step_directory}/output/{workspace.design.name}_{safe_step_name}.v" + output_verilog = f"{step_directory}/output/{workspace.design.name}_{safe_step_name}.v.gz" step = ecc_builder.build_step( workspace=workspace, diff --git a/chipcompiler/tools/yosys/builder.py b/chipcompiler/tools/yosys/builder.py index 98c331b8..ac271611 100644 --- a/chipcompiler/tools/yosys/builder.py +++ b/chipcompiler/tools/yosys/builder.py @@ -194,14 +194,14 @@ def build_step(workspace: Workspace, } if output_verilog is None: - output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v" + output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v.gz" if output_def is None: output_def = f"{step.directory}/output/{workspace.design.name}_{step.name}.def.gz" step.output = { "dir": f"{step.directory}/output", "def": output_def, "verilog": output_verilog, - "fixed_verilog": f"{step.directory}/output/{workspace.design.name}_{step.name}_fixed.v", + "fixed_verilog": f"{step.directory}/output/{workspace.design.name}_{step.name}_fixed.v.gz", "json": f"{step.directory}/output/{workspace.design.name}_{step.name}.json", "report": f"{step.directory}/output/{workspace.design.name}_{step.name}.rpt", "image": f"{step.directory}/output/{workspace.design.name}_{step.name}.png" From ae91190574741709ac245ff9cbc77bc1b2e10ded Mon Sep 17 00:00:00 2001 From: Emin Date: Thu, 18 Jun 2026 19:43:09 +0800 Subject: [PATCH 07/10] fix(test): update default sizer output to .v.gz Signed-off-by: Emin --- test/test_ecc_sizer_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ecc_sizer_module.py b/test/test_ecc_sizer_module.py index 7e01cf2d..0f448c06 100644 --- a/test/test_ecc_sizer_module.py +++ b/test/test_ecc_sizer_module.py @@ -123,7 +123,7 @@ def test_sizer_step_declares_no_db_output_and_keeps_standard_dirs(tmp_path): assert " " not in os.path.basename(step.output["def"]) assert " " not in os.path.basename(step.output["verilog"]) assert os.path.basename(step.output["def"]) == "gcd_timing_optimization.def.gz" - assert os.path.basename(step.output["verilog"]) == "gcd_timing_optimization.v" + assert os.path.basename(step.output["verilog"]) == "gcd_timing_optimization.v.gz" sizer_builder.build_step_space(step) From 9f3449e91885e80c2c6b2317b387d4ad69391b95 Mon Sep 17 00:00:00 2001 From: Emin Date: Sat, 20 Jun 2026 12:03:15 +0800 Subject: [PATCH 08/10] chore: bump ecc-tools ecc-dreamplace Signed-off-by: Emin --- chipcompiler/thirdparty/ecc-tools | 2 +- pyproject.toml | 4 ++-- uv.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chipcompiler/thirdparty/ecc-tools b/chipcompiler/thirdparty/ecc-tools index 4f78d45e..2e3928a7 160000 --- a/chipcompiler/thirdparty/ecc-tools +++ b/chipcompiler/thirdparty/ecc-tools @@ -1 +1 @@ -Subproject commit 4f78d45e306de74dd5524aed03ff4e0c9afa8e5f +Subproject commit 2e3928a78f8bca971324beba70bac33945904f87 diff --git a/pyproject.toml b/pyproject.toml index 262351c0..b8127c7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "ecc-dreamplace==0.1.0a4", - "ecc-tools-bin==0.1.0a5", + "ecc-dreamplace==0.1.0a5", + "ecc-tools-bin==0.1.0a6", "fastapi>=0.109", "klayout>=0.30.2", "matplotlib>=3.4", diff --git a/uv.lock b/uv.lock index bfa06271..fbefce0c 100644 --- a/uv.lock +++ b/uv.lock @@ -515,7 +515,7 @@ dev = [ [[package]] name = "ecc-dreamplace" -version = "0.1.0a4" +version = "0.1.0a5" source = { editable = "chipcompiler/thirdparty/ecc-dreamplace" } dependencies = [ { name = "cairocffi" }, @@ -574,7 +574,7 @@ dev = [ [[package]] name = "ecc-tools-bin" -version = "0.1.0a5" +version = "0.1.0a6" source = { editable = "chipcompiler/thirdparty/ecc-tools" } dependencies = [ { name = "numpy" }, From bf2614af4a7e9ded397862b5f5e47ce27b4ae082 Mon Sep 17 00:00:00 2001 From: Yell-walkalone <12112088@qq.com> Date: Tue, 23 Jun 2026 15:16:01 +0800 Subject: [PATCH 09/10] add signoff resource package --- chipcompiler/cli/commands/workspace.py | 22 + chipcompiler/cli/workspace/service.py | 77 +++- chipcompiler/engine/__init__.py | 9 +- chipcompiler/engine/flow.py | 19 +- chipcompiler/engine/signoff.py | 614 +++++++++++++++++++++++++ chipcompiler/thirdparty/ecc-tools | 2 +- chipcompiler/tools/ecc/module.py | 36 +- chipcompiler/tools/ecc/runner.py | 7 +- test/cli/test_workspace_cli.py | 71 ++- test/test_ecc_tools_module.py | 12 +- test/test_harden.py | 16 +- test/test_signoff_package.py | 109 +++++ 12 files changed, 974 insertions(+), 20 deletions(-) create mode 100644 chipcompiler/engine/signoff.py create mode 100644 test/test_signoff_package.py diff --git a/chipcompiler/cli/commands/workspace.py b/chipcompiler/cli/commands/workspace.py index cee92e87..8ac3d55f 100644 --- a/chipcompiler/cli/commands/workspace.py +++ b/chipcompiler/cli/commands/workspace.py @@ -14,6 +14,7 @@ workspace_response, ) from chipcompiler.cli.workspace.service import ( + collect_signoff_package, create_workspace_from_request, get_workspace_home, get_workspace_info, @@ -272,3 +273,24 @@ def get_home_cmd( json_output: Annotated[bool, typer.Option("--json")] = False, ) -> None: _dispatch_runtime(lambda: get_workspace_home(directory), json_output) + + +@workspace_app.command("signoff", help="Collect a harden-flow signoff package") +def signoff_cmd( + directory: Annotated[str, typer.Option("--directory")] = "", + output_dir: Annotated[str, typer.Option("--output")] = "", + archive: Annotated[bool, typer.Option("--archive/--no-archive")] = True, + include_debug: Annotated[bool, typer.Option("--include-debug")] = False, + allow_incomplete: Annotated[bool, typer.Option("--allow-incomplete")] = False, + json_output: Annotated[bool, typer.Option("--json")] = False, +) -> None: + _dispatch_runtime( + lambda: collect_signoff_package( + directory, + output_dir, + archive, + include_debug, + allow_incomplete, + ), + json_output, + ) diff --git a/chipcompiler/cli/workspace/service.py b/chipcompiler/cli/workspace/service.py index d8f2394e..29c70c79 100644 --- a/chipcompiler/cli/workspace/service.py +++ b/chipcompiler/cli/workspace/service.py @@ -204,7 +204,10 @@ def run_workspace_step(directory: str, step: str, rerun: bool) -> dict: def refresh_workspace_config(directory: str) -> dict: cmd = "refresh_config" - response_data = {"directory": os.path.abspath(directory) if directory else "", "refreshed": False} + response_data = { + "directory": os.path.abspath(directory) if directory else "", + "refreshed": False, + } if not directory: return workspace_response( cmd, @@ -394,6 +397,78 @@ def get_workspace_home(directory: str) -> dict: ) +def collect_signoff_package(directory: str, + output_dir: str, + archive: bool, + include_debug: bool, + allow_incomplete: bool) -> dict: + cmd = "signoff" + response_data = { + "directory": os.path.abspath(directory) if directory else "", + "output_dir": os.path.abspath(output_dir) if output_dir else "", + "archive": bool(archive), + "include_debug": bool(include_debug), + "allow_incomplete": bool(allow_incomplete), + } + if not directory: + return workspace_response( + cmd, + "failed", + data=response_data, + message=["missing required field: directory"], + ) + + try: + workspace, engine_flow = load_workspace_runtime( + directory, + create_step_workspaces=False, + ) + from chipcompiler.engine.signoff import SignoffPackageOptions + + result = engine_flow.collect_signoff_package( + SignoffPackageOptions( + output_dir=output_dir or None, + archive=archive, + include_debug=include_debug, + allow_incomplete=allow_incomplete, + ) + ) + except WorkspaceValidationError as exc: + return workspace_response(cmd, "failed", data=response_data, message=[str(exc)]) + except Exception as exc: + return workspace_response( + cmd, + "error", + data=response_data, + message=[f"collect signoff package failed : {exc}"], + ) + + response_data.update({ + "directory": os.path.abspath(workspace.directory), + "package_dir": result.package_dir, + "archive_path": result.archive_path or "", + "manifest_path": result.manifest_path or "", + "summary_path": result.summary_path or "", + "copied_count": len(result.copied), + "missing_required": result.missing_required, + "warnings": result.warnings, + }) + if result.ok: + return workspace_response( + cmd, + "success", + data=response_data, + message=[f"collect signoff package success : {result.package_dir}"], + ) + + return workspace_response( + cmd, + "failed", + data=response_data, + message=["collect signoff package incomplete"], + ) + + def load_workspace_runtime(directory: str, create_step_workspaces: bool = True): import chipcompiler.data as data_api diff --git a/chipcompiler/engine/__init__.py b/chipcompiler/engine/__init__.py index b6f0a2de..0b2be020 100644 --- a/chipcompiler/engine/__init__.py +++ b/chipcompiler/engine/__init__.py @@ -6,7 +6,14 @@ EngineFlow ) +from .signoff import ( + SignoffPackageCollector, + SignoffPackageOptions +) + __all__ = [ 'EngineDB', - 'EngineFlow' + 'EngineFlow', + 'SignoffPackageCollector', + 'SignoffPackageOptions' ] \ No newline at end of file diff --git a/chipcompiler/engine/flow.py b/chipcompiler/engine/flow.py index c5ed908f..924b1f17 100644 --- a/chipcompiler/engine/flow.py +++ b/chipcompiler/engine/flow.py @@ -1,18 +1,24 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- +import logging import os import time -import logging import traceback from threading import Event, Thread -from chipcompiler.data import Workspace, WorkspaceStep, StateEnum, StepEnum, log_flow +from chipcompiler.data import StateEnum, StepEnum, Workspace, WorkspaceStep, log_flow from chipcompiler.engine import EngineDB +from chipcompiler.engine.signoff import ( + SignoffPackageCollector, + SignoffPackageOptions, + SignoffPackageResult, +) 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: @@ -220,6 +226,15 @@ def check_step_result(self, success = True return success + def collect_signoff_package( + self, + options: SignoffPackageOptions | None = None, + ) -> SignoffPackageResult: + """ + Collect harden-flow signoff resources from this flow workspace. + """ + return SignoffPackageCollector(self.workspace).collect(options) + def create_step_workspaces(self): """ create all step workspaces diff --git a/chipcompiler/engine/signoff.py b/chipcompiler/engine/signoff.py new file mode 100644 index 00000000..7cf7d564 --- /dev/null +++ b/chipcompiler/engine/signoff.py @@ -0,0 +1,614 @@ +import glob +import hashlib +import json +import shutil +import tarfile +import time +from dataclasses import dataclass, field +from pathlib import Path + +from chipcompiler.data import StateEnum, StepEnum, Workspace + + +@dataclass(frozen=True) +class SignoffPackageOptions: + output_dir: str | None = None + archive: bool = True + include_debug: bool = False + allow_incomplete: bool = False + + +@dataclass +class SignoffPackageResult: + ok: bool + package_dir: str + archive_path: str | None = None + manifest_path: str | None = None + summary_path: str | None = None + copied: list[dict] = field(default_factory=list) + missing_required: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + + +class SignoffPackageCollector: + def __init__(self, workspace: Workspace): + self.workspace = workspace + + def collect( + self, + options: SignoffPackageOptions | None = None, + ) -> SignoffPackageResult: + options = options or SignoffPackageOptions() + if self.workspace is None or not self.workspace.directory: + raise FileNotFoundError("workspace is not configured") + + workspace_dir = Path(self.workspace.directory) + if not workspace_dir.exists(): + raise FileNotFoundError(f"workspace does not exist: {workspace_dir}") + + parameters = self._read_json(workspace_dir / "home" / "parameters.json") + design = ( + self.workspace.design.name + or parameters.get("Design", "") + or self._design_from_outputs(workspace_dir) + ) + top_module = self.workspace.design.top_module or parameters.get("Top module", "") or design + pdk_name = getattr(self.workspace.pdk, "name", "") or parameters.get("PDK", "") + if not design: + raise ValueError("cannot determine design name for signoff package") + + package_root = Path(options.output_dir) if options.output_dir else workspace_dir / "signoff" + package_dir = package_root / f"{design}_signoff_package" + if package_dir.exists(): + shutil.rmtree(package_dir) + package_dir.mkdir(parents=True, exist_ok=True) + + copied: list[dict] = [] + missing_required: list[str] = [] + warnings: list[str] = [] + + def add_file(role: str, + source: Path | None, + destination: str, + required: bool = False) -> None: + self._add_file( + workspace_dir=workspace_dir, + package_dir=package_dir, + role=role, + source=source, + destination=destination, + required=required, + copied=copied, + missing_required=missing_required, + ) + + flow_path = workspace_dir / "home" / "flow.json" + checklist_path = workspace_dir / "home" / "checklist.json" + flow_data = self.workspace.flow.data or self._read_json(flow_path) + checklist_data = self._read_json(checklist_path) + + required_steps = self._required_step_states(flow_data) + for step_name, state in required_steps.items(): + if state != StateEnum.Success.value: + missing_required.append(f"flow step {step_name} is {state or 'missing'}") + + config_dir = workspace_dir / "config" + required_configs = { + "db_default_config.json", + "flow_config.json", + "rcx.json", + "sta.json", + } + if not config_dir.is_dir(): + missing_required.append("config directory") + else: + for config_file in sorted(path for path in config_dir.rglob("*") if path.is_file()): + rel = config_file.relative_to(config_dir).as_posix() + add_file( + role=f"config.{config_file.stem}", + source=config_file, + destination=f"config/{rel}", + required=config_file.name in required_configs, + ) + for config_name in sorted(required_configs): + if not (config_dir / config_name).is_file(): + missing_required.append(f"config/{config_name}") + + db_config = self._read_json(config_dir / "db_default_config.json") + origin_verilog = self._find_one( + workspace_dir / "origin", + preferred_name=f"{design}.v", + pattern="*.v", + missing_label="origin Verilog", + missing_required=missing_required, + ) + origin_sdc = self._path_from_config( + workspace_dir, + db_config.get("INPUT", {}).get("sdc_path", ""), + ) + if origin_sdc is None: + origin_sdc = self._find_one( + workspace_dir / "origin", + preferred_name=f"{design}.sdc", + pattern="*.sdc", + missing_label="origin SDC", + missing_required=missing_required, + ) + + add_file( + role="harden.gds", + source=workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.gds", + destination=f"harden/{design}.gds", + required=True, + ) + add_file( + role="harden.lef", + source=workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.lef", + destination=f"harden/{design}.lef", + required=True, + ) + add_file( + role="harden.lib", + source=workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.lib", + destination=f"harden/{design}.lib", + required=True, + ) + add_file( + role="harden.lib_check_sources", + source=( + workspace_dir + / "Harden_ecc" + / "output" + / f"{design}_Harden.lib.check_sources.tsv" + ), + destination=f"harden/{design}.lib.check_sources.tsv", + ) + add_file( + role="harden.image", + source=workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.png", + destination=f"harden/{design}.png", + ) + + add_file("initial.verilog", origin_verilog, f"initial/{design}.v", required=True) + add_file("initial.sdc", origin_sdc, f"initial/{design}.sdc", required=True) + add_file( + "initial.parameters", + workspace_dir / "home" / "parameters.json", + "initial/parameters.json", + required=True, + ) + + add_file( + role="final.design.verilog", + source=workspace_dir / "filler_ecc" / "output" / f"{design}_filler.v.gz", + destination=f"final/design/{design}.v.gz", + required=True, + ) + add_file( + role="final.design.def", + source=workspace_dir / "filler_ecc" / "output" / f"{design}_filler.def.gz", + destination=f"final/design/{design}.def.gz", + required=True, + ) + add_file( + role="final.design.gds", + source=workspace_dir / "filler_ecc" / "output" / f"{design}_filler.gds", + destination=f"final/design/{design}.gds", + required=True, + ) + add_file( + role="final.design.image", + source=workspace_dir / "filler_ecc" / "output" / f"{design}_filler.png", + destination=f"final/design/{design}.png", + ) + + sta_config = self._read_json(config_dir / "sta.json") + sta_matrix = self._sta_matrix(sta_config) + expected_spefs = set() + for item in sta_matrix: + expected_spefs.add( + f"{design}_{item['rcx_corner']}_{self._temperature_token(item['temperature'])}C.spef" + ) + report_dir = ( + workspace_dir + / "sta_ecc" + / "output" + / f"{item['lib_corner']}_{self._temperature_token(item['temperature'])}" + / item["rcx_corner"] + ) + report_dest = ( + f"final/timing/sta/{item['lib_corner']}_" + f"{self._temperature_token(item['temperature'])}/" + f"{item['rcx_corner']}" + ) + for suffix in ( + ".rpt.json", + ".rpt", + ".cap", + ".fanout", + ".trans", + "_hold.skew", + "_setup.skew", + ): + add_file( + role="final.sta_report", + source=report_dir / f"{top_module}{suffix}", + destination=f"{report_dest}/{top_module}{suffix}", + required=suffix in (".rpt.json", ".rpt"), + ) + item["report"] = f"{report_dest}/{top_module}.rpt.json" + + rcx_output_dir = workspace_dir / "RCX_ecc" / "output" + spef_paths = sorted(rcx_output_dir.glob("*.spef")) if rcx_output_dir.is_dir() else [] + if expected_spefs: + for spef_name in sorted(expected_spefs): + add_file( + role="final.spef", + source=rcx_output_dir / spef_name, + destination=f"final/timing/spef/{spef_name}", + required=True, + ) + for spef_path in spef_paths: + if spef_path.name not in expected_spefs: + add_file( + role="final.spef", + source=spef_path, + destination=f"final/timing/spef/{spef_path.name}", + ) + elif spef_paths: + for spef_path in spef_paths: + add_file( + role="final.spef", + source=spef_path, + destination=f"final/timing/spef/{spef_path.name}", + required=True, + ) + else: + missing_required.append("RCX SPEF files") + + add_file("status.flow", flow_path, "final/reports/flow.json", required=True) + add_file( + "status.checklist", + checklist_path, + "final/reports/checklist.json", + required=True, + ) + + for step_name, step_dir in self._step_dirs().items(): + for kind in ("analysis", "report"): + self._copy_tree_files( + workspace_dir=workspace_dir, + package_dir=package_dir, + source_dir=workspace_dir / step_dir / kind, + destination_dir=f"final/reports/{step_name}/{kind}", + role=f"report.{kind}", + copied=copied, + ) + + if options.include_debug: + self._collect_debug_files( + workspace_dir=workspace_dir, + package_dir=package_dir, + copied=copied, + ) + + checklist_counts = self._checklist_counts(checklist_data) + if checklist_counts.get("warning", 0) or checklist_counts.get("failed", 0): + warnings.append( + "home checklist contains failed or warning items; " + "see final/reports/checklist.json" + ) + + metrics = self._read_json( + workspace_dir / "drc_ecc" / "analysis" / "drc_metrics.json" + ) + ok = len(missing_required) == 0 + flow_success = all( + state == StateEnum.Success.value + for state in required_steps.values() + ) + summary = { + "schema_version": 1, + "status": "ok" if ok else "incomplete", + "design": design, + "top_module": top_module, + "pdk": pdk_name, + "required_steps": required_steps, + "checks": { + "flow": "passed" if flow_success else "failed", + "home_checklist": checklist_counts, + }, + "initial": { + "verilog": f"initial/{design}.v", + "sdc": f"initial/{design}.sdc", + "parameters": "initial/parameters.json", + }, + "config": "config/", + "harden": { + "gds": f"harden/{design}.gds", + "lef": f"harden/{design}.lef", + "lib": f"harden/{design}.lib", + }, + "final": { + "verilog": f"final/design/{design}.v.gz", + "def": f"final/design/{design}.def.gz", + "gds": f"final/design/{design}.gds", + "image": f"final/design/{design}.png", + }, + "metrics": metrics, + "sta_matrix": sta_matrix, + "missing_required": missing_required, + "warnings": warnings, + } + summary_path = package_dir / "summary.json" + summary_path.write_text(json.dumps(summary, indent=2)) + + manifest = { + "schema_version": 1, + "created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "workspace": str(workspace_dir.resolve()), + "design": design, + "top_module": top_module, + "pdk": pdk_name, + "flow": { + "source": "home/flow.json", + "all_required_steps_success": flow_success, + }, + "files": copied, + "missing_required": missing_required, + "warnings": warnings, + } + manifest_path = package_dir / "manifest.json" + manifest_path.write_text(json.dumps(manifest, indent=2)) + + readme_path = package_dir / "README.md" + readme_path.write_text( + f"# {design} Signoff Package\n\n" + f"- Workspace: {workspace_dir.resolve()}\n" + f"- Status: {summary['status']}\n" + "- Harden outputs are under `harden/`.\n" + "- Final physical resources are under `final/`.\n" + ) + + archive_path = None + if options.archive and (ok or options.allow_incomplete): + archive_path = str(package_dir.with_suffix(".tar.gz")) + archive_file = Path(archive_path) + if archive_file.exists(): + archive_file.unlink() + with tarfile.open(archive_file, "w:gz") as archive: + archive.add(package_dir, arcname=package_dir.name) + + return SignoffPackageResult( + ok=ok, + package_dir=str(package_dir), + archive_path=archive_path, + manifest_path=str(manifest_path), + summary_path=str(summary_path), + copied=copied, + missing_required=missing_required, + warnings=warnings, + ) + + def _add_file(self, + workspace_dir: Path, + package_dir: Path, + role: str, + source: Path | None, + destination: str, + required: bool, + copied: list[dict], + missing_required: list[str]) -> None: + if source is None or not source.is_file() or source.stat().st_size <= 0: + if required: + missing_required.append(destination) + return + + target = package_dir / destination + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + copied.append({ + "role": role, + "required": required, + "source": self._source_path(workspace_dir, source), + "destination": destination, + "size_bytes": target.stat().st_size, + "sha256": self._sha256(target), + }) + + def _copy_tree_files(self, + workspace_dir: Path, + package_dir: Path, + source_dir: Path, + destination_dir: str, + role: str, + copied: list[dict]) -> None: + if not source_dir.is_dir(): + return + for source in sorted(path for path in source_dir.rglob("*") if path.is_file()): + relative = source.relative_to(source_dir).as_posix() + self._add_file( + workspace_dir=workspace_dir, + package_dir=package_dir, + role=role, + source=source, + destination=f"{destination_dir}/{relative}", + required=False, + copied=copied, + missing_required=[], + ) + + def _collect_debug_files(self, + workspace_dir: Path, + package_dir: Path, + copied: list[dict]) -> None: + patterns = [ + "*_ecc/feature/*", + "*_ecc/subflow.json", + "sta_ecc/output/**/wire_paths/*", + ] + for pattern in patterns: + for path_text in sorted(glob.glob(str(workspace_dir / pattern), recursive=True)): + source = Path(path_text) + if not source.is_file(): + continue + destination = f"debug/{source.relative_to(workspace_dir).as_posix()}" + self._add_file( + workspace_dir=workspace_dir, + package_dir=package_dir, + role="debug", + source=source, + destination=destination, + required=False, + copied=copied, + missing_required=[], + ) + output_db_dirs = sorted(workspace_dir.glob("*_ecc/output/*_db")) + output_view_dirs = sorted(workspace_dir.glob("*_ecc/output/*_view")) + for source in output_db_dirs + output_view_dirs: + if not source.is_dir(): + continue + self._copy_tree_files( + workspace_dir=workspace_dir, + package_dir=package_dir, + source_dir=source, + destination_dir=f"debug/{source.relative_to(workspace_dir).as_posix()}", + role="debug", + copied=copied, + ) + + def _read_json(self, path: Path) -> dict: + try: + with open(path, encoding="utf-8") as file: + data = json.load(file) + except (OSError, json.JSONDecodeError): + return {} + return data if isinstance(data, dict) else {} + + def _sha256(self, path: Path) -> str: + digest = hashlib.sha256() + with open(path, "rb") as file: + for chunk in iter(lambda: file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + def _path_from_config(self, + workspace_dir: Path, + path_text: str) -> Path | None: + if not path_text: + return None + path = Path(path_text) + if not path.is_absolute(): + path = workspace_dir / path + return path if path.is_file() else None + + def _source_path(self, workspace_dir: Path, source: Path) -> str: + try: + return source.relative_to(workspace_dir).as_posix() + except ValueError: + return str(source) + + def _find_one(self, + directory: Path, + preferred_name: str, + pattern: str, + missing_label: str, + missing_required: list[str]) -> Path | None: + preferred = directory / preferred_name + if preferred.is_file(): + return preferred + matches = sorted(directory.glob(pattern)) if directory.is_dir() else [] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + missing_required.append(f"{missing_label}: multiple matches") + return None + missing_required.append(missing_label) + return None + + def _design_from_outputs(self, workspace_dir: Path) -> str: + for pattern, suffix in ( + ("Harden_ecc/output/*_Harden.gds", "_Harden.gds"), + ("filler_ecc/output/*_filler.v.gz", "_filler.v.gz"), + ): + matches = sorted(workspace_dir.glob(pattern)) + if matches: + name = matches[0].name + if name.endswith(suffix): + return name[: -len(suffix)] + return "" + + def _required_step_states(self, flow_data: dict) -> dict: + required = [ + StepEnum.HARDEN.value, + StepEnum.RCX.value, + StepEnum.STA.value, + StepEnum.DRC.value, + StepEnum.FILLER.value, + StepEnum.ROUTING.value, + ] + state_by_step = { + step.get("name"): step.get("state", "") + for step in flow_data.get("steps", []) + if isinstance(step, dict) + } + return {step: state_by_step.get(step, "") for step in required} + + def _checklist_counts(self, checklist_data: dict) -> dict: + counts = {"passed": 0, "warning": 0, "failed": 0} + for item in checklist_data.get("checklist", []): + state = str(item.get("state", "")).lower() + if state == "passed": + counts["passed"] += 1 + elif state == "warning": + counts["warning"] += 1 + elif state == "failed": + counts["failed"] += 1 + return counts + + def _sta_matrix(self, sta_config: dict) -> list[dict]: + liberty_by_corner = { + item.get("corner"): item + for item in sta_config.get("liberty", []) + if isinstance(item, dict) + } + matrix = [] + for signoff_group in sta_config.get("signoff", []): + if not isinstance(signoff_group, dict): + continue + for lib_corner, rcx_corners in signoff_group.items(): + liberty = liberty_by_corner.get(lib_corner, {}) + if isinstance(rcx_corners, str): + rcx_corners = [rcx_corners] + for rcx_corner in rcx_corners: + matrix.append({ + "lib_corner": lib_corner, + "temperature": liberty.get("temperature", ""), + "rcx_corner": rcx_corner, + }) + return matrix + + def _temperature_token(self, temperature) -> str: + try: + numeric = float(temperature) + if numeric.is_integer(): + temperature = int(numeric) + except (TypeError, ValueError): + pass + return str(temperature).replace("-", "m").replace(".", "p") + + def _step_dirs(self) -> dict[str, str]: + return { + StepEnum.SYNTHESIS.value: "Synthesis_yosys", + StepEnum.FLOORPLAN.value: "Floorplan_ecc", + StepEnum.NETLIST_OPT.value: "fixFanout_ecc", + StepEnum.PLACEMENT.value: "place_dreamplace", + StepEnum.CTS.value: "CTS_ecc", + StepEnum.LEGALIZATION.value: "legalization_dreamplace", + StepEnum.ROUTING.value: "route_ecc", + StepEnum.DRC.value: "drc_ecc", + StepEnum.FILLER.value: "filler_ecc", + StepEnum.RCX.value: "RCX_ecc", + StepEnum.STA.value: "sta_ecc", + StepEnum.HARDEN.value: "Harden_ecc", + } diff --git a/chipcompiler/thirdparty/ecc-tools b/chipcompiler/thirdparty/ecc-tools index 2e3928a7..45b4f883 160000 --- a/chipcompiler/thirdparty/ecc-tools +++ b/chipcompiler/thirdparty/ecc-tools @@ -1 +1 @@ -Subproject commit 2e3928a78f8bca971324beba70bac33945904f87 +Subproject commit 45b4f88338d631f6b0b34ce2d8dd4a991a9bdefe diff --git a/chipcompiler/tools/ecc/module.py b/chipcompiler/tools/ecc/module.py index a351f33a..5eaee77f 100644 --- a/chipcompiler/tools/ecc/module.py +++ b/chipcompiler/tools/ecc/module.py @@ -224,11 +224,39 @@ def json_save(self, path : str): self.ecc.json_save(path=path) - def view_json_save(self, output_dir: str): - return self.ecc.view_json_save(output_dir=output_dir) + def view_json_save( + self, + output_dir: str, + json_format: str = "pretty", + compress: bool = False, + ): + """ + Export the current iDB design as a view JSON package. - def view_json_apply_edits(self, edits_path: str): - return self.ecc.view_json_apply_edits(edits_path=edits_path) + Args: + output_dir: Directory used to write manifest.json and package files. + json_format: JSON text layout. Use "pretty" for indented output or + "compact" to remove extra spaces/newlines and reduce file size. + compress: When True, write package JSON files as .json.gz. The + manifest.json entry file remains plain JSON and points to the + compressed package files. + """ + return self.ecc.view_json_save( + output_dir=output_dir, + json_format=json_format, + compress=compress, + ) + + def view_json_apply_edits(self, edits_path: str, compress: bool = False): + """ + Apply edits generated for a view JSON package. + + Args: + edits_path: Path to layout_edits.json or layout_edits.json.gz. + compress: When True, prefer reading edits_path + ".gz" if edits_path + does not already end with ".gz". + """ + return self.ecc.view_json_apply_edits(edits_path=edits_path, compress=compress) def save_data(self, path: str): """save ECC data""" diff --git a/chipcompiler/tools/ecc/runner.py b/chipcompiler/tools/ecc/runner.py index efa0bf23..5fde8d79 100644 --- a/chipcompiler/tools/ecc/runner.py +++ b/chipcompiler/tools/ecc/runner.py @@ -142,8 +142,11 @@ def save_data(workspace: Workspace, ecc_module.gds_save(output_path=step.output.get("gds", "")) ecc_module.save_data(path=step.output.get("db", "")) # ecc_module.json_save(path=step.output.get("json", "")) - ecc_module.view_json_save(output_dir=step.output.get("view_json", "")) - ecc_module.view_json_apply_edits(edits_path=step.output.get("view_json_edits", "")) + ecc_module.view_json_save(output_dir=step.output.get("view_json", ""), + json_format="compact", + compress=True) + ecc_module.view_json_apply_edits(edits_path=step.output.get("view_json_edits", ""), + compress=True) ecc_module.feature_sammry(json_path=step.feature.get("db", "")) if feature_step: ecc_module.feature_step(step=step.name, diff --git a/test/cli/test_workspace_cli.py b/test/cli/test_workspace_cli.py index 9b2146a2..9f0ab428 100644 --- a/test/cli/test_workspace_cli.py +++ b/test/cli/test_workspace_cli.py @@ -935,7 +935,10 @@ def prepare_workspace_for_rerun(workspace, engine_flow): engine_flow.prepared_for_rerun = True engine_flow.call_order.append(("prepare_rerun", workspace.directory)) - monkeypatch.setattr("chipcompiler.data.prepare_workspace_for_rerun", prepare_workspace_for_rerun) + monkeypatch.setattr( + "chipcompiler.data.prepare_workspace_for_rerun", + prepare_workspace_for_rerun, + ) rc = cli_main.run(["workspace", "run-flow", "--directory", str(ws), "--rerun", "--json"]) @@ -977,7 +980,10 @@ def test_run_flow_rerun_stops_when_prepare_fails(monkeypatch, tmp_path, capsys): def prepare_workspace_for_rerun(workspace, engine_flow): raise RuntimeError("cleanup failed") - monkeypatch.setattr("chipcompiler.data.prepare_workspace_for_rerun", prepare_workspace_for_rerun) + monkeypatch.setattr( + "chipcompiler.data.prepare_workspace_for_rerun", + prepare_workspace_for_rerun, + ) rc = cli_main.run(["workspace", "run-flow", "--directory", str(ws), "--rerun", "--json"]) @@ -1170,6 +1176,7 @@ def test_workspace_help_uses_typer_app(capsys): assert "Usage: ecc workspace" in out assert "create" in out assert "run-flow" in out + assert "signoff" in out def test_workspace_create_help_lists_existing_options(capsys): @@ -1187,6 +1194,66 @@ def test_workspace_create_help_lists_existing_options(capsys): assert "--freq" in out +def test_workspace_signoff_routes_to_service(monkeypatch, tmp_path, capsys): + calls = {} + + def fake_collect_signoff_package( + directory, + output_dir, + archive, + include_debug, + allow_incomplete, + ): + calls.update({ + "directory": directory, + "output_dir": output_dir, + "archive": archive, + "include_debug": include_debug, + "allow_incomplete": allow_incomplete, + }) + return { + "cmd": "signoff", + "response": "success", + "data": { + "package_dir": str(tmp_path / "out" / "gcd_signoff_package"), + "archive_path": "", + "copied_count": 3, + }, + "message": ["ok"], + } + + monkeypatch.setattr( + "chipcompiler.cli.commands.workspace.collect_signoff_package", + fake_collect_signoff_package, + ) + + rc = cli_main.run([ + "workspace", + "signoff", + "--directory", + str(tmp_path / "workspace"), + "--output", + str(tmp_path / "out"), + "--no-archive", + "--include-debug", + "--allow-incomplete", + "--json", + ]) + + result = _response(capsys) + assert rc == 0 + assert result["cmd"] == "signoff" + assert result["response"] == "success" + assert result["data"]["copied_count"] == 3 + assert calls == { + "directory": str(tmp_path / "workspace"), + "output_dir": str(tmp_path / "out"), + "archive": False, + "include_debug": True, + "allow_incomplete": True, + } + + def test_workspace_json_output_suppresses_runtime_stdout(monkeypatch, tmp_path, capsys): from chipcompiler.cli.workspace.response import workspace_response diff --git a/test/test_ecc_tools_module.py b/test/test_ecc_tools_module.py index f31248f3..b2bc4dfb 100644 --- a/test/test_ecc_tools_module.py +++ b/test/test_ecc_tools_module.py @@ -55,25 +55,25 @@ def test_init_rcx_omits_explicit_empty_pdk_for_backward_compatibility(): assert module.ecc.calls == [{"config": "/tmp/rcx.json"}] -def test_view_json_save_passes_output_dir(): +def test_view_json_save_passes_output_options(): module = ECCToolsModule.__new__(ECCToolsModule) module.ecc = FakeEcc() - assert module.view_json_save(output_dir="/tmp/view_json") is True + assert module.view_json_save(output_dir="/tmp/view_json", json_format="compact", compress=True) is True assert module.ecc.calls == [ - ("view_json_save", {"output_dir": "/tmp/view_json"}), + ("view_json_save", {"output_dir": "/tmp/view_json", "json_format": "compact", "compress": True}), ] -def test_view_json_apply_edits_passes_edits_path(): +def test_view_json_apply_edits_passes_compress_option(): module = ECCToolsModule.__new__(ECCToolsModule) module.ecc = FakeEcc() - assert module.view_json_apply_edits(edits_path="/tmp/view_json/edits/layout_edits.json") is True + assert module.view_json_apply_edits(edits_path="/tmp/view_json/edits/layout_edits.json.gz", compress=True) is True assert module.ecc.calls == [ - ("view_json_apply_edits", {"edits_path": "/tmp/view_json/edits/layout_edits.json"}), + ("view_json_apply_edits", {"edits_path": "/tmp/view_json/edits/layout_edits.json.gz", "compress": True}), ] diff --git a/test/test_harden.py b/test/test_harden.py index 884d137e..7ec7d9fe 100644 --- a/test/test_harden.py +++ b/test/test_harden.py @@ -21,9 +21,11 @@ from chipcompiler.engine import ( EngineDB, - EngineFlow + EngineFlow, + SignoffPackageOptions ) + def test_ics55_gcd(): workspace_dir="{}/test/examples/ics55_gcd_harden".format(root) @@ -52,6 +54,18 @@ def test_ics55_gcd(): engine_flow.create_step_workspaces() assert engine_flow.run_steps() + + # result = engine_flow.collect_signoff_package( + # SignoffPackageOptions( + # output_dir=f"{workspace_dir}/signoff", + # archive=True, + # include_debug=False, + # allow_incomplete=False, + # ) + # ) + + # assert result.ok, result.missing_required + # assert result.archive_path if __name__ == "__main__": test_ics55_gcd() diff --git a/test/test_signoff_package.py b/test/test_signoff_package.py new file mode 100644 index 00000000..a4bdaeff --- /dev/null +++ b/test/test_signoff_package.py @@ -0,0 +1,109 @@ +import json +from pathlib import Path + +from chipcompiler.data import OriginDesign, Parameters, StateEnum, Workspace +from chipcompiler.engine import EngineFlow +from chipcompiler.engine.signoff import SignoffPackageOptions + + +def _write(path: Path, text: str = "data") -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + + +def _write_json(path: Path, data: dict) -> None: + _write(path, json.dumps(data, indent=2)) + + +def _make_signoff_workspace(tmp_path: Path) -> Path: + workspace_dir = tmp_path / "gcd_workspace" + design = "gcd" + + _write(workspace_dir / "origin" / f"{design}.v", "module gcd; endmodule\n") + _write(workspace_dir / "origin" / f"{design}.sdc", "create_clock -period 10 clk\n") + _write_json( + workspace_dir / "home" / "parameters.json", + {"Design": design, "Top module": design, "PDK": "ics55"}, + ) + _write_json( + workspace_dir / "home" / "flow.json", + { + "steps": [ + {"name": "route", "tool": "ecc", "state": StateEnum.Success.value}, + {"name": "drc", "tool": "ecc", "state": StateEnum.Success.value}, + {"name": "filler", "tool": "ecc", "state": StateEnum.Success.value}, + {"name": "RCX", "tool": "ecc", "state": StateEnum.Success.value}, + {"name": "sta", "tool": "ecc", "state": StateEnum.Success.value}, + {"name": "Harden", "tool": "ecc", "state": StateEnum.Success.value}, + ], + }, + ) + _write_json(workspace_dir / "home" / "checklist.json", {"checklist": []}) + + _write_json( + workspace_dir / "config" / "sta.json", + { + "liberty": [{"corner": "MAX", "temperature": 125, "path": ["max.lib"]}], + "signoff": [{"MAX": ["RCworst"]}], + }, + ) + for config_name in ("db_default_config.json", "flow_config.json", "rcx.json"): + _write_json(workspace_dir / "config" / config_name, {}) + + _write(workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.gds") + _write(workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.lef") + _write(workspace_dir / "Harden_ecc" / "output" / f"{design}_Harden.lib") + _write(workspace_dir / "filler_ecc" / "output" / f"{design}_filler.v.gz") + _write(workspace_dir / "filler_ecc" / "output" / f"{design}_filler.def.gz") + _write(workspace_dir / "filler_ecc" / "output" / f"{design}_filler.gds") + _write(workspace_dir / "filler_ecc" / "output" / f"{design}_filler.png") + _write(workspace_dir / "RCX_ecc" / "output" / f"{design}_RCworst_125C.spef") + + sta_dir = workspace_dir / "sta_ecc" / "output" / "MAX_125" / "RCworst" + _write_json(sta_dir / f"{design}.rpt.json", {"slack": []}) + _write(sta_dir / f"{design}.rpt") + + _write_json(workspace_dir / "route_ecc" / "analysis" / "route_metrics.json", {}) + _write(workspace_dir / "route_ecc" / "report" / "route.db.rpt") + return workspace_dir + + +def _make_engine_flow(workspace_dir: Path) -> EngineFlow: + workspace = Workspace() + workspace.directory = str(workspace_dir) + workspace.design = OriginDesign(name="gcd", top_module="gcd") + workspace.flow.path = str(workspace_dir / "home" / "flow.json") + workspace.parameters = Parameters( + path=str(workspace_dir / "home" / "parameters.json"), + data={"Design": "gcd", "Top module": "gcd", "PDK": "ics55"}, + ) + return EngineFlow(workspace=workspace) + + +def test_collect_signoff_package_uses_final_design_layout(tmp_path): + workspace_dir = _make_signoff_workspace(tmp_path) + engine_flow = _make_engine_flow(workspace_dir) + + result = engine_flow.collect_signoff_package(SignoffPackageOptions(archive=True)) + + package_dir = Path(result.package_dir) + assert result.ok is True + assert (package_dir / "final" / "design" / "gcd.v.gz").is_file() + assert (package_dir / "final" / "design" / "gcd.def.gz").is_file() + assert (package_dir / "final" / "design" / "gcd.gds").is_file() + assert (package_dir / "final" / "design" / "gcd.png").is_file() + assert (package_dir / "final" / "timing" / "spef" / "gcd_RCworst_125C.spef").is_file() + assert (package_dir / "final" / "reports" / "flow.json").is_file() + assert not (package_dir / "signoff").exists() + assert not (package_dir / "final" / "final").exists() + + summary = json.loads((package_dir / "summary.json").read_text()) + assert summary["final"]["verilog"] == "final/design/gcd.v.gz" + assert summary["sta_matrix"][0]["report"] == ( + "final/timing/sta/MAX_125/RCworst/gcd.rpt.json" + ) + + manifest = json.loads((package_dir / "manifest.json").read_text()) + destinations = {item["destination"] for item in manifest["files"]} + assert "final/design/gcd.def.gz" in destinations + assert "final/reports/route/analysis/route_metrics.json" in destinations From 783fb42c6753cc31b1944d7ff4ca19b48399ed16 Mon Sep 17 00:00:00 2001 From: Yell-walkalone <12112088@qq.com> Date: Tue, 23 Jun 2026 17:06:00 +0800 Subject: [PATCH 10/10] fix bug for flow ci --- .github/workflows/ci.yml | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aec9846..4e391b29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,39 +53,8 @@ jobs: with: submodules: recursive - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Setup uv - uses: astral-sh/setup-uv@v3 - with: - version: latest - enable-cache: "true" - - - name: Install native build dependencies - run: | - set -euo pipefail - - packages=( - software-properties-common ca-certificates curl git xz-utils - build-essential python3 python3-dev python3-venv - cmake ninja-build pkg-config patchelf mold lld - flex libfl-dev bison unzip zip - zlib1g-dev libboost-all-dev libcairo2-dev - libgflags-dev libgoogle-glog-dev libeigen3-dev libgtest-dev - libtbb-dev libhwloc-dev libcurl4-openssl-dev libunwind-dev - libmetis-dev libgmp-dev tcl-dev tcl8.6-dev - libyaml-cpp-dev libqhull-dev libffi-dev libssl-dev - ) - - sudo apt-get update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" - - - name: Install uv - run: uv sync --no-build-isolation-package ecc-dreamplace --no-build-isolation-package ecc-tools-bin --verbose - # uses: ./.github/actions/setup-python-deps + - name: Setup Python dependencies + uses: ./.github/actions/setup-python-deps - name: Setup PDK run: |