Skip to content

Commit 0364faa

Browse files
oharboeclaude
andcommitted
flow/designs: add cross-PDK WNS estimate-accuracy view
Extend plot_wns.py to quantify how well the cts and globalroute worst-slack estimates predict the final WNS. Each design's per-stage error (stage - finish) is normalized by its clock period, parsed from the .sdc, so PDKs with different timing units are comparable. Adds flow/designs/wns_accuracy.png (per-PDK strip plot of normalized estimate error, + optimistic / - pessimistic) and a new flow/designs/README.md with a "## WNS estimate accuracy across PDKs" section: a per-PDK MAE/bias table plus hand-written findings. Covers the 67 designs across 8 PDKs that expose cts/globalroute slack and a parsable clock period; the rest are noted as omitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
1 parent 9b94d86 commit 0364faa

3 files changed

Lines changed: 240 additions & 10 deletions

File tree

flow/designs/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# ORFS designs
2+
3+
## Findings: how accurate are early-stage WNS estimates?
4+
5+
Reading the committed `rules-base.json` baselines, normalized by each design's clock
6+
period (parsed from its `.sdc`), the picture across the 67 designs / 8 PDKs that expose
7+
`cts` and `globalroute` slack is:
8+
9+
- **Global route usually tightens the estimate.** For most PDKs the mean absolute error
10+
drops from `cts` to `globalroute` — dramatically for `sky130hs` (10.5% → 1.9%) and
11+
`gt2n` (3.3% → 0.0%), and clearly for `gf12` (2.2% → 1.1%) and `ihp-sg13g2`
12+
(0.6% → 0.0%). `ihp-sg13g2` and `gf180` are already accurate at `cts`.
13+
14+
- **`cts` is biased optimistic; `globalroute` often overshoots into pessimism.** Every
15+
PDK's `cts` bias is ≥ 0 (cts reports more slack than the design finally closes with),
16+
whereas `globalroute` bias flips negative for `sky130hd` (−3.5%), `sky130hs` (−1.9%),
17+
`gf12` (−1.1%) and `nangate45` (−0.5%). Global route tends to *over-correct*.
18+
19+
- **`sky130hd` is the exception where routing makes the estimate worse**, not better:
20+
`globalroute` MAE (3.5%) exceeds `cts` MAE (2.9%), and it is consistently pessimistic.
21+
22+
- **Outliers are design-specific, not PDK-wide.** `sky130hs/gcd` has `cts` +45.9%
23+
(wildly optimistic, fully corrected by `globalroute`), and `asap7/swerv_wrapper` is
24+
+14.9% optimistic at *both* stages — the cases most likely to mislead an early-stage
25+
go/no-go decision.
26+
27+
Practical reading: `cts` slack is a usable optimistic rank-ordering; `globalroute` is the
28+
first estimate within a few % of final for most PDKs, but on `sky130hd` (and for specific
29+
designs elsewhere) even `globalroute` can be off by 3–10% of the clock period. This is the
30+
design-level companion to the per-net GRT-vs-RCX divergence in
31+
[`flow/docs/rcx`](../docs/rcx/README.md) (PR #4302). Per-PDK design breakdowns:
32+
[asap7](asap7/README.md), [nangate45](nangate45/README.md), [sky130hd](sky130hd/README.md),
33+
[sky130hs](sky130hs/README.md), [gf12](gf12/README.md), [gf180](gf180/README.md),
34+
[gt2n](gt2n/README.md), [ihp-sg13g2](ihp-sg13g2/README.md).
35+
36+
<!-- BEGIN WNS-ACCURACY (generated by flow/util/plot_wns.py) -->
37+
## WNS estimate accuracy across PDKs
38+
39+
How closely the earlier-stage worst-slack estimates (`cts`, `globalroute`) match the final (`finish`) WNS, per design, normalized by that design's clock period so PDKs with different timing units are comparable. Error is `(stage − finish) / clock_period`; **positive = optimistic** (the stage reported more slack than the design actually closes with), negative = pessimistic. Clock period is parsed from each design's `.sdc`; designs whose period could not be parsed are omitted.
40+
41+
![WNS estimate accuracy by stage, across PDKs](wns_accuracy.png)
42+
43+
Mean absolute error (MAE) and mean signed error (bias), in % of clock period:
44+
45+
| PDK | designs | cts MAE | cts bias | grt MAE | grt bias | worst (design) |
46+
| --- | ---: | ---: | ---: | ---: | ---: | --- |
47+
| asap7 | 16 | 2.8% | +1.5% | 2.9% | +1.1% | +14.9% (swerv_wrapper globalroute) |
48+
| gf12 | 9 | 2.2% | -2.2% | 1.1% | -1.1% | -14.2% (jpeg cts) |
49+
| gf180 | 5 | 1.0% | +1.0% | 0.4% | +0.3% | +4.3% (aes cts) |
50+
| gt2n | 3 | 3.3% | +3.3% | 0.0% | +0.0% | +10.0% (aes cts) |
51+
| ihp-sg13g2 | 7 | 0.6% | +0.6% | 0.0% | +0.0% | +4.5% (spi cts) |
52+
| nangate45 | 15 | 0.8% | +0.6% | 1.0% | -0.5% | -3.2% (black_parrot globalroute) |
53+
| sky130hd | 7 | 2.9% | +2.0% | 3.5% | -3.5% | -10.0% (gcd globalroute) |
54+
| sky130hs | 5 | 10.5% | +10.5% | 1.9% | -1.9% | +45.9% (gcd cts) |
55+
56+
_Generated by `flow/util/plot_wns.py`; regenerate with `python3 flow/util/plot_wns.py`._
57+
<!-- END WNS-ACCURACY -->

flow/designs/wns_accuracy.png

82.8 KB
Loading

flow/util/plot_wns.py

Lines changed: 183 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
flow/designs/<pdk>/wns.png -- horizontal bar chart of finish-stage WNS
1515
flow/designs/<pdk>/README.md -- a "## WNS" section (between generated markers)
1616
17+
It also produces a cross-PDK view of how well the cts/globalroute estimates predict the
18+
final WNS (each design's per-stage estimate error, normalized by its clock period so the
19+
PDKs are comparable):
20+
21+
flow/designs/wns_accuracy.png -- per-PDK strip plot of normalized estimate error
22+
flow/designs/README.md -- a "## WNS estimate accuracy across PDKs" section
23+
1724
No OpenROAD/ORFS flow run is required -- the data is already in the tree, so the plots
1825
are deterministic and reproducible. Run from anywhere in the repo:
1926
@@ -25,8 +32,10 @@
2532
"""
2633

2734
import argparse
35+
import glob
2836
import json
2937
import os
38+
import re
3039
import sys
3140

3241
import matplotlib
@@ -45,6 +54,37 @@
4554
BEGIN = "<!-- BEGIN WNS (generated by flow/util/plot_wns.py) -->"
4655
END = "<!-- END WNS -->"
4756

57+
ACC_BEGIN = "<!-- BEGIN WNS-ACCURACY (generated by flow/util/plot_wns.py) -->"
58+
ACC_END = "<!-- END WNS-ACCURACY -->"
59+
60+
# Estimate stages whose accuracy (vs finish) we report, in flow order.
61+
EST_STAGES = ["cts", "globalroute"]
62+
EST_MARKERS = {"cts": "v", "globalroute": "^"}
63+
64+
_PERIOD_RE = (
65+
re.compile(r"set\s+clk_period\s+([0-9.]+)"),
66+
re.compile(r"create_clock[^\n]*-period\s+([0-9.]+)"),
67+
)
68+
69+
70+
def clock_period(design_dir):
71+
"""Clock period for a design, parsed from its .sdc, or None if not found.
72+
73+
Handles the two idioms used across PDKs: `set clk_period <N>` and
74+
`create_clock ... -period <N>`. Returns the first match (designs here are
75+
single-clock); units are the PDK's native timing unit, same as the WNS values.
76+
"""
77+
for sdc in sorted(glob.glob(os.path.join(design_dir, "*.sdc"))):
78+
try:
79+
text = open(sdc, errors="ignore").read()
80+
except OSError:
81+
continue
82+
for rx in _PERIOD_RE:
83+
m = rx.search(text)
84+
if m:
85+
return float(m.group(1))
86+
return None
87+
4888

4989
def designs_dir():
5090
"""flow/designs, located relative to this script (flow/util/plot_wns.py)."""
@@ -153,23 +193,140 @@ def wns_section(pdk, rows):
153193
return "\n".join(lines) + "\n"
154194

155195

156-
def write_readme(readme_path, pdk, section):
157-
"""Create README or replace the WNS section between markers, preserving prose."""
158-
if os.path.isfile(readme_path):
159-
with open(readme_path) as f:
196+
def splice_readme(path, section, begin, end, title):
197+
"""Create README, or replace the marked section in place (preserving prose)."""
198+
if os.path.isfile(path):
199+
with open(path) as f:
160200
text = f.read()
161-
if BEGIN in text and END in text:
162-
pre = text[: text.index(BEGIN)]
163-
post = text[text.index(END) + len(END):]
201+
if begin in text and end in text:
202+
pre = text[: text.index(begin)]
203+
post = text[text.index(end) + len(end):]
164204
new = pre + section + post.lstrip("\n")
165205
else:
166206
new = text.rstrip("\n") + "\n\n" + section
167207
else:
168-
new = f"# {pdk} designs\n\n" + section
169-
with open(readme_path, "w") as f:
208+
new = (f"# {title}\n\n" if title else "") + section
209+
with open(path, "w") as f:
170210
f.write(new)
171211

172212

213+
# --- cross-PDK estimate accuracy --------------------------------------------
214+
215+
def collect_accuracy(pdk_dir):
216+
"""Per-design normalized estimate error vs finish, in % of the clock period.
217+
218+
Returns [(design, {stage: err_pct}), ...] for designs that have a clock period and
219+
finish + estimate-stage WNS. err_pct = 100 * (stage_ws - finish_ws) / period;
220+
positive means the stage was *optimistic* (reported more slack than the final result).
221+
Normalizing by clock period makes the error comparable across PDKs with different units.
222+
"""
223+
out = []
224+
for design in sorted(os.listdir(pdk_dir)):
225+
ddir = os.path.join(pdk_dir, design)
226+
rules = os.path.join(ddir, "rules-base.json")
227+
if not os.path.isfile(rules):
228+
continue
229+
period = clock_period(ddir)
230+
fin = load_value(rules, FINISH_KEY)
231+
if not period or fin is None:
232+
continue
233+
errs = {}
234+
for stage in EST_STAGES:
235+
v = load_value(rules, f"{stage}__timing__setup__ws")
236+
if v is not None:
237+
errs[stage] = 100.0 * (v - fin) / period
238+
if errs:
239+
out.append((design, errs))
240+
return out
241+
242+
243+
def _stats(rows, stage):
244+
vals = [e[stage] for _, e in rows if stage in e]
245+
if not vals:
246+
return None
247+
mae = sum(abs(v) for v in vals) / len(vals)
248+
bias = sum(vals) / len(vals)
249+
return len(vals), mae, bias, max(vals, key=abs)
250+
251+
252+
def plot_accuracy(acc, out_png):
253+
"""Strip plot: per-PDK distribution of cts/globalroute estimate error vs finish."""
254+
pdks = sorted(acc)
255+
colors = {"cts": "#2980b9", "globalroute": "#e67e22"}
256+
off = {"cts": -0.18, "globalroute": 0.18}
257+
258+
fig, ax = plt.subplots(figsize=(max(8, 1.3 * len(pdks) + 2), 5.5))
259+
for i, pdk in enumerate(pdks):
260+
rows = acc[pdk]
261+
for stage in EST_STAGES:
262+
pts = [e[stage] for _, e in rows if stage in e]
263+
k = len(pts)
264+
xs = [
265+
i + off[stage] + (0 if k < 2 else (j / (k - 1) - 0.5) * 0.26)
266+
for j in range(k)
267+
]
268+
ax.scatter(xs, pts, s=28, color=colors[stage], alpha=0.75,
269+
edgecolors="black", linewidths=0.3, zorder=3,
270+
label=stage if i == 0 else None)
271+
if pts: # mean tick
272+
m = sum(pts) / k
273+
ax.plot([i + off[stage] - 0.12, i + off[stage] + 0.12], [m, m],
274+
color=colors[stage], linewidth=2.5, zorder=4)
275+
276+
ax.axhline(0, color="black", linewidth=0.9, zorder=1)
277+
ax.set_xticks(range(len(pdks)))
278+
ax.set_xticklabels([f"{p}\n(n={len(acc[p])})" for p in pdks])
279+
ax.set_ylabel("estimate − final WNS (% of clock period)\n+ optimistic / − pessimistic")
280+
ax.set_title("WNS estimate accuracy by stage, across PDKs")
281+
ax.grid(axis="y", linestyle=":", alpha=0.5, zorder=0)
282+
ax.legend(title="estimate stage", loc="upper left", fontsize=9)
283+
fig.savefig(out_png, dpi=150, bbox_inches="tight")
284+
plt.close(fig)
285+
286+
287+
def accuracy_section(acc):
288+
lines = [
289+
ACC_BEGIN,
290+
"## WNS estimate accuracy across PDKs",
291+
"",
292+
"How closely the earlier-stage worst-slack estimates (`cts`, `globalroute`) match "
293+
"the final (`finish`) WNS, per design, normalized by that design's clock period so "
294+
"PDKs with different timing units are comparable. Error is "
295+
"`(stage − finish) / clock_period`; **positive = optimistic** (the stage reported "
296+
"more slack than the design actually closes with), negative = pessimistic. Clock "
297+
"period is parsed from each design's `.sdc`; designs whose period could not be "
298+
"parsed are omitted.",
299+
"",
300+
"![WNS estimate accuracy by stage, across PDKs](wns_accuracy.png)",
301+
"",
302+
"Mean absolute error (MAE) and mean signed error (bias), in % of clock period:",
303+
"",
304+
"| PDK | designs | cts MAE | cts bias | grt MAE | grt bias | worst (design) |",
305+
"| --- | ---: | ---: | ---: | ---: | ---: | --- |",
306+
]
307+
for pdk in sorted(acc):
308+
rows = acc[pdk]
309+
cs, gs = _stats(rows, "cts"), _stats(rows, "globalroute")
310+
# worst single |error| over both stages, for context
311+
worst = max(
312+
((abs(e[s]), e[s], d, s) for d, e in rows for s in e),
313+
default=(0, 0, "-", ""),
314+
)
315+
c = f"{cs[1]:.1f}% | {cs[2]:+.1f}%" if cs else " | "
316+
g = f"{gs[1]:.1f}% | {gs[2]:+.1f}%" if gs else " | "
317+
lines.append(
318+
f"| {pdk} | {len(rows)} | {c} | {g} | "
319+
f"{worst[1]:+.1f}% ({worst[2]} {worst[3]}) |"
320+
)
321+
lines += [
322+
"",
323+
"_Generated by `flow/util/plot_wns.py`; regenerate with "
324+
"`python3 flow/util/plot_wns.py`._",
325+
ACC_END,
326+
]
327+
return "\n".join(lines) + "\n"
328+
329+
173330
def main():
174331
ap = argparse.ArgumentParser(description=__doc__)
175332
ap.add_argument("--pdk", help="only process this PDK (default: all)")
@@ -195,13 +352,29 @@ def main():
195352
if not rows:
196353
continue
197354
plot_pdk(pdk, rows, os.path.join(pdk_dir, "wns.png"))
198-
write_readme(os.path.join(pdk_dir, "README.md"), pdk, wns_section(pdk, rows))
355+
splice_readme(os.path.join(pdk_dir, "README.md"), wns_section(pdk, rows),
356+
BEGIN, END, f"{pdk} designs")
199357
miss = sum(1 for _, v in rows if v["finish"] < 0)
200358
print(f"{pdk}: {len(rows)} designs ({miss} with negative finish WNS)")
201359
processed += 1
202360

203361
if not processed:
204362
sys.exit("no PDKs with rules-base.json WNS data found")
363+
364+
# Cross-PDK estimate-accuracy view (only meaningful over all PDKs).
365+
if not args.pdk:
366+
acc = {}
367+
for pdk in pdks:
368+
rows = collect_accuracy(os.path.join(base, pdk))
369+
if rows:
370+
acc[pdk] = rows
371+
if acc:
372+
plot_accuracy(acc, os.path.join(base, "wns_accuracy.png"))
373+
splice_readme(os.path.join(base, "README.md"), accuracy_section(acc),
374+
ACC_BEGIN, ACC_END, "ORFS designs")
375+
total = sum(len(v) for v in acc.values())
376+
print(f"accuracy: {total} designs across {len(acc)} PDKs (clock period found)")
377+
205378
print(f"done: {processed} PDK(s)")
206379

207380

0 commit comments

Comments
 (0)