edgeeda-agent / scripts /summarize_sh001_results.py
SamChYe's picture
Publish EdgeEDA agent
aa677e3 verified
#!/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()