#!/usr/bin/env python3 """Summarize sh001_29b345d42a results and generate paper-ready plots.""" from __future__ import annotations import csv import math import re from collections import Counter from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt RESULT_DIR = Path( "/Users/thalia/Desktop/EdgePPAgent/edgeeda-agent/OpenROAD-flow-scripts/flow/results/nangate45/gcd/sh001_29b345d42a" ) REPORT_DIR = Path( "/Users/thalia/Desktop/EdgePPAgent/edgeeda-agent/OpenROAD-flow-scripts/flow/reports/nangate45/gcd/sh001_29b345d42a" ) LOG_DIR = Path( "/Users/thalia/Desktop/EdgePPAgent/edgeeda-agent/OpenROAD-flow-scripts/flow/logs/nangate45/gcd/sh001_29b345d42a" ) DEF_PATH = RESULT_DIR / "6_final.def" V_PATH = RESULT_DIR / "6_final.v" MEM_PATH = RESULT_DIR / "mem.json" FINISH_RPT_PATH = REPORT_DIR / "6_finish.rpt" REPORT_LOG_PATH = LOG_DIR / "6_report.log" OUT_FIG_DIR = Path( "/Users/thalia/Desktop/EdgePPAgent/edgeeda-agent/IEEE_EdgeEDA_Agent_ISVLSI/figures" ) OUT_CSV = Path( "/Users/thalia/Desktop/EdgePPAgent/edgeeda-agent/runs/sh001_29b345d42a_summary.csv" ) OUT_TEX = Path( "/Users/thalia/Desktop/EdgePPAgent/edgeeda-agent/IEEE_EdgeEDA_Agent_ISVLSI/gcd_sh001_results_table.tex" ) def parse_def_metrics(def_text: str) -> dict: metrics = {} units_match = re.search(r"^UNITS\s+DISTANCE\s+MICRONS\s+(\d+)\s*;", def_text, re.M) die_match = re.search(r"^DIEAREA\s*\(\s*(\d+)\s+(\d+)\s*\)\s*\(\s*(\d+)\s+(\d+)\s*\)\s*;", def_text, re.M) comp_match = re.search(r"^COMPONENTS\s+(\d+)\s*;", def_text, re.M) pin_match = re.search(r"^PINS\s+(\d+)\s*;", def_text, re.M) net_match = re.search(r"^NETS\s+(\d+)\s*;", def_text, re.M) row_count = len(re.findall(r"^ROW\s+", def_text, re.M)) if units_match: metrics["units_per_micron"] = int(units_match.group(1)) if die_match: x0, y0, x1, y1 = map(int, die_match.groups()) metrics["die_x0"] = x0 metrics["die_y0"] = y0 metrics["die_x1"] = x1 metrics["die_y1"] = y1 if comp_match: metrics["components"] = int(comp_match.group(1)) if pin_match: metrics["pins"] = int(pin_match.group(1)) if net_match: metrics["nets"] = int(net_match.group(1)) metrics["rows"] = row_count if "units_per_micron" in metrics and "die_x1" in metrics: units = metrics["units_per_micron"] width = (metrics["die_x1"] - metrics["die_x0"]) / units height = (metrics["die_y1"] - metrics["die_y0"]) / units metrics["die_width_um"] = width metrics["die_height_um"] = height metrics["die_area_um2"] = width * height return metrics def parse_def_cell_counts(def_text: str) -> Counter: comp_match = re.search(r"^COMPONENTS\s+\d+\s*;\n(.*?)\nEND COMPONENTS", def_text, re.S | re.M) if not comp_match: return Counter() section = comp_match.group(1) counts = Counter() for line in section.splitlines(): line = line.strip() if not line.startswith("-"): continue parts = line.split() if len(parts) >= 3: counts[parts[2]] += 1 return counts def parse_netlist_cell_counts(v_text: str) -> Counter: pattern = re.compile(r"^\s*([A-Za-z_][\w$]*)\s+([A-Za-z_][\w$]*)\s*\(", re.M) counts = Counter() for cell, _inst in pattern.findall(v_text): if cell in {"module", "endmodule", "input", "output", "wire", "reg", "assign", "always"}: continue counts[cell] += 1 return counts def parse_finish_rpt(text: str) -> dict: metrics = {} if not text: return metrics tns_match = re.search(r"tns max\s+([+-]?\d+(?:\.\d+)?)", text) wns_match = re.search(r"wns max\s+([+-]?\d+(?:\.\d+)?)", text) worst_match = re.search(r"worst slack max\s+([+-]?\d+(?:\.\d+)?)", text) period_match = re.search(r"period_min\s*=\s*([0-9.]+)\s+fmax\s*=\s*([0-9.]+)", text) if tns_match: metrics["tns_ns"] = float(tns_match.group(1)) if wns_match: metrics["wns_ns"] = float(wns_match.group(1)) if worst_match: metrics["worst_slack_ns"] = float(worst_match.group(1)) if period_match: metrics["clock_period_ns"] = float(period_match.group(1)) metrics["clock_fmax_mhz"] = float(period_match.group(2)) return metrics def parse_report_log(text: str) -> dict: metrics = {} if not text: return metrics design_match = re.search(r"Design area\s+([0-9.]+)\s+um\^2\s+([0-9.]+)% utilization", text) power_match = re.search(r"Total power\s*:\s*([0-9.eE+-]+)\s*W", text) ir_avg_match = re.search(r"Average IR drop\s*:\s*([0-9.eE+-]+)\s*V", text) ir_worst_match = re.search(r"Worstcase IR drop:\s*([0-9.eE+-]+)\s*V", text) ir_pct_match = re.search(r"Percentage drop\s*:\s*([0-9.eE+-]+)\s*%", text) total_cells_match = re.search(r"^\s*Total\s+(\d+)\s+([0-9.]+)\s*$", text, re.M) if design_match: metrics["design_area_um2"] = float(design_match.group(1)) metrics["design_utilization_pct"] = float(design_match.group(2)) if power_match: metrics["total_power_w"] = float(power_match.group(1)) if ir_avg_match: metrics["ir_drop_avg_v"] = float(ir_avg_match.group(1)) if ir_worst_match: metrics["ir_drop_worst_v"] = float(ir_worst_match.group(1)) if ir_pct_match: metrics["ir_drop_pct"] = float(ir_pct_match.group(1)) if total_cells_match: metrics["cell_total_count"] = int(total_cells_match.group(1)) metrics["cell_total_area_um2"] = float(total_cells_match.group(2)) return metrics def classify_cells(counts: Counter) -> dict: categories = { "filler": 0, "tap": 0, "sequential": 0, "combinational": 0, "other": 0, } for cell, count in counts.items(): ucell = cell.upper() if "FILL" in ucell: categories["filler"] += count elif "TAP" in ucell: categories["tap"] += count elif "DFF" in ucell or "LATCH" in ucell: categories["sequential"] += count elif re.match(r"[A-Z]+\d+_X\d+", ucell) or any(k in ucell for k in ["NAND", "NOR", "AOI", "OAI", "INV", "BUF", "XOR", "XNOR"]): categories["combinational"] += count else: categories["other"] += count return categories def write_summary_csv(metrics: dict, def_counts: Counter, v_counts: Counter, categories: dict) -> None: OUT_CSV.parent.mkdir(parents=True, exist_ok=True) with OUT_CSV.open("w", newline="") as f: writer = csv.writer(f) writer.writerow(["metric", "value"]) for key in [ "components", "pins", "nets", "rows", "units_per_micron", "die_width_um", "die_height_um", "die_area_um2", "tns_ns", "wns_ns", "worst_slack_ns", "clock_period_ns", "clock_fmax_mhz", "design_area_um2", "design_utilization_pct", "total_power_w", "ir_drop_avg_v", "ir_drop_worst_v", "ir_drop_pct", "cell_total_count", "cell_total_area_um2", ]: if key in metrics: writer.writerow([key, metrics[key]]) writer.writerow(["def_instance_total", sum(def_counts.values())]) writer.writerow(["netlist_instance_total", sum(v_counts.values())]) for k, v in categories.items(): writer.writerow([f"category_{k}", v]) def write_latex_table(metrics: dict, def_counts: Counter, categories: dict) -> None: OUT_TEX.parent.mkdir(parents=True, exist_ok=True) total = sum(def_counts.values()) def pct(x): return 0.0 if total == 0 else (100.0 * x / total) def fmt_num(value, fmt: str) -> str: try: return format(float(value), fmt) except (TypeError, ValueError): return "n/a" die_width = fmt_num(metrics.get("die_width_um"), ".3f") die_height = fmt_num(metrics.get("die_height_um"), ".3f") die_area = fmt_num(metrics.get("die_area_um2"), ".2f") wns = fmt_num(metrics.get("wns_ns"), ".3f") tns = fmt_num(metrics.get("tns_ns"), ".3f") worst_slack = fmt_num(metrics.get("worst_slack_ns"), ".3f") period = fmt_num(metrics.get("clock_period_ns"), ".3f") fmax = fmt_num(metrics.get("clock_fmax_mhz"), ".2f") design_area = fmt_num(metrics.get("design_area_um2"), ".2f") util = fmt_num(metrics.get("design_utilization_pct"), ".1f") power_w = metrics.get("total_power_w") power_mw = fmt_num(power_w * 1e3 if power_w is not None else None, ".3f") ir_avg = fmt_num(metrics.get("ir_drop_avg_v"), ".4f") ir_worst = fmt_num(metrics.get("ir_drop_worst_v"), ".4f") ir_pct = fmt_num(metrics.get("ir_drop_pct"), ".2f") lines = [ r"\\begin{table}[t]", r"\\caption{Post-route summary for \texttt{nangate45/gcd/sh001\_29b345d42a}.}", r"\\label{tab:postroute_sh001}", r"\\centering", r"\\small", r"\\begin{tabular}{@{}ll@{}}", r"\\toprule", r"Metric & Value \\", r"\\midrule", f"Components & {metrics.get('components', 'n/a')} \\\\", f"Pins / nets & {metrics.get('pins', 'n/a')} / {metrics.get('nets', 'n/a')} \\\\", f"Rows & {metrics.get('rows', 'n/a')} \\\\", rf"Die size ($\mu m$) & {die_width} $\times$ {die_height} \\", rf"Die area ($\mu m^2$) & {die_area} \\", f"WNS / TNS / worst (ns) & {wns} / {tns} / {worst_slack} \\\\", f"Clock period / fmax & {period} ns / {fmax} MHz \\\\", rf"Design area / util & {design_area} $\mu m^2$ / {util}\% \\", f"Total power & {power_mw} mW \\\\", rf"IR drop avg / worst / \% & {ir_avg} / {ir_worst} / {ir_pct} \\", f"Filler / tap cells & {categories['filler']} ({pct(categories['filler']):.1f}\\%) / {categories['tap']} ({pct(categories['tap']):.1f}\\%) \\\\", f"Sequential / combinational & {categories['sequential']} ({pct(categories['sequential']):.1f}\\%) / {categories['combinational']} ({pct(categories['combinational']):.1f}\\%) \\\\", r"\\bottomrule", r"\\end{tabular}", r"\\end{table}", "", ] OUT_TEX.write_text("\n".join(lines)) def plot_top_cell_types(def_counts: Counter) -> None: OUT_FIG_DIR.mkdir(parents=True, exist_ok=True) top = def_counts.most_common(10) labels = [k for k, _ in top] values = [v for _k, v in top] fig, ax = plt.subplots(figsize=(7.2, 4.2)) bars = ax.bar(labels, values, color="#4B8BBE") ax.set_ylabel("Instance count") ax.set_title("Top cell types (post-route DEF)") ax.tick_params(axis="x", rotation=45, labelsize=8) for bar, value in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), str(value), ha="center", va="bottom", fontsize=7) fig.tight_layout() for ext in ("png", "pdf"): fig.savefig(OUT_FIG_DIR / f"gcd_sh001_celltype_top10.{ext}", dpi=300) plt.close(fig) def plot_category_pie(categories: dict) -> None: OUT_FIG_DIR.mkdir(parents=True, exist_ok=True) labels = ["Combinational", "Sequential", "Filler", "Tap", "Other"] values = [ categories["combinational"], categories["sequential"], categories["filler"], categories["tap"], categories["other"], ] fig, ax = plt.subplots(figsize=(5.0, 4.2)) ax.pie(values, labels=labels, autopct=lambda p: f"{p:.1f}%" if p > 0 else "") ax.set_title("Cell category mix (post-route DEF)") fig.tight_layout() for ext in ("png", "pdf"): fig.savefig(OUT_FIG_DIR / f"gcd_sh001_celltype_categories.{ext}", dpi=300) plt.close(fig) def main() -> None: def_text = DEF_PATH.read_text() v_text = V_PATH.read_text() metrics = parse_def_metrics(def_text) finish_metrics = parse_finish_rpt(FINISH_RPT_PATH.read_text() if FINISH_RPT_PATH.exists() else "") report_metrics = parse_report_log(REPORT_LOG_PATH.read_text() if REPORT_LOG_PATH.exists() else "") metrics.update(finish_metrics) metrics.update(report_metrics) def_counts = parse_def_cell_counts(def_text) v_counts = parse_netlist_cell_counts(v_text) categories = classify_cells(def_counts) write_summary_csv(metrics, def_counts, v_counts, categories) write_latex_table(metrics, def_counts, categories) plot_top_cell_types(def_counts) plot_category_pie(categories) if __name__ == "__main__": main()