Python Part II · Module 28 · Report Parsing
WNS, TNS and Violation Summaries
Three numbers come out of a timing report and get quoted in every status meeting: the worst slack, the total negative slack, and how many paths are failing. Each is a genuine summary of something and each discards almost everything else, so two designs can agree on one of them while being in completely different states. The most consequential thing about them, though, is not what they hide about the design — it is that one of the three depends on a report switch, so comparing it between two runs can be meaningless.
Advanced19 min readPythonPart IIAutomationSTAReport Parsing
Part II · Module 28 · Chapter 28.2 · WNS, TNS and Violation Summaries
1. Three Numbers
DEFINITIONS = [
("WNS", "worst negative slack", "the single most negative slack", "how bad the worst path is"),
("TNS", "total negative slack", "the sum of all negative slacks", "how much failure there is in total"),
("count", "violating paths", "how many paths have negative slack", "how many things are failing"),
]
print(f"{'name':<7} {'stands for':<22} {'is':<34} answers")
for name, expands, what, answers in DEFINITIONS:
print(f"{name:<7} {expands:<22} {what:<34} {answers}")
print()
print("each is a summary of:", "the same list of per-path slacks")
print("each discards :", "almost everything the others keep")name stands for is answers
WNS worst negative slack the single most negative slack how bad the worst path is
TNS total negative slack the sum of all negative slacks how much failure there is in total
count violating paths how many paths have negative slack how many things are failing
each is a summary of: the same list of per-path slacks
each discards : almost everything the others keepThree reductions of one list, and the rest of this chapter is about what each reduction loses. The order matters: WNS is what gates a tape-out, TNS is what estimates the work, and the count is what estimates the number of places to look.
2. The Annotation
wns = min(p["slack"] for p in paths)
tns = sum(s for s in slacks if s < 0)
violating = [p for p in paths if p["slack"] < 0]import math
PATHS = [{"slack": -0.045}, {"slack": 0.250}, {"slack": -0.120},
{"slack": -0.000}, {"slack": 0.000}]
slacks = [p["slack"] for p in PATHS]
def negative(s):
"""Chapter 28.1 §6: agree with the person reading the report."""
return math.copysign(1.0, s) < 0
wns = min(slacks)
tns_annotation = round(sum(s for s in slacks if s < 0), 6)
tns_signbit = round(sum(s for s in slacks if negative(s)), 6)
count_annotation = len([p for p in PATHS if p["slack"] < 0])
count_signbit = len([p for p in PATHS if negative(p["slack"])])
print("wns :", f"{wns:+.3f}")
print("tns (annotation) :", f"{tns_annotation:+.3f}")
print("tns (sign bit) :", f"{tns_signbit:+.3f}")
print("count (annotation):", count_annotation)
print("count (sign bit) :", count_signbit)
print()
print("TNS is unaffected by -0.000:", tns_annotation == tns_signbit)
print("the count is not :", count_annotation != count_signbit)wns : -0.120
tns (annotation) : -0.165
tns (sign bit) : -0.165
count (annotation): 2
count (sign bit) : 3
TNS is unaffected by -0.000: True
the count is not : TrueChapter 28.1 Section 6's -0.000 carries straight into these three, and it affects exactly one of them.
TNS is unchanged because -0.0 contributes no magnitude. WNS is unchanged because -0.120 is more negative. The count changes, from two to three, because a path whose slack rounds to minus zero is a path the report shows with a minus sign.
So the annotation is right about two of the three numbers and wrong about the one a reviewer cross-checks by eye.
3. Same WNS, Different Designs
A = [-0.050] + [0.100] * 99
B = [-0.050] * 40 + [0.100] * 60
def summarise(slacks):
neg = [s for s in slacks if s < 0]
return {"wns": round(min(slacks), 3),
"tns": round(sum(neg), 3),
"violators": len(neg),
"paths": len(slacks)}
for name, slacks in (("design A", A), ("design B", B)):
s = summarise(slacks)
print(f"{name}: wns {s['wns']:+.3f} tns {s['tns']:+.3f} "
f"violators {s['violators']:>3} paths {s['paths']}")
print()
print("same WNS :", summarise(A)["wns"] == summarise(B)["wns"])
print("TNS differs by :", round(summarise(B)["tns"] - summarise(A)["tns"], 3))
print("violators differ:", summarise(B)["violators"] - summarise(A)["violators"])
print()
print("A needs :", "one path fixed")
print("B needs :", "forty paths fixed, or one systemic change")design A: wns -0.050 tns -0.050 violators 1 paths 100
design B: wns -0.050 tns -2.000 violators 40 paths 100
same WNS : True
TNS differs by : -1.95
violators differ: 39
A needs : one path fixed
B needs : forty paths fixed, or one systemic changeIdentical WNS, and the two designs are nowhere near each other. A is one awkward path; B is a systemic problem — a clock that is too fast, a library corner, a badly placed block.
The practical consequence is about effort estimation. A WNS of -0.050 sounds like one day's work and in design B it is not, and nothing in the WNS distinguishes them. That is why a timing report summary that quotes WNS alone is incomplete rather than merely terse.
4. Same TNS, Different Designs
C = [-1.000] + [0.500] * 99
D = [-0.010] * 100
def summarise(slacks):
neg = [s for s in slacks if s < 0]
return {"wns": round(min(slacks), 3), "tns": round(sum(neg), 3),
"violators": len(neg)}
for name, slacks in (("design C", C), ("design D", D)):
s = summarise(slacks)
print(f"{name}: wns {s['wns']:+.3f} tns {s['tns']:+.3f} violators {s['violators']:>3}")
print()
print("same TNS :", summarise(C)["tns"] == summarise(D)["tns"])
print("WNS differs by:", round(summarise(D)["wns"] - summarise(C)["wns"], 3))
print()
print("C is :", "one path a nanosecond out — probably unfixable by tuning")
print("D is :", "a hundred paths ten picoseconds out — probably one placement pass")design C: wns -1.000 tns -1.000 violators 1
design D: wns -0.010 tns -1.000 violators 100
same TNS : True
WNS differs by: 0.99
C is : one path a nanosecond out — probably unfixable by tuning
D is : a hundred paths ten picoseconds out — probably one placement passIdentical TNS, and the WNS differs by 0.99 nanoseconds.
Design C has one path missing by a full nanosecond, which usually means something structural — an unregistered path across a block boundary, a missing pipeline stage. Design D has a hundred paths each missing by ten picoseconds, which a placement or sizing pass often fixes wholesale.
Sections 3 and 4 together give the rule: quote all three, always. Any two of them can agree while the designs differ completely, and the triple is short enough that there is no reason to pick.
5. Truncation Moves TNS and Not WNS
VIOLATORS = [-0.050, -0.045, -0.040, -0.035, -0.030,
-0.025, -0.020, -0.015, -0.010, -0.005]
print(f"{'-max_paths':>11} {'paths':>6} {'wns':>8} {'tns':>8}")
for k in (1, 3, 5, 10):
part = VIOLATORS[:k]
print(f"{k:>11} {len(part):>6} {min(part):>+8.3f} {round(sum(part), 3):>+8.3f}")
wns_values = {min(VIOLATORS[:k]) for k in (1, 3, 5, 10)}
tns_values = {round(sum(VIOLATORS[:k]), 3) for k in (1, 3, 5, 10)}
print()
print("distinct WNS values:", len(wns_values))
print("distinct TNS values:", len(tns_values))
print()
print("WNS survives truncation because:", "the worst path is reported first")
print("TNS does not because :", "it needs every violator") -max_paths paths wns tns
1 1 -0.050 -0.050
3 3 -0.050 -0.135
5 5 -0.050 -0.200
10 10 -0.050 -0.275
distinct WNS values: 1
distinct TNS values: 4
WNS survives truncation because: the worst path is reported first
TNS does not because : it needs every violatorOne WNS and four TNS values from the same design.
This is the chapter's most consequential finding, and it is about the report rather than the design. Chapter 28.1 Section 3 listed -max_paths as a switch that changes the content; here is what it changes it into. A report truncated to the worst path gives a perfectly correct WNS and a TNS that is 94% too small.
Two rules follow, and the second is routinely broken:
RUNS = [("nightly", 100, -0.275), ("tapeout_check", 10000, -3.900)]
print(f"{'run':<16} {'max_paths':>10} {'reported tns':>13}")
for name, max_paths, tns in RUNS:
print(f"{name:<16} {max_paths:>10} {tns:>+13.3f}")
print()
print("TNS got worse by:", round(RUNS[1][2] - RUNS[0][2], 3))
print("design changed :", "unknown — the reports are not comparable")
print("what differs :", "the -max_paths switch, by 100x")
print()
print("comparing TNS across runs requires:", "identical -max_paths")
print("and TNS from a truncated report is:", "a lower bound on the magnitude")run max_paths reported tns
nightly 100 -0.275
tapeout_check 10000 -3.900
TNS got worse by: -3.625
design changed : unknown — the reports are not comparable
what differs : the -max_paths switch, by 100x
comparing TNS across runs requires: identical -max_paths
and TNS from a truncated report is: a lower bound on the magnitudeA TNS that "got worse by 3.6" when only the switch changed. Chapter 28.5 compares QoR between two runs and Chapter 28.6 turns that into a verdict — and both are worthless if the two reports were produced with different switches, which is why Chapter 28.1 Section 3 recorded them alongside the numbers.
A TNS from a truncated report is a lower bound on the magnitude: the true TNS is at least this negative and may be far more so. Reporting it without that qualification is the single easiest way to understate a timing problem.
6. Paths Are Not Endpoints
import math
PATHS = [
{"start": "regfile/a", "end": "fifo/wptr_reg[0]", "slack": -0.050},
{"start": "regfile/b", "end": "fifo/wptr_reg[0]", "slack": -0.045},
{"start": "ctrl/c", "end": "fifo/count_reg[4]", "slack": -0.010},
{"start": "ctrl/d", "end": "ctrl/busy_reg", "slack": 0.100},
]
def negative(s):
return math.copysign(1.0, s) < 0
violating = [p for p in PATHS if negative(p["slack"])]
endpoints = sorted({p["end"] for p in violating})
print("violating paths :", len(violating))
print("violating endpoints:", len(endpoints))
print("the endpoints :", endpoints)
print()
worst_per_endpoint = {}
for p in violating:
e = p["end"]
if e not in worst_per_endpoint or p["slack"] < worst_per_endpoint[e]:
worst_per_endpoint[e] = p["slack"]
for e, s in sorted(worst_per_endpoint.items()):
n = sum(1 for p in violating if p["end"] == e)
print(f" {e:<20} worst {s:+.3f} across {n} path(s)")
print()
print("fixing the worst endpoint removes:", 2, "paths")
print("the violator count overstates :", "the number of places to look")violating paths : 3
violating endpoints: 2
the endpoints : ['fifo/count_reg[4]', 'fifo/wptr_reg[0]']
fifo/count_reg[4] worst -0.010 across 1 path(s)
fifo/wptr_reg[0] worst -0.050 across 2 path(s)
fixing the worst endpoint removes: 2 paths
the violator count overstates : the number of places to lookThree violating paths and two places to look.
Many paths converge on one endpoint — several sources feeding one register — and fixing that register's timing fixes all of them at once. A violator count of three suggests three units of work; grouping by endpoint says two, and one of them accounts for most of the damage.
This is Chapter 26.7's bucketing applied to timing: the raw count is the symptom count and the grouped count is the problem count. Both belong in a report, and the grouped one is the actionable number.
7. An Overall WNS Names No Group
GROUPS = {"clk": [-0.050, -0.020, 0.300],
"clk_div2": [0.300, 0.250],
"async_in": [-0.005]}
all_slacks = [s for v in GROUPS.values() for s in v]
print("overall wns:", f"{min(all_slacks):+.3f}")
print("overall tns:", f"{round(sum(s for s in all_slacks if s < 0), 3):+.3f}")
print()
print(f"{'group':<10} {'wns':>8} {'tns':>8} {'paths':>6}")
for name, slacks in GROUPS.items():
neg = [s for s in slacks if s < 0]
print(f"{name:<10} {min(slacks):>+8.3f} {round(sum(neg), 3):>+8.3f} {len(slacks):>6}")
print()
worst_group = min(GROUPS, key=lambda g: min(GROUPS[g]))
clean = [g for g in GROUPS if min(GROUPS[g]) >= 0]
print("worst group :", worst_group)
print("groups fully met :", clean)
print("the overall number names:", "no group at all")overall wns: -0.050
overall tns: -0.075
group wns tns paths
clk -0.050 -0.070 3
clk_div2 +0.250 +0.000 2
async_in -0.005 -0.005 1
worst group : clk
groups fully met : ['clk_div2']
the overall number names: no group at allAn overall WNS of -0.050 and the report does not say which clock.
Per-group figures turn one number into a map: clk is the problem, clk_div2 has a quarter of a nanosecond to spare, and async_in has a five-picosecond miss that is probably a constraint rather than a design issue.
Note clk_div2's TNS of +0.000 — the sum of an empty list of negatives is zero, formatted with a sign. That is correct and slightly odd to read; a group with no violators has a TNS of zero, which is genuinely different from Chapter 28.1 Section 9's unknown.
Chapter 28.5 compares runs on these figures and the per-group split is what makes the comparison diagnostic: an overall TNS that worsened tells you something changed, and a per-group TNS that worsened tells you where.
8. The Paths the Report Does Not Contain
ENDPOINTS_TOTAL = 48213
ANALYSED = 46100
EXCLUDED = [
("unconstrained", 1842, "no clock reaches them — never analysed"),
("false path", 198, "excluded by a set_false_path constraint"),
("multicycle", 73, "analysed against a relaxed requirement"),
]
print(f"{'category':<16} {'endpoints':>10} why")
for name, n, why in EXCLUDED:
print(f"{name:<16} {n:>10} {why}")
print()
print("endpoints in the design :", ENDPOINTS_TOTAL)
print("endpoints analysed :", ANALYSED)
print("excluded :", sum(n for _, n, _ in EXCLUDED))
print("accounted for :", ANALYSED + sum(n for _, n, _ in EXCLUDED) == ENDPOINTS_TOTAL)
print()
print("WNS is computed over :", "the analysed set only")
print("so a perfect WNS means :", "every analysed path met — and says nothing about 2113 others")category endpoints why
unconstrained 1842 no clock reaches them — never analysed
false path 198 excluded by a set_false_path constraint
multicycle 73 analysed against a relaxed requirement
endpoints in the design : 48213
endpoints analysed : 46100
excluded : 2113
accounted for : True
WNS is computed over : the analysed set only
so a perfect WNS means : every analysed path met — and says nothing about 2113 othersA WNS of +0.250 over 46,100 endpoints and 2,113 endpoints nobody looked at.
Chapter 28.2's three numbers are all computed over the paths the report contains, and a timing report contains the paths the constraints told it to analyse. Three categories sit outside that, and they are not equivalent:
Unconstrained endpoints are the dangerous ones. No clock reaches them, so the tool has nothing to check against and reports nothing at all — not a violation, not a met path, nothing. A missing create_clock on a newly added block produces a clean timing report for a block that was never analysed.
False paths are a deliberate exclusion and usually correct — a path between two asynchronous domains genuinely has no timing relationship. They are also the easiest place to hide a problem, because adding one makes a violation disappear.
Multicycle paths are analysed against a relaxed requirement, so they appear in the report with slack computed against a different number of clock periods.
The defence is to report the counts alongside the numbers:
def timing_summary(wns, tns, analysed, unconstrained, false_paths, multicycle):
total = analysed + unconstrained + false_paths + multicycle
coverage = analysed / total
return {"wns": wns, "tns": tns,
"endpoints_analysed": analysed,
"endpoints_total": total,
"analysis_coverage": round(coverage, 4),
"unconstrained": unconstrained,
"exclusions": false_paths + multicycle}
CLEAN = timing_summary(0.250, 0.0, 46100, 1842, 198, 73)
FULL = timing_summary(0.250, 0.0, 48213, 0, 0, 0)
for label, s in (("as reported", CLEAN), ("fully constrained", FULL)):
print(f"{label:<19} wns {s['wns']:+.3f} coverage {s['analysis_coverage']:.2%} "
f"unconstrained {s['unconstrained']}")
print()
print("the WNS is identical :", CLEAN["wns"] == FULL["wns"])
print("the analyses are not :", CLEAN["analysis_coverage"] != FULL["analysis_coverage"])
print()
print("a timing signoff needs:", "wns, tns, AND unconstrained == 0")
print("without the third :", "a clean report proves nothing about 4% of the design")as reported wns +0.250 coverage 95.62% unconstrained 1842
fully constrained wns +0.250 coverage 100.00% unconstrained 0
the WNS is identical : True
the analyses are not : True
a timing signoff needs: wns, tns, AND unconstrained == 0
without the third : a clean report proves nothing about 4% of the designIdentical WNS, and one of the two analyses covered 95.62% of the design.
This is the same shape as Chapter 26.1's accounting invariant: a number over a subset is only interpretable alongside how large the subset was, and the subset has to be reconciled against a total the report cannot shrink.
The practical rule is that unconstrained count belongs in the headline, not in a detail section. A WNS without it is Chapter 26.1 Section 4's optimistic error — a good number that nobody investigates, computed over whatever happened to be analysed.
9. The Summary, Assembled
import math
from collections import defaultdict
def negative(s):
return math.copysign(1.0, s) < 0
def summarise(paths, *, max_paths=None, unit="ns"):
"""Everything the three headline numbers need, plus what qualifies them."""
if not paths:
return {"status": "NO_PATHS", "wns": None, "tns": None, "violators": 0}
slacks = [p["slack"] for p in paths]
violating = [p for p in paths if negative(p["slack"])]
by_group = defaultdict(list)
for p in paths:
by_group[p.get("group", "default")].append(p["slack"])
endpoints = defaultdict(list)
for p in violating:
endpoints[p.get("end", "?")].append(p["slack"])
truncated = max_paths is not None and len(paths) >= max_paths
return {
"status": "OK",
"unit": unit,
"wns": round(min(slacks), 6),
"tns": round(sum(s for s in slacks if negative(s)), 6),
"tns_is_lower_bound": truncated,
"violating_paths": len(violating),
"violating_endpoints": len(endpoints),
"worst_endpoint": min(endpoints, key=lambda e: min(endpoints[e])) if endpoints else None,
"per_group": {g: {"wns": round(min(v), 6),
"tns": round(sum(s for s in v if negative(s)), 6)}
for g, v in sorted(by_group.items())},
}
PATHS = [
{"end": "fifo/wptr_reg[0]", "group": "clk", "slack": -0.050},
{"end": "fifo/wptr_reg[0]", "group": "clk", "slack": -0.045},
{"end": "fifo/count_reg[4]", "group": "clk", "slack": -0.000},
{"end": "ctrl/busy_reg", "group": "clk", "slack": 0.300},
{"end": "cdc/sync_reg", "group": "async_in", "slack": 0.120},
]
s = summarise(PATHS, max_paths=5)
for key in ("status", "unit", "wns", "tns", "tns_is_lower_bound",
"violating_paths", "violating_endpoints", "worst_endpoint"):
print(f"{key:<21} {s[key]}")
print()
for group, v in s["per_group"].items():
print(f" {group:<10} wns {v['wns']:+.3f} tns {v['tns']:+.3f}")
print()
print("headline:", f"WNS {s['wns']:+.3f} {s['unit']}, TNS {s['tns']:+.3f} {s['unit']}"
f"{' (lower bound)' if s['tns_is_lower_bound'] else ''}, "
f"{s['violating_paths']} paths on {s['violating_endpoints']} endpoints")status OK
unit ns
wns -0.05
tns -0.095
tns_is_lower_bound True
violating_paths 3
violating_endpoints 2
worst_endpoint fifo/wptr_reg[0]
async_in wns +0.120 tns +0.000
clk wns -0.050 tns -0.095
headline: WNS -0.050 ns, TNS -0.095 ns (lower bound), 3 paths on 2 endpointsSix decisions, and the third is the one that makes the number honest.
negative is the sign-bit test, so the -0.000 path counts — three violating paths rather than two.
tns_is_lower_bound is derived from max_paths. When the path count reaches the limit the report was almost certainly truncated, and the flag travels with the number so Chapter 28.6 cannot compare two incomparable TNS values without noticing.
The unit is carried, per Chapter 28.1 Section 8, so the headline string can never be misread.
Endpoints are grouped and the worst is named, giving the actionable count alongside the raw one.
Per-group figures are included, so the single WNS has a location.
The headline is one string assembled from the record, which is what goes in a status mail — and it says (lower bound) when it must.
10. Predict, Run, Observe
import math
A = [-0.050] + [0.1] * 9
B = [-0.050] * 4 + [0.1] * 6
V = [-0.05, -0.04, -0.03]
# A
print("A:", min(A), min(B))
# B
print("B:", round(sum(s for s in A if s < 0), 3), round(sum(s for s in B if s < 0), 3))
# C
print("C:", len([s for s in A if s < 0]), len([s for s in B if s < 0]))
# D
print("D:", [round(sum(V[:k]), 3) for k in (1, 2, 3)])
# E
print("E:", [min(V[:k]) for k in (1, 2, 3)])
# F
S = [-0.0, -0.045]
print("F:", len([s for s in S if s < 0]),
len([s for s in S if math.copysign(1, s) < 0]))
# G
print("G:", round(sum(s for s in S if s < 0), 3),
round(sum(s for s in S if math.copysign(1, s) < 0), 3))
# H
print("H:", sum(s for s in [0.1, 0.2] if s < 0))The verified output:
A: -0.05 -0.05
B: -0.05 -0.2
C: 1 4
D: [-0.05, -0.09, -0.12]
E: [-0.05, -0.05, -0.05]
F: 1 2
G: -0.045 -0.045
H: 0A — identical WNS from two different designs.
B — and TNS four times apart.
C — with four times the violators.
D — truncation changes TNS at every length.
E — and never changes WNS.
F — the -0.000 path changes the count.
G — and not the TNS.
H — a group with no violators has a TNS of zero, which is a number rather than unknown.
11. Exercises
Exercise 1 — three reductions. For each number, say what it keeps and what it throws away.
Exercise 2 — the ordering. Say which of the three gates a tape-out, which estimates effort, and which estimates places to look.
Exercise 3 — minus zero, three ways. Show which of WNS, TNS and the count the -0.000 path changes.
Exercise 4 — why only one. Explain why TNS is unaffected.
Exercise 5 — same WNS. Construct two slack lists with identical WNS and TNS an order of magnitude apart.
Exercise 6 — effort. Say what a WNS of −0.050 implies about the work, and why it is not reliable.
Exercise 7 — same TNS. Construct two with identical TNS and very different WNS, and describe the two fixes.
Exercise 8 — quote all three. State the rule in one sentence.
Exercise 9 — truncate it. Take ten violators and report WNS and TNS at four truncation lengths.
Exercise 10 — which survives. Explain why WNS is robust to truncation and TNS is not.
Exercise 11 — the incomparable comparison. Construct two runs whose TNS differs only because of the switch.
Exercise 12 — a lower bound. Write the qualification that must accompany a truncated TNS.
Exercise 13 — check the switch. Say what Chapter 28.5 must verify before comparing two TNS figures.
Exercise 14 — paths and endpoints. Group your violators by endpoint and report both counts.
Exercise 15 — the actionable number. Say which of the two belongs at the top of a report.
Exercise 16 — per group. Split your own timing results by clock group and find a group fully met.
Exercise 17 — zero is not unknown. Distinguish a group with no violators from a report with no paths.
Exercise 18 — the headline string. Assemble one from your record, including the lower-bound qualification.
Exercise 19 — carry the unit. Say what the headline string prevents, citing Chapter 28.1.
Exercise 20 — what is missing. Count your design's unconstrained endpoints and express them as a fraction.
Exercise 21 — three exclusions. Distinguish unconstrained, false and multicycle, and say which is dangerous.
Exercise 22 — the missing clock. Describe the report a newly added block with no create_clock produces.
Exercise 23 — coverage in the headline. Add the unconstrained count to your summary and justify its position.
Exercise 24 — a free improvement. Construct two runs where WNS improved because an exclusion was added.
**Exercise 25 — derive the flag.**Exercise 20 — derive the flag. Implement tns_is_lower_bound and say what it protects Chapter 28.6 from.
12. Summary
- Three numbers reduce one list of slacks, and each discards almost everything the others keep.
- WNS gates, TNS estimates effort, the count estimates places to look.
- Chapter 28.1's
-0.000changes the violator count and neither WNS nor TNS, because it has a sign and no magnitude. - Two designs can share a WNS and differ in TNS by forty times — one awkward path against a systemic problem.
- Two designs can share a TNS and differ in WNS by a nanosecond — one structural failure against a hundred near-misses.
- Quote all three, always; any two can agree while the designs are nothing alike.
- Truncation changes TNS and never WNS — one WNS and four TNS values from the same ten violators.
- A TNS from a truncated report is a lower bound on the magnitude, and saying so is the difference between understating and reporting a timing problem.
- Comparing TNS across runs requires identical report switches, which Chapter 28.1 Section 3 recorded for this reason.
- Many paths converge on one endpoint: three violating paths were two places to look.
- The raw count is the symptom count and the grouped count is the problem count.
- An overall WNS names no clock group, and per-group figures turn one number into a map.
- A group with no violators has a TNS of zero, which is a number — unlike Chapter 28.1's unknown.
- All three numbers are computed over the paths the report contains, which is the set the constraints asked for.
- Unconstrained endpoints are reported as nothing at all — not met, not violated — so a missing
create_clockproduces a clean report. - A false path is a deliberate exclusion and the easiest place to hide a violation.
- Report the analysed fraction and the unconstrained count in the headline; a WNS over 95.62% of the design is not a signoff.
- Track exclusion counts run over run — a WNS improvement accompanied by more exclusions is not an improvement.
- Carry the unit and the lower-bound flag with the numbers, so the headline string cannot be misread and Chapter 28.6 cannot compare the incomparable.
The next chapter turns from timing to area, and to the question of which synthesis numbers are comparable between two runs at all.
Where this fits
Part of the Python curriculum.
