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+
1724No OpenROAD/ORFS flow run is required -- the data is already in the tree, so the plots
1825are deterministic and reproducible. Run from anywhere in the repo:
1926
2532"""
2633
2734import argparse
35+ import glob
2836import json
2937import os
38+ import re
3039import sys
3140
3241import matplotlib
4554BEGIN = "<!-- BEGIN WNS (generated by flow/util/plot_wns.py) -->"
4655END = "<!-- 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
4989def 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+ "" ,
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+
173330def 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