diff --git a/flow/designs/README.md b/flow/designs/README.md new file mode 100644 index 0000000000..c2b471eff6 --- /dev/null +++ b/flow/designs/README.md @@ -0,0 +1,57 @@ +# ORFS designs + +## Findings: how accurate are early-stage WNS estimates? + +Reading the committed `rules-base.json` baselines, normalized by each design's clock +period (parsed from its `.sdc`), the picture across the 67 designs / 8 PDKs that expose +`cts` and `globalroute` slack is: + +- **Global route usually tightens the estimate.** For most PDKs the mean absolute error + drops from `cts` to `globalroute` — dramatically for `sky130hs` (10.5% → 1.9%) and + `gt2n` (3.3% → 0.0%), and clearly for `gf12` (2.2% → 1.1%) and `ihp-sg13g2` + (0.6% → 0.0%). `ihp-sg13g2` and `gf180` are already accurate at `cts`. + +- **`cts` is biased optimistic; `globalroute` often overshoots into pessimism.** Every + PDK's `cts` bias is ≥ 0 (cts reports more slack than the design finally closes with), + whereas `globalroute` bias flips negative for `sky130hd` (−3.5%), `sky130hs` (−1.9%), + `gf12` (−1.1%) and `nangate45` (−0.5%). Global route tends to *over-correct*. + +- **`sky130hd` is the exception where routing makes the estimate worse**, not better: + `globalroute` MAE (3.5%) exceeds `cts` MAE (2.9%), and it is consistently pessimistic. + +- **Outliers are design-specific, not PDK-wide.** `sky130hs/gcd` has `cts` +45.9% + (wildly optimistic, fully corrected by `globalroute`), and `asap7/swerv_wrapper` is + +14.9% optimistic at *both* stages — the cases most likely to mislead an early-stage + go/no-go decision. + +Practical reading: `cts` slack is a usable optimistic rank-ordering; `globalroute` is the +first estimate within a few % of final for most PDKs, but on `sky130hd` (and for specific +designs elsewhere) even `globalroute` can be off by 3–10% of the clock period. This is the +design-level companion to the per-net GRT-vs-RCX divergence in +[`flow/docs/rcx`](../docs/rcx/README.md) (PR #4302). Per-PDK design breakdowns: +[asap7](asap7/README.md), [nangate45](nangate45/README.md), [sky130hd](sky130hd/README.md), +[sky130hs](sky130hs/README.md), [gf12](gf12/README.md), [gf180](gf180/README.md), +[gt2n](gt2n/README.md), [ihp-sg13g2](ihp-sg13g2/README.md). + + +## WNS estimate accuracy across PDKs + +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. + +![WNS estimate accuracy by stage, across PDKs](wns_accuracy.png) + +Mean absolute error (MAE) and mean signed error (bias), in % of clock period: + +| PDK | designs | cts MAE | cts bias | grt MAE | grt bias | worst (design) | +| --- | ---: | ---: | ---: | ---: | ---: | --- | +| asap7 | 16 | 2.8% | +1.5% | 2.9% | +1.1% | +14.9% (swerv_wrapper globalroute) | +| gf12 | 9 | 2.2% | -2.2% | 1.1% | -1.1% | -14.2% (jpeg cts) | +| gf180 | 5 | 1.0% | +1.0% | 0.4% | +0.3% | +4.3% (aes cts) | +| gt2n | 3 | 3.3% | +3.3% | 0.0% | +0.0% | +10.0% (aes cts) | +| ihp-sg13g2 | 7 | 0.6% | +0.6% | 0.0% | +0.0% | +4.5% (spi cts) | +| nangate45 | 15 | 0.8% | +0.6% | 1.0% | -0.5% | -3.2% (black_parrot globalroute) | +| sky130hd | 7 | 2.9% | +2.0% | 3.5% | -3.5% | -10.0% (gcd globalroute) | +| sky130hs | 5 | 10.5% | +10.5% | 1.9% | -1.9% | +45.9% (gcd cts) | + +_Generated by `flow/util/plot_wns.py`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/asap7/README.md b/flow/designs/asap7/README.md new file mode 100644 index 0000000000..901603f308 --- /dev/null +++ b/flow/designs/asap7/README.md @@ -0,0 +1,68 @@ +# asap7 designs + +## Findings: worst negative slack (WNS) across asap7 + +These notes read the committed `rules-base.json` baselines — the worst setup slack each +design reaches at clock-tree synthesis (`cts`), global route (`globalroute`) and `finish`. +The plot and table below are regenerated from that data by +[`flow/util/plot_wns.py`](../../util/plot_wns.py); no flow run is needed, so the numbers +are exactly what CI checks against. + +What stands out: + +- **Every asap7 design closes with negative setup slack.** All 18 baselines have + `finish` WNS < 0, so the committed asap7 constraints target clocks the flow does not + actually meet — these baselines track *how far short* each design lands, not timing + closure. `swerv_wrapper` (≈ −318) and `mock-alu` (≈ −300) are the extreme cases, an + order of magnitude worse than the cluster around −15 … −50 (`aes*`, `mock-cpu`, + `uart`, `jpeg`). + +- **Worst slack is often not stable across stages.** Some designs pin their critical + path early and barely move (`cva6`, `jpeg`, `mock-cpu`: `cts ≈ globalroute ≈ finish`). + Others move a lot: `swerv_wrapper` degrades from −80 at cts/globalroute to −318 at + finish, and `riscv32i`/`riscv32i-mock-sram` slip from ≈ −47 (cts) to ≈ −81 (finish) — + routing and final extraction make the path materially worse than CTS predicted. + +- **Global route is sometimes more pessimistic than finish.** For `gcd` + (cts −85.9 → globalroute −110 → finish −96.7) and `aes-block` + (cts −84 → globalroute −31 → finish −57) the worst-slack estimate swings between + stages rather than monotonically worsening. This is the same GRT-vs-post-route + estimate gap explored in [`flow/docs/rcx`](../../docs/rcx/README.md) (PR #4302), here + visible at the design level rather than per net. + +The takeaway for anyone using cts-stage slack as a proxy for the final result: for asap7 +it is a usable rank ordering but not a reliable magnitude — several designs move tens of +units (and `swerv_wrapper` hundreds) between cts and finish. + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — asap7](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| swerv_wrapper | -80 | -80 | -318 | +| mock-alu | -289 | -303 | -300 | +| gcd | -85.9 | -110 | -96.7 | +| gcd-ccs | -101 | -93.7 | -96.4 | +| ethmac | -88.6 | -103 | -93.3 | +| riscv32i-mock-sram | -47.5 | -52.8 | -81.3 | +| riscv32i | -47.5 | -49.8 | -81.2 | +| jpeg_lvt | -30 | -30 | -63.9 | +| aes-block | -84.1 | -31.1 | -56.6 | +| ibex | -79.4 | -74.3 | -52.5 | +| cva6 | -50 | -50 | -50 | +| uart | -47.6 | -58.7 | -49.1 | +| jpeg | -34 | -34 | -34 | +| aes_lvt | -18 | -18 | -26.1 | +| aes | -28.9 | -28 | -24.2 | +| aes-mbff | -20.8 | -19 | -20.8 | +| mock-cpu | -16.6 | -16.6 | -16.6 | +| ethmac_lvt | -19 | -29.5 | -15.2 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/asap7/wns.png b/flow/designs/asap7/wns.png new file mode 100644 index 0000000000..2a4a021c3d Binary files /dev/null and b/flow/designs/asap7/wns.png differ diff --git a/flow/designs/gf12/README.md b/flow/designs/gf12/README.md new file mode 100644 index 0000000000..3ad109e449 --- /dev/null +++ b/flow/designs/gf12/README.md @@ -0,0 +1,29 @@ +# gf12 designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — gf12](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| bp_single | -632 | -470 | -227 | +| bp_quad | -375 | -223 | -225 | +| ariane | -257 | -214 | -212 | +| coyote | -200 | -200 | -200 | +| bp_dual | -758 | -100 | -165 | +| ca53 | -100 | -100 | -100 | +| swerv_wrapper | -75 | -75 | -75 | +| ibex | -51 | -51 | -51 | +| tinyRocket | -40 | -40 | -40 | +| jpeg | -96 | -53.2 | -25 | +| aes | -21 | -21 | -21 | +| gcd | -21.5 | -26.3 | -14 | +| ariane133 | -20.2409 | 0 | 0 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/gf12/wns.png b/flow/designs/gf12/wns.png new file mode 100644 index 0000000000..16f7673381 Binary files /dev/null and b/flow/designs/gf12/wns.png differ diff --git a/flow/designs/gf180/README.md b/flow/designs/gf180/README.md new file mode 100644 index 0000000000..2ac6540684 --- /dev/null +++ b/flow/designs/gf180/README.md @@ -0,0 +1,22 @@ +# gf180 designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — gf180](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| aes-hybrid | -0.967 | -1.1 | -1.1 | +| aes | -0.789 | -0.876 | -0.918 | +| riscv32i | -0.586 | -0.659 | -0.63 | +| ibex | -0.5 | -0.5 | -0.539 | +| jpeg | -0.375 | -0.375 | -0.375 | +| uart-blocks | -0.3 | -0.3 | -0.3 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/gf180/wns.png b/flow/designs/gf180/wns.png new file mode 100644 index 0000000000..0d07611130 Binary files /dev/null and b/flow/designs/gf180/wns.png differ diff --git a/flow/designs/gf55/README.md b/flow/designs/gf55/README.md new file mode 100644 index 0000000000..06d75913a3 --- /dev/null +++ b/flow/designs/gf55/README.md @@ -0,0 +1,17 @@ +# gf55 designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — gf55](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| aes | | | 0 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/gf55/wns.png b/flow/designs/gf55/wns.png new file mode 100644 index 0000000000..bfc9693570 Binary files /dev/null and b/flow/designs/gf55/wns.png differ diff --git a/flow/designs/gt2n/README.md b/flow/designs/gt2n/README.md new file mode 100644 index 0000000000..459624ac72 --- /dev/null +++ b/flow/designs/gt2n/README.md @@ -0,0 +1,19 @@ +# gt2n designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — gt2n](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| aes | -25 | -75.2 | -75.2 | +| jpeg | -50 | -50 | -50 | +| gcd | -25 | -25 | -25 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/gt2n/wns.png b/flow/designs/gt2n/wns.png new file mode 100644 index 0000000000..c299f3d54b Binary files /dev/null and b/flow/designs/gt2n/wns.png differ diff --git a/flow/designs/ihp-sg13g2/README.md b/flow/designs/ihp-sg13g2/README.md new file mode 100644 index 0000000000..3b20f3eb50 --- /dev/null +++ b/flow/designs/ihp-sg13g2/README.md @@ -0,0 +1,23 @@ +# ihp-sg13g2 designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — ihp-sg13g2](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| i2c-gpio-expander | -1 | -1 | -1 | +| ibex | -0.4 | -0.4 | -0.4 | +| jpeg | -0.4 | -0.4 | -0.4 | +| riscv32i | -0.3 | -0.3 | -0.3 | +| aes | -0.225 | -0.225 | -0.225 | +| gcd | -0.13 | -0.13 | -0.13 | +| spi | 0 | -0.0426 | -0.045 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/ihp-sg13g2/wns.png b/flow/designs/ihp-sg13g2/wns.png new file mode 100644 index 0000000000..3553d9a981 Binary files /dev/null and b/flow/designs/ihp-sg13g2/wns.png differ diff --git a/flow/designs/nangate45/README.md b/flow/designs/nangate45/README.md new file mode 100644 index 0000000000..d1e3aa2f57 --- /dev/null +++ b/flow/designs/nangate45/README.md @@ -0,0 +1,32 @@ +# nangate45 designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — nangate45](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| bp_quad | -150 | -150 | -150 | +| black_parrot | -3.31 | -3.45 | -3.26 | +| mempool_group | -2.31 | -2.31 | -2.31 | +| swerv | -0.671 | -0.719 | -0.677 | +| ariane133 | -0.579 | -0.569 | -0.595 | +| swerv_wrapper | -0.344 | -0.357 | -0.35 | +| dynamic_node | -0.362 | -0.38 | -0.344 | +| ariane136 | -0.3 | -0.3 | -0.318 | +| bp_be_top | -0.331 | -0.315 | -0.318 | +| bp_multi_top | -0.24 | -0.24 | -0.24 | +| jpeg | -0.147 | -0.165 | -0.164 | +| tinyRocket | -0.14 | -0.168 | -0.154 | +| bp_fe_top | -0.09 | -0.09 | -0.131 | +| ibex | -0.11 | -0.127 | -0.11 | +| aes | -0.041 | -0.0692 | -0.0667 | +| gcd | -0.0529 | -0.0657 | -0.0559 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/nangate45/wns.png b/flow/designs/nangate45/wns.png new file mode 100644 index 0000000000..8820d577f9 Binary files /dev/null and b/flow/designs/nangate45/wns.png differ diff --git a/flow/designs/sky130hd/README.md b/flow/designs/sky130hd/README.md new file mode 100644 index 0000000000..5825fee559 --- /dev/null +++ b/flow/designs/sky130hd/README.md @@ -0,0 +1,23 @@ +# sky130hd designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — sky130hd](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| microwatt | -2.71 | -2.8 | -2.73 | +| gcd | -1.36 | -1.58 | -1.47 | +| riscv32i | -1.41 | -1.55 | -1.37 | +| jpeg | -0.716 | -0.877 | -0.654 | +| ibex | -0.505 | -0.576 | -0.5 | +| aes | -0.18 | -0.554 | -0.425 | +| chameleon | -0.321 | -0.356 | -0.282 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/sky130hd/wns.png b/flow/designs/sky130hd/wns.png new file mode 100644 index 0000000000..726d41557a Binary files /dev/null and b/flow/designs/sky130hd/wns.png differ diff --git a/flow/designs/sky130hs/README.md b/flow/designs/sky130hs/README.md new file mode 100644 index 0000000000..4df31eea08 --- /dev/null +++ b/flow/designs/sky130hs/README.md @@ -0,0 +1,21 @@ +# sky130hs designs + + +## WNS + +Worst setup slack per design at three flow stages — clock-tree synthesis (`cts`), global route (`globalroute`) and `finish` — read from each design's `rules-base.json`. Negative means setup timing is not met. Values are in this PDK's native timing unit (ps for `asap7`, ns for most others), so they are comparable within this PDK but not across PDKs. + +The bar is the `finish` slack; the markers show the `cts` and `globalroute` slack for the same design, so stage-to-stage movement is visible. + +![WNS by design — sky130hs](wns.png) + +| design | cts | globalroute | finish | +| --- | ---: | ---: | ---: | +| gcd | -0.358 | -1 | -1 | +| riscv32i | -0.303 | -0.695 | -0.571 | +| ibex | -0.35 | -0.525 | -0.35 | +| jpeg | -0.2 | -0.268 | -0.2 | +| aes | -0.14 | -0.206 | -0.145 | + +_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with `python3 flow/util/plot_wns.py`._ + diff --git a/flow/designs/sky130hs/wns.png b/flow/designs/sky130hs/wns.png new file mode 100644 index 0000000000..ad4963f3c3 Binary files /dev/null and b/flow/designs/sky130hs/wns.png differ diff --git a/flow/designs/wns_accuracy.png b/flow/designs/wns_accuracy.png new file mode 100644 index 0000000000..4c78bc48b6 Binary files /dev/null and b/flow/designs/wns_accuracy.png differ diff --git a/flow/util/plot_wns.py b/flow/util/plot_wns.py new file mode 100644 index 0000000000..9d613df325 --- /dev/null +++ b/flow/util/plot_wns.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +"""Plot worst setup slack (WNS) per design, per PDK, from committed rules-base.json. + +ORFS stores a golden-metrics baseline (rules-base.json) next to every design under +flow/designs///. Each baseline records the worst setup slack reached at +three flow stages: + + cts__timing__setup__ws + globalroute__timing__setup__ws + finish__timing__setup__ws + +This script reads those values for every PDK that has them and regenerates, per PDK: + + flow/designs//wns.png -- horizontal bar chart of finish-stage WNS + flow/designs//README.md -- a "## WNS" section (between generated markers) + +It also produces a cross-PDK view of how well the cts/globalroute estimates predict the +final WNS (each design's per-stage estimate error, normalized by its clock period so the +PDKs are comparable): + + flow/designs/wns_accuracy.png -- per-PDK strip plot of normalized estimate error + flow/designs/README.md -- a "## WNS estimate accuracy across PDKs" section + +No OpenROAD/ORFS flow run is required -- the data is already in the tree, so the plots +are deterministic and reproducible. Run from anywhere in the repo: + + python3 flow/util/plot_wns.py # all PDKs + python3 flow/util/plot_wns.py --pdk asap7 # one PDK + +Only matplotlib (pinned in flow/util/requirements_lock.txt) and the standard library are +used. +""" + +import argparse +import glob +import json +import os +import re +import sys + +import matplotlib + +matplotlib.use("Agg") # headless: write PNGs, never open a window +import matplotlib.pyplot as plt # noqa: E402 + +# Stage key -> (short label, plot marker). Order is flow order. +STAGES = [ + ("cts__timing__setup__ws", "cts", "v"), + ("globalroute__timing__setup__ws", "globalroute", "^"), + ("finish__timing__setup__ws", "finish", None), # finish is drawn as the bar +] +FINISH_KEY = "finish__timing__setup__ws" + +BEGIN = "" +END = "" + +ACC_BEGIN = "" +ACC_END = "" + +# Estimate stages whose accuracy (vs finish) we report, in flow order. +EST_STAGES = ["cts", "globalroute"] +EST_MARKERS = {"cts": "v", "globalroute": "^"} + +_PERIOD_RE = ( + re.compile(r"set\s+clk_period\s+([0-9.]+)"), + re.compile(r"create_clock[^\n]*-period\s+([0-9.]+)"), +) + + +def clock_period(design_dir): + """Clock period for a design, parsed from its .sdc, or None if not found. + + Handles the two idioms used across PDKs: `set clk_period ` and + `create_clock ... -period `. Returns the first match (designs here are + single-clock); units are the PDK's native timing unit, same as the WNS values. + """ + for sdc in sorted(glob.glob(os.path.join(design_dir, "*.sdc"))): + try: + text = open(sdc, errors="ignore").read() + except OSError: + continue + for rx in _PERIOD_RE: + m = rx.search(text) + if m: + return float(m.group(1)) + return None + + +def designs_dir(): + """flow/designs, located relative to this script (flow/util/plot_wns.py).""" + here = os.path.dirname(os.path.abspath(__file__)) + return os.path.normpath(os.path.join(here, "..", "designs")) + + +def load_value(path, key): + """Return the float value for `key` in a rules-base.json, or None if absent.""" + try: + with open(path) as f: + data = json.load(f) + except (OSError, ValueError): + return None + entry = data.get(key) + if isinstance(entry, dict) and isinstance(entry.get("value"), (int, float)): + return float(entry["value"]) + return None + + +def collect(pdk_dir): + """Return [(design, {stage_label: value_or_None}), ...] for designs with WNS data.""" + rows = [] + for design in sorted(os.listdir(pdk_dir)): + rules = os.path.join(pdk_dir, design, "rules-base.json") + if not os.path.isfile(rules): + continue + vals = {label: load_value(rules, key) for key, label, _ in STAGES} + if vals["finish"] is None: # need at least the headline metric + continue + rows.append((design, vals)) + # worst (most negative) finish slack first + rows.sort(key=lambda r: r[1]["finish"]) + return rows + + +def plot_pdk(pdk, rows, out_png): + names = [r[0] for r in rows] + finish = [r[1]["finish"] for r in rows] + y = list(range(len(names))) + + fig_h = max(2.5, 0.42 * len(names) + 1.2) + fig, ax = plt.subplots(figsize=(9, fig_h)) + + colors = ["#c0392b" if v < 0 else "#27ae60" for v in finish] + ax.barh(y, finish, color=colors, height=0.6, zorder=2, + label="finish (bar)") + + # overlay cts / globalroute as markers to show stage-to-stage movement + for key, label, marker in STAGES: + if marker is None: + continue + xs = [r[1][label] for r in rows] + yy = [yi for yi, x in zip(y, xs) if x is not None] + xx = [x for x in xs if x is not None] + if xx: + ax.scatter(xx, yy, marker=marker, s=40, zorder=3, + edgecolors="black", linewidths=0.4, label=label) + + ax.axvline(0, color="black", linewidth=0.8, zorder=1) + ax.set_yticks(y) + ax.set_yticklabels(names) + ax.invert_yaxis() # worst slack on top + ax.set_xlabel("worst setup slack (PDK native timing unit; negative = not met)") + ax.set_title(f"WNS by design — {pdk}") + ax.grid(axis="x", linestyle=":", alpha=0.5, zorder=0) + ax.legend(loc="lower right", fontsize=8, framealpha=0.9) + fig.savefig(out_png, dpi=150, bbox_inches="tight") + plt.close(fig) + + +def fmt(v): + return "" if v is None else f"{v:g}" + + +def wns_section(pdk, rows): + lines = [ + BEGIN, + "## WNS", + "", + "Worst setup slack per design at three flow stages — clock-tree synthesis " + "(`cts`), global route (`globalroute`) and `finish` — read from each " + "design's `rules-base.json`. Negative means setup timing is not met. Values are " + "in this PDK's native timing unit (ps for `asap7`, ns for most others), so they " + "are comparable within this PDK but not across PDKs.", + "", + "The bar is the `finish` slack; the markers show the `cts` and `globalroute` " + "slack for the same design, so stage-to-stage movement is visible.", + "", + f"![WNS by design — {pdk}](wns.png)", + "", + "| design | cts | globalroute | finish |", + "| --- | ---: | ---: | ---: |", + ] + for design, vals in rows: + lines.append( + f"| {design} | {fmt(vals['cts'])} | " + f"{fmt(vals['globalroute'])} | {fmt(vals['finish'])} |" + ) + lines += [ + "", + "_Generated by `flow/util/plot_wns.py` from `rules-base.json`; regenerate with " + "`python3 flow/util/plot_wns.py`._", + END, + ] + return "\n".join(lines) + "\n" + + +def splice_readme(path, section, begin, end, title): + """Create README, or replace the marked section in place (preserving prose).""" + if os.path.isfile(path): + with open(path) as f: + text = f.read() + if begin in text and end in text: + pre = text[: text.index(begin)] + post = text[text.index(end) + len(end):] + new = pre + section + post.lstrip("\n") + else: + new = text.rstrip("\n") + "\n\n" + section + else: + new = (f"# {title}\n\n" if title else "") + section + with open(path, "w") as f: + f.write(new) + + +# --- cross-PDK estimate accuracy -------------------------------------------- + +def collect_accuracy(pdk_dir): + """Per-design normalized estimate error vs finish, in % of the clock period. + + Returns [(design, {stage: err_pct}), ...] for designs that have a clock period and + finish + estimate-stage WNS. err_pct = 100 * (stage_ws - finish_ws) / period; + positive means the stage was *optimistic* (reported more slack than the final result). + Normalizing by clock period makes the error comparable across PDKs with different units. + """ + out = [] + for design in sorted(os.listdir(pdk_dir)): + ddir = os.path.join(pdk_dir, design) + rules = os.path.join(ddir, "rules-base.json") + if not os.path.isfile(rules): + continue + period = clock_period(ddir) + fin = load_value(rules, FINISH_KEY) + if not period or fin is None: + continue + errs = {} + for stage in EST_STAGES: + v = load_value(rules, f"{stage}__timing__setup__ws") + if v is not None: + errs[stage] = 100.0 * (v - fin) / period + if errs: + out.append((design, errs)) + return out + + +def _stats(rows, stage): + vals = [e[stage] for _, e in rows if stage in e] + if not vals: + return None + mae = sum(abs(v) for v in vals) / len(vals) + bias = sum(vals) / len(vals) + return len(vals), mae, bias, max(vals, key=abs) + + +def plot_accuracy(acc, out_png): + """Strip plot: per-PDK distribution of cts/globalroute estimate error vs finish.""" + pdks = sorted(acc) + colors = {"cts": "#2980b9", "globalroute": "#e67e22"} + off = {"cts": -0.18, "globalroute": 0.18} + + fig, ax = plt.subplots(figsize=(max(8, 1.3 * len(pdks) + 2), 5.5)) + for i, pdk in enumerate(pdks): + rows = acc[pdk] + for stage in EST_STAGES: + pts = [e[stage] for _, e in rows if stage in e] + k = len(pts) + xs = [ + i + off[stage] + (0 if k < 2 else (j / (k - 1) - 0.5) * 0.26) + for j in range(k) + ] + ax.scatter(xs, pts, s=28, color=colors[stage], alpha=0.75, + edgecolors="black", linewidths=0.3, zorder=3, + label=stage if i == 0 else None) + if pts: # mean tick + m = sum(pts) / k + ax.plot([i + off[stage] - 0.12, i + off[stage] + 0.12], [m, m], + color=colors[stage], linewidth=2.5, zorder=4) + + ax.axhline(0, color="black", linewidth=0.9, zorder=1) + ax.set_xticks(range(len(pdks))) + ax.set_xticklabels([f"{p}\n(n={len(acc[p])})" for p in pdks]) + ax.set_ylabel("estimate − final WNS (% of clock period)\n+ optimistic / − pessimistic") + ax.set_title("WNS estimate accuracy by stage, across PDKs") + ax.grid(axis="y", linestyle=":", alpha=0.5, zorder=0) + ax.legend(title="estimate stage", loc="upper left", fontsize=9) + fig.savefig(out_png, dpi=150, bbox_inches="tight") + plt.close(fig) + + +def accuracy_section(acc): + lines = [ + ACC_BEGIN, + "## WNS estimate accuracy across PDKs", + "", + "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.", + "", + "![WNS estimate accuracy by stage, across PDKs](wns_accuracy.png)", + "", + "Mean absolute error (MAE) and mean signed error (bias), in % of clock period:", + "", + "| PDK | designs | cts MAE | cts bias | grt MAE | grt bias | worst (design) |", + "| --- | ---: | ---: | ---: | ---: | ---: | --- |", + ] + for pdk in sorted(acc): + rows = acc[pdk] + cs, gs = _stats(rows, "cts"), _stats(rows, "globalroute") + # worst single |error| over both stages, for context + worst = max( + ((abs(e[s]), e[s], d, s) for d, e in rows for s in e), + default=(0, 0, "-", ""), + ) + c = f"{cs[1]:.1f}% | {cs[2]:+.1f}%" if cs else " | " + g = f"{gs[1]:.1f}% | {gs[2]:+.1f}%" if gs else " | " + lines.append( + f"| {pdk} | {len(rows)} | {c} | {g} | " + f"{worst[1]:+.1f}% ({worst[2]} {worst[3]}) |" + ) + lines += [ + "", + "_Generated by `flow/util/plot_wns.py`; regenerate with " + "`python3 flow/util/plot_wns.py`._", + ACC_END, + ] + return "\n".join(lines) + "\n" + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--pdk", help="only process this PDK (default: all)") + args = ap.parse_args() + + base = designs_dir() + if not os.path.isdir(base): + sys.exit(f"flow/designs not found at {base}") + + pdks = sorted( + d for d in os.listdir(base) + if os.path.isdir(os.path.join(base, d)) + ) + if args.pdk: + pdks = [p for p in pdks if p == args.pdk] + if not pdks: + sys.exit(f"no such PDK directory: {args.pdk}") + + processed = 0 + for pdk in pdks: + pdk_dir = os.path.join(base, pdk) + rows = collect(pdk_dir) + if not rows: + continue + plot_pdk(pdk, rows, os.path.join(pdk_dir, "wns.png")) + splice_readme(os.path.join(pdk_dir, "README.md"), wns_section(pdk, rows), + BEGIN, END, f"{pdk} designs") + miss = sum(1 for _, v in rows if v["finish"] < 0) + print(f"{pdk}: {len(rows)} designs ({miss} with negative finish WNS)") + processed += 1 + + if not processed: + sys.exit("no PDKs with rules-base.json WNS data found") + + # Cross-PDK estimate-accuracy view (only meaningful over all PDKs). + if not args.pdk: + acc = {} + for pdk in pdks: + rows = collect_accuracy(os.path.join(base, pdk)) + if rows: + acc[pdk] = rows + if acc: + plot_accuracy(acc, os.path.join(base, "wns_accuracy.png")) + splice_readme(os.path.join(base, "README.md"), accuracy_section(acc), + ACC_BEGIN, ACC_END, "ORFS designs") + total = sum(len(v) for v in acc.values()) + print(f"accuracy: {total} designs across {len(acc)} PDKs (clock period found)") + + print(f"done: {processed} PDK(s)") + + +if __name__ == "__main__": + main()