Skip to content
VLSI Mentor

Python Part II · Module 28 · Report Parsing

Comparing QoR Between Two Runs

Four chapters have turned three kinds of report into records of numbers. Comparing two of those records is a one-line dict comprehension, and the line is correct on clean input and wrong about almost everything else: it raises on the unknown value Chapter 28.1 deliberately produces, it says nothing about which direction is an improvement, and expressing its results as percentages makes a thirty-three picosecond change look like the largest movement in the report.

Advanced19 min readPythonPart IIAutomationQoRReport Parsing

Part II · Module 28 · Chapter 28.5 · Comparing QoR Between Two Runs

1. Two Records

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BEFORE = {"context": {"library": "tcbn05_lvt", "corner": "ss0p675v125c",
                      "effort": "high", "max_paths": 100},
          "metrics": {"wns": -0.045, "tns": -1.230, "total_area": 128456.3,
                      "cells": 45231, "utilisation": 0.8521, "power_w": 0.2343}}
AFTER = {"context": dict(BEFORE["context"]),
         "metrics": {"wns": -0.012, "tns": -0.310, "total_area": 131002.7,
                     "cells": 45980, "utilisation": 0.8712, "power_w": 0.2401}}

print(f"{'metric':<14} {'before':>13} {'after':>13}")
for k in BEFORE["metrics"]:
    print(f"{k:<14} {BEFORE['metrics'][k]:>13.4f} {AFTER['metrics'][k]:>13.4f}")

print()
print("context identical:", BEFORE["context"] == AFTER["context"])
print("metrics          :", len(BEFORE["metrics"]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
metric                before         after
wns                  -0.0450       -0.0120
tns                  -1.2300       -0.3100
total_area       128456.3000   131002.7000
cells             45231.0000    45980.0000
utilisation           0.8521        0.8712
power_w               0.2343        0.2401

context identical: True
metrics          : 6

Two records of the shape Chapters 28.3 and 28.4 produce: a context of what the runs were, and a metrics of what they measured. The split is doing work, and the next section is what.

2. The Gate Comes First

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PINNED = ("library", "corner", "effort", "max_paths")

BASE = {"library": "tcbn05_lvt", "corner": "ss0p675v125c", "effort": "high",
        "max_paths": 100}
SAME = dict(BASE)
NEW_LIB = dict(BASE, library="tcbn05_svt", corner="tt0p75v25c")
TRUNCATED = dict(BASE, max_paths=10)


def comparable(a, b):
    differ = sorted(k for k in PINNED if a.get(k) != b.get(k))
    return (not differ), differ


for label, other in (("same setup", SAME), ("different library", NEW_LIB),
                     ("different max_paths", TRUNCATED)):
    ok, differ = comparable(BASE, other)
    print(f"{label:<21} comparable={str(ok):<6} differing: {differ}")

print()
print("why max_paths is pinned:", "Chapter 28.2 §5 — TNS depends on it")
print("why library is pinned  :", "Chapter 28.3 §7 — area depends on it")
print("a delta across either  :", "measures the setup, not the design")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
same setup            comparable=True   differing: []
different library     comparable=False  differing: ['corner', 'library']
different max_paths   comparable=False  differing: ['max_paths']

why max_paths is pinned: Chapter 28.2 §5 — TNS depends on it
why library is pinned  : Chapter 28.3 §7 — area depends on it
a delta across either  : measures the setup, not the design

The gate is a precondition, not advice. Chapter 28.2 Section 5 showed TNS changing by a factor of five with -max_paths alone, and Chapter 28.3 Section 7 showed area improving 25% from a library swap — so a comparison across runs that differ in either produces deltas that measure the configuration.

Chapter 27.2 Section 6 made this exact argument about a parallelisation baseline, with workers as the one field allowed to differ. Here nothing is allowed to differ: a QoR comparison between two runs of the same flow at two commits is the whole point, so every setup field is pinned and the design is the only variable.

The function returns the differing fields rather than a boolean alone, so a refusal names what to fix.

3. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
delta = {k: after[k] - before[k] for k in METRICS}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
METRICS = ("wns", "tns", "total_area", "cells", "utilisation", "power_w")
before = {"wns": -0.045, "tns": -1.230, "total_area": 128456.3,
          "cells": 45231, "utilisation": 0.8521, "power_w": 0.2343}
after = {"wns": -0.012, "tns": -0.310, "total_area": 131002.7,
         "cells": 45980, "utilisation": 0.8712, "power_w": 0.2401}

delta = {k: after[k] - before[k] for k in METRICS}

for k, v in delta.items():
    print(f"{k:<14} {v:+13.4f}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wns                  +0.0330
tns                  +0.9200
total_area        +2546.4000
cells              +749.0000
utilisation          +0.0191
power_w              +0.0058

Six deltas, every one positive, and three of them are improvements while three are not. The line is correct arithmetic and an incomplete answer, and before that matters it has to survive the input.

4. Three Ways It Raises

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
METRICS = ("wns", "tns", "total_area", "power_w")
before = {"wns": -0.045, "tns": -1.230, "total_area": 128456.3, "power_w": 0.2343}

CASES = [
    ("unknown wns", {"wns": None, "tns": -0.310, "total_area": 131002.7, "power_w": 0.24}),
    ("missing key", {"wns": -0.012, "tns": -0.310, "total_area": 131002.7}),
    ("string value", {"wns": -0.012, "tns": -0.310, "total_area": "131002.7", "power_w": 0.24}),
]

for label, after in CASES:
    try:
        {k: after[k] - before[k] for k in METRICS}
        print(f"{label:<14} no error")
    except (TypeError, KeyError) as exc:
        print(f"{label:<14} {type(exc).__name__}: {exc}")

print()
print("where the None comes from:", "Chapter 28.1 §9 — an unparseable report")
print("where the missing key does:", "a tool that stopped printing a field")
print("where the string does     :", "a parser that forgot to convert")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unknown wns    TypeError: unsupported operand type(s) for -: 'NoneType' and 'float'
missing key    KeyError: 'power_w'
string value   TypeError: unsupported operand type(s) for -: 'str' and 'float'

where the None comes from: Chapter 28.1 §9 — an unparseable report
where the missing key does: a tool that stopped printing a field
where the string does     : a parser that forgot to convert

The first case is the one this curriculum guaranteed. Chapter 28.1 Section 9 decided that an unparseable timing report yields wns=None rather than a fabricated zero — which was the right decision, and it means the comparison raises on precisely the run somebody most needs compared.

The three failures also arrive as two different exception types from one line, so a caller catching TypeError alone misses the missing-key case.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
METRICS = ("wns", "tns", "total_area", "power_w")


def compare(before, after, metrics=METRICS):
    """Every metric gets an outcome, including the ones that cannot be compared."""
    deltas, unavailable = {}, {}
    for k in metrics:
        b, a = before.get(k), after.get(k)
        if b is None or a is None:
            unavailable[k] = ("missing" if k not in before or k not in after
                              else "unknown in one run")
            continue
        if not isinstance(b, (int, float)) or not isinstance(a, (int, float)):
            unavailable[k] = f"non-numeric ({type(b).__name__}, {type(a).__name__})"
            continue
        deltas[k] = a - b
    return deltas, unavailable


BEFORE = {"wns": -0.045, "tns": -1.230, "total_area": 128456.3, "power_w": 0.2343}
AFTER = {"wns": None, "tns": -0.310, "total_area": "131002.7"}

deltas, unavailable = compare(BEFORE, AFTER)
print("compared    :", {k: round(v, 4) for k, v in deltas.items()})
print("unavailable :", unavailable)
print()
print("metrics asked for:", len(METRICS))
print("accounted for    :", len(deltas) + len(unavailable))
print("nothing silently dropped:", len(deltas) + len(unavailable) == len(METRICS))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compared    : {'tns': 0.92}
unavailable : {'wns': 'unknown in one run', 'total_area': 'non-numeric (float, str)', 'power_w': 'missing'}

metrics asked for: 4
accounted for    : 4
nothing silently dropped: True

Every metric gets an outcome, which is Chapter 26.1's accounting invariant applied to a comparison: the deltas plus the unavailable must equal the metrics asked for.

A comparison that silently omitted the three unavailable metrics would report one delta and look complete.

5. A Delta Has No Direction

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BETTER_WHEN_HIGHER = {"wns", "tns"}

DELTA = {"wns": 0.0330, "tns": 0.9200, "total_area": 2546.4,
         "cells": 749.0, "utilisation": 0.0191, "power_w": 0.0058}

print(f"{'metric':<14} {'delta':>12}  direction")
for k, v in DELTA.items():
    improved = v > 0 if k in BETTER_WHEN_HIGHER else v < 0
    print(f"{k:<14} {v:>+12.4f}  {'BETTER' if improved else 'worse'}")

print()
positive = sum(1 for v in DELTA.values() if v > 0)
better = sum(1 for k, v in DELTA.items()
             if (v > 0 if k in BETTER_WHEN_HIGHER else v < 0))
print("deltas that are positive:", positive, "of", len(DELTA))
print("deltas that are better  :", better, "of", len(DELTA))
print("sign and direction agree:", positive == better)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
metric                delta  direction
wns                 +0.0330  BETTER
tns                 +0.9200  BETTER
total_area       +2546.4000  worse
cells             +749.0000  worse
utilisation         +0.0191  worse
power_w             +0.0058  worse

deltas that are positive: 6 of 6
deltas that are better  : 2 of 6
sign and direction agree: False

Six positive deltas, two improvements.

A table of signed numbers invites the reading "positive is good", and for four of these six it is the opposite. Which way is better is a property of the metric, not of the number, and it has to be declared:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DIRECTION = {"wns": "higher", "tns": "higher", "total_area": "lower",
             "cells": "lower", "utilisation": "lower", "power_w": "lower"}


def verdict(metric, delta):
    if delta == 0:
        return "unchanged"
    better = delta > 0 if DIRECTION[metric] == "higher" else delta < 0
    return "better" if better else "worse"


DELTA = {"wns": 0.0330, "tns": -0.2000, "total_area": -1200.0, "cells": 0.0}

for k, v in DELTA.items():
    print(f"{k:<13} {v:>+11.4f}  better when {DIRECTION[k]:<6} -> {verdict(k, v)}")

print()
print("every metric has a declared direction:", set(DELTA) <= set(DIRECTION))
missing = {"congestion"} - set(DIRECTION)
print("an undeclared metric would:", "KeyError in verdict() — loudly")
print("which is better than      :", "silently assuming higher is good")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wns               +0.0330  better when higher -> better
tns               -0.2000  better when higher -> worse
total_area     -1200.0000  better when lower  -> better
cells             +0.0000  better when lower  -> unchanged

every metric has a declared direction: True
an undeclared metric would: KeyError in verdict() — loudly
which is better than      : silently assuming higher is good

Three decisions worth copying.

The direction table is data, so adding a metric means adding a row and forgetting to means a KeyError rather than a wrong verdict.

Utilisation is declared better-lower, which is a judgement rather than a fact — a design at 60% has room and one at 95% does not, so rising utilisation is usually bad news. A team targeting density might declare it the other way, and the point is that the declaration exists somewhere a reviewer can see it.

Zero is unchanged, not better. Folding it into either direction overstates the number of movements, which matters when Chapter 28.6 counts them.

6. Percentages Lie Near Zero

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BEFORE = {"wns": -0.045, "tns": -1.230, "total_area": 128456.3,
          "cells": 45231, "utilisation": 0.8521, "power_w": 0.2343}
AFTER = {"wns": -0.012, "tns": -0.310, "total_area": 131002.7,
         "cells": 45980, "utilisation": 0.8712, "power_w": 0.2401}

print(f"{'metric':<14} {'before':>13} {'after':>13} {'absolute':>12} {'percent':>9}")
for k in BEFORE:
    b, a = BEFORE[k], AFTER[k]
    pct = (a - b) / abs(b) * 100
    print(f"{k:<14} {b:>13.4f} {a:>13.4f} {a - b:>+12.4f} {pct:>+8.2f}%")

print()
print("largest percentage change:", "tns, +74.80%")
print("largest absolute change  :", "total_area, +2546.4")
print("wns moved by             :", "33 picoseconds")
print("and shows as             :", "+73.33%")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
metric                before         after     absolute   percent
wns                  -0.0450       -0.0120      +0.0330   +73.33%
tns                  -1.2300       -0.3100      +0.9200   +74.80%
total_area       128456.3000   131002.7000   +2546.4000    +1.98%
cells             45231.0000    45980.0000    +749.0000    +1.66%
utilisation           0.8521        0.8712      +0.0191    +2.24%
power_w               0.2343        0.2401      +0.0058    +2.48%

largest percentage change: tns, +74.80%
largest absolute change  : total_area, +2546.4
wns moved by             : 33 picoseconds
and shows as             : +73.33%

A thirty-three picosecond change shows as +73.33%.

Sorted by percentage, the timing metrics dominate the report and area looks like a rounding error. Sorted by absolute value, area dominates and timing vanishes. Both orderings are arithmetically correct and only one of them is informative, and which one depends on the metric.

The reason is structural rather than a quirk of these numbers: a percentage is a ratio to the previous value, and WNS is always near zero — that is what WNS is. A design closing timing goes from −0.045 to −0.005, which is a 89% improvement and 40 picoseconds, and the same 40 picoseconds on a design at −2.0 is 2%.

So the rule is per metric:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RELATIVE_OK = {"total_area", "cells", "utilisation", "power_w"}

ROWS = [("wns", -0.045, -0.012, "ns"), ("tns", -1.230, -0.310, "ns"),
        ("total_area", 128456.3, 131002.7, "um^2"), ("cells", 45231, 45980, "")]

for name, b, a, unit in ROWS:
    absolute = a - b
    if name in RELATIVE_OK:
        print(f"{name:<12} {absolute:+12.1f} {unit:<5} ({(a - b) / abs(b) * 100:+.2f}%)")
    else:
        print(f"{name:<12} {absolute:+12.4f} {unit:<5} (no percentage — near zero)")

print()
print("metrics reported as a percentage:", sorted(RELATIVE_OK))
print("metrics reported absolutely     :", sorted({r[0] for r in ROWS} - RELATIVE_OK))
print("the rule                        :", "a percentage needs a baseline far from zero")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wns               +0.0330 ns    (no percentage — near zero)
tns               +0.9200 ns    (no percentage — near zero)
total_area        +2546.4 um^2  (+1.98%)
cells              +749.0       (+1.66%)

metrics reported as a percentage: ['cells', 'power_w', 'total_area', 'utilisation']
metrics reported absolutely     : ['tns', 'wns']
the rule                        : a percentage needs a baseline far from zero

Timing in absolute units, everything else as a percentage. A +0.033 ns is a figure a timing engineer acts on; a +73% is one they have to convert back.

TNS is the borderline case and is listed with timing here. It is a sum and can be large, so a percentage is sometimes meaningful — and it is zero on a design that meets timing, which makes the ratio undefined. Chapter 28.6 handles that explicitly rather than relying on the value staying away from zero.

7. Metrics Move Together

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DELTA = {"wns": 0.0330, "tns": 0.9200, "total_area": 2546.4,
         "cells": 749.0, "utilisation": 0.0191, "power_w": 0.0058}
DIRECTION = {"wns": "higher", "tns": "higher", "total_area": "lower",
             "cells": "lower", "utilisation": "lower", "power_w": "lower"}

better = [k for k, v in DELTA.items()
          if (v > 0 if DIRECTION[k] == "higher" else v < 0)]
worse = [k for k in DELTA if k not in better]

print("improved:", better)
print("worsened:", worse)
print()
print("six independent findings?", False)
print("what actually happened   :", "the tool traded area for timing")
print("how many decisions       :", 1)
print()
EXPECTED = [("timing improved, area grew", "the optimiser upsized and buffered"),
            ("area grew, utilisation grew", "the same cells, in the same floorplan"),
            ("cells grew, power grew", "more cells switch")]
for pattern, why in EXPECTED:
    print(f"   {pattern:<30} {why}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
improved: ['wns', 'tns']
worsened: ['total_area', 'cells', 'utilisation', 'power_w']

six independent findings? False
what actually happened   : the tool traded area for timing
how many decisions       : 1

   timing improved, area grew     the optimiser upsized and buffered
   area grew, utilisation grew    the same cells, in the same floorplan
   cells grew, power grew         more cells switch

Six metrics moved and one thing happened.

The optimiser upsized cells and inserted buffers to close timing; that grows the cell count, which grows the area, which grows the utilisation in a fixed floorplan, and more cells switching grows the power. Four "regressions" and two "improvements" are one trade.

A comparison that lists six independent deltas invites six investigations, and this is Chapter 26.7's bucketing argument in a new setting: the raw list is the symptom list and the grouping is the finding. Naming the expected co-movements turns a wall of numbers into one sentence.

The practically useful form is to flag the unexpected combinations instead:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def surprising(delta):
    out = []
    if delta["wns"] > 0 and delta["total_area"] < 0:
        out.append("timing improved and area shrank — unusually good, check the setup")
    if delta["wns"] < 0 and delta["total_area"] > 0:
        out.append("timing worsened and area grew — paid for nothing")
    if delta["cells"] > 0 and delta["power_w"] < 0:
        out.append("more cells and less power — check the activity assumptions")
    return out


EXPECTED_TRADE = {"wns": 0.033, "total_area": 2546.4, "cells": 749.0, "power_w": 0.0058}
BAD = {"wns": -0.020, "total_area": 1800.0, "cells": 500.0, "power_w": 0.004}
ODD = {"wns": 0.050, "total_area": -900.0, "cells": -200.0, "power_w": -0.002}

for label, d in (("expected trade", EXPECTED_TRADE), ("paid for nothing", BAD),
                 ("too good", ODD)):
    print(f"{label:<18} {surprising(d) or ['nothing surprising']}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
expected trade     ['nothing surprising']
paid for nothing   ['timing worsened and area grew — paid for nothing']
too good           ['timing improved and area shrank — unusually good, check the setup']

The expected trade produces no finding, which is the point: a comparison should be quiet when the design behaved the way designs behave, and loud when it did not.

The third case deserves its wording. Timing improving while area shrinks is possible — a better floorplan, a fixed constraint — and it is also the signature of a comparison across two different setups that Section 2's gate was supposed to catch. Unusually good results are worth the same scrutiny as unusually bad ones, and almost nothing in a flow checks for them.

8. More Than Two Runs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SWEEP = [
    ("effort=low", {"wns": -0.180, "total_area": 121003.4, "power_w": 0.2201}),
    ("effort=medium", {"wns": -0.072, "total_area": 125440.1, "power_w": 0.2288}),
    ("effort=high", {"wns": -0.012, "total_area": 131002.7, "power_w": 0.2401}),
    ("effort=ultra", {"wns": -0.008, "total_area": 138771.2, "power_w": 0.2620}),
]

base = SWEEP[0][1]
print(f"{'run':<15} {'wns':>9} {'area':>12} {'power':>8}  {'d-wns':>8} {'d-area':>10}")
for name, m in SWEEP:
    print(f"{name:<15} {m['wns']:>+9.3f} {m['total_area']:>12.1f} {m['power_w']:>8.4f}  "
          f"{m['wns'] - base['wns']:>+8.3f} {m['total_area'] - base['total_area']:>+10.1f}")

print()
print("runs            :", len(SWEEP))
print("pairwise deltas :", len(SWEEP) * (len(SWEEP) - 1) // 2)
print("useful deltas   :", len(SWEEP) - 1, "— each against the common baseline")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
run                   wns         area    power     d-wns     d-area
effort=low         -0.180     121003.4   0.2201    +0.000       +0.0
effort=medium      -0.072     125440.1   0.2288    +0.108    +4436.7
effort=high        -0.012     131002.7   0.2401    +0.168    +9999.3
effort=ultra       -0.008     138771.2   0.2620    +0.172   +17767.8

runs            : 4
pairwise deltas : 6
useful deltas   : 3 — each against the common baseline

A QoR comparison is usually presented as two runs, and the question that actually gets asked is a sweep: which effort level, which floorplan, which of four candidate constraint files. Chapter 28.5's pairwise function generalises by fixing one run as the baseline rather than by comparing every pair.

Six pairwise comparisons over four runs is not six findings — it is three deltas against a common reference plus three derivable from them. Presenting all six invites the reader to find a pattern in redundant numbers.

What a sweep reveals that a pair cannot is the shape of the trade:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SWEEP = [
    ("low", -0.180, 121003.4), ("medium", -0.072, 125440.1),
    ("high", -0.012, 131002.7), ("ultra", -0.008, 138771.2),
]

print(f"{'step':<16} {'d-wns (ns)':>11} {'d-area (um^2)':>14} {'um^2 per ps':>13}")
for (n0, w0, a0), (n1, w1, a1) in zip(SWEEP, SWEEP[1:]):
    dw, da = w1 - w0, a1 - a0
    cost = da / (dw * 1000) if dw else float("inf")
    print(f"{n0 + ' -> ' + n1:<16} {dw:>+11.3f} {da:>+14.1f} {cost:>13.0f}")

print()
print("the marginal cost of a picosecond rises:", True)
print("the last step buys                     :", "4 ps for 7769 um^2")
print("which is                               :", "about 47x the cost of the first step")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step              d-wns (ns)  d-area (um^2)   um^2 per ps
low -> medium         +0.108        +4436.7            41
medium -> high        +0.060        +5562.6            93
high -> ultra         +0.004        +7768.5          1942

the marginal cost of a picosecond rises: True
the last step buys                     : 4 ps for 7769 um^2
which is                               : about 47x the cost of the first step

That output stops before the three statements; the real output follows.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step               d-wns (ns)  d-area (um^2)   um^2 per ps
low -> medium          +0.108        +4436.7            41
medium -> high         +0.060        +5562.6            93
high -> ultra          +0.004        +7768.5          1942

the marginal cost of a picosecond rises: True
the last step buys                     : 4 ps for 7769 um^2
which is                               : about 47x the cost of the first step

41, 93 and 1942 square microns per picosecond. The last step costs twenty-one times what the one before it did, which is the diminishing-returns curve every optimiser has and which no pair of runs can show.

That number is the one a decision is made on. "High effort gives −0.012 and ultra gives −0.008" is two facts; "the last four picoseconds cost 7,769 µm²" is a choice somebody can make.

Three properties of the sweep comparison are worth copying.

One baseline, not every pair. SWEEP[0] is the reference and every row is relative to it, so the table has n-1 meaningful rows rather than n(n-1)/2 redundant ones.

The marginal view is between adjacent runs, which is a different computation from the cumulative one and answers the different question — what did this step buy? rather than where are we?

The derived ratio is the output. Chapter 28.5 Section 6 argued against percentages near zero; a ratio of two deltas in their own units sidesteps that entirely and is more actionable than either column.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SWEEP = {"low": {"wns": -0.180, "area": 121003.4},
         "medium": {"wns": -0.072, "area": 125440.1},
         "high": {"wns": -0.012, "area": 131002.7},
         "ultra": {"wns": -0.008, "area": 138771.2}}
TARGET_WNS = -0.020

meeting = [n for n, m in SWEEP.items() if m["wns"] >= TARGET_WNS]
cheapest = min(meeting, key=lambda n: SWEEP[n]["area"]) if meeting else None

print("target wns     :", f"{TARGET_WNS:+.3f}")
print("runs that meet :", meeting)
print("cheapest of them:", cheapest)
print("its area       :", SWEEP[cheapest]["area"] if cheapest else None)
print()
best = min(SWEEP, key=lambda n: SWEEP[n]["wns"] and -SWEEP[n]["wns"])
print("best timing    :", max(SWEEP, key=lambda n: SWEEP[n]["wns"]))
print("area it costs  :", SWEEP[max(SWEEP, key=lambda n: SWEEP[n]['wns'])]["area"])
print("area saved by picking the cheapest that meets:",
      round(SWEEP["ultra"]["area"] - SWEEP[cheapest]["area"], 1))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
target wns     : -0.020
runs that meet : ['high', 'ultra']
cheapest of them: high
its area       : 131002.7

best timing    : ultra
area it costs  : 138771.2
area saved by picking the cheapest that meets: 7768.5

The cheapest run that meets the target, not the best run. ultra wins on timing and costs 7,768 µm² more than high, which also meets the target — so the sweep's output is not a maximum but a constrained minimum.

That is what turns a comparison into a decision, and it is the shape Chapter 28.6's gate needs: a target to meet rather than a direction to improve.

9. The Comparison, Assembled

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import json

METRICS = ("wns", "tns", "total_area", "cells", "utilisation", "power_w")
DIRECTION = {"wns": "higher", "tns": "higher", "total_area": "lower",
             "cells": "lower", "utilisation": "lower", "power_w": "lower"}
RELATIVE_OK = {"total_area", "cells", "utilisation", "power_w"}
PINNED = ("library", "corner", "effort", "max_paths")
UNITS = {"wns": "ns", "tns": "ns", "total_area": "um^2", "cells": "",
         "utilisation": "", "power_w": "W"}


class NotComparable(Exception):
    """The two runs differ in setup, so no delta means anything."""


def compare_qor(before, after):
    differ = sorted(k for k in PINNED
                    if before["context"].get(k) != after["context"].get(k))
    if differ:
        raise NotComparable(f"contexts differ in {differ} — compare the setup, not the design")

    rows, unavailable = [], {}
    for name in METRICS:
        b, a = before["metrics"].get(name), after["metrics"].get(name)
        if b is None or a is None or not isinstance(b, (int, float)) \
                or not isinstance(a, (int, float)):
            unavailable[name] = "unknown or missing in one run"
            continue
        delta = a - b
        if delta == 0:
            direction = "unchanged"
        else:
            direction = "better" if (delta > 0 if DIRECTION[name] == "higher"
                                     else delta < 0) else "worse"
        percent = None
        if name in RELATIVE_OK and b != 0:
            percent = round(delta / abs(b) * 100, 2)
        rows.append({"metric": name, "before": b, "after": a,
                     "delta": round(delta, 6), "percent": percent,
                     "direction": direction, "unit": UNITS[name]})
    return {"rows": rows, "unavailable": unavailable,
            "better": [r["metric"] for r in rows if r["direction"] == "better"],
            "worse": [r["metric"] for r in rows if r["direction"] == "worse"],
            "accounted": len(rows) + len(unavailable) == len(METRICS)}


CTX = {"library": "tcbn05_lvt", "corner": "ss0p675v125c", "effort": "high",
       "max_paths": 100}
BEFORE = {"context": CTX, "metrics": {"wns": -0.045, "tns": -1.230,
                                      "total_area": 128456.3, "cells": 45231,
                                      "utilisation": 0.8521, "power_w": 0.2343}}
AFTER = {"context": dict(CTX), "metrics": {"wns": -0.012, "tns": -0.310,
                                           "total_area": 131002.7, "cells": 45980,
                                           "utilisation": 0.8712, "power_w": None}}

result = compare_qor(BEFORE, AFTER)
print(f"{'metric':<13} {'delta':>12} {'pct':>8}  {'dir':<9} unit")
for r in result["rows"]:
    pct = f"{r['percent']:+.2f}%" if r["percent"] is not None else "—"
    print(f"{r['metric']:<13} {r['delta']:>+12.4f} {pct:>8}  {r['direction']:<9} {r['unit']}")

print()
print("better      :", result["better"])
print("worse       :", result["worse"])
print("unavailable :", result["unavailable"])
print("accounted   :", result["accounted"])
print()
try:
    compare_qor(BEFORE, {"context": dict(CTX, library="other"), "metrics": {}})
except NotComparable as exc:
    print("gate:", exc)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
metric               delta      pct  dir       unit
wns                +0.0330        —  better    ns
tns                +0.9200        —  better    ns
total_area      +2546.4000   +1.98%  worse     um^2
cells            +749.0000   +1.66%  worse     
utilisation        +0.0191   +2.24%  worse     

better      : ['wns', 'tns']
worse       : ['total_area', 'cells', 'utilisation']
unavailable : {'power_w': 'unknown or missing in one run'}
accounted   : True

gate: contexts differ in ['library'] — compare the setup, not the design

Six decisions, and the first is the one that makes everything after it mean something.

The gate raises before any arithmetic. A NotComparable naming the fields is the only honest response to two different experiments, and it cannot be ignored the way a warning can.

Every metric is accounted for, as a row or as unavailable, and the accounted flag asserts it — Chapter 26.1's invariant, one more time.

percent is None for timing and for a zero baseline, so the formatting shows an em dash rather than a misleading number or a ZeroDivisionError.

direction is derived from a declared table, and an undeclared metric raises rather than defaulting.

Zero is unchanged, kept out of both lists.

The unit travels with each row, per Chapter 28.1 Section 8, so the rendered table cannot be misread.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B = {"wns": -0.045, "area": 128456.3}
A = {"wns": -0.012, "area": 131002.7}
DIR = {"wns": "higher", "area": "lower"}

# A
print("A:", {k: round(A[k] - B[k], 4) for k in B})

# B
print("B:", all(A[k] - B[k] > 0 for k in B))

# C
print("C:", [(k, "better" if ((A[k] - B[k] > 0) if DIR[k] == "higher"
                              else (A[k] - B[k] < 0)) else "worse") for k in B])

# D
print("D:", round((A["wns"] - B["wns"]) / abs(B["wns"]) * 100, 2),
      round((A["area"] - B["area"]) / abs(B["area"]) * 100, 2))

# E
print("E:", round(A["wns"] - B["wns"], 4))

# F
try:
    None - 1.0
except TypeError as exc:
    print("F:", type(exc).__name__)

# G
try:
    {"a": 1}["b"]
except KeyError as exc:
    print("G:", type(exc).__name__, exc)

# H
try:
    1.0 / abs(0.0)
except ZeroDivisionError:
    print("H: ZeroDivisionError")

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: {'wns': 0.033, 'area': 2546.4}
B: True
C: [('wns', 'better'), ('area', 'worse')]
D: 73.33 1.98
E: 0.033
F: TypeError
G: KeyError 'b'
H: ZeroDivisionError

A — the annotation's deltas.

B — both positive.

C — and only one is an improvement.

D — the percentage says timing moved 37 times more than area.

E — in nanoseconds it moved 33 picoseconds.

F — an unknown value raises, and Chapter 28.1 produces those by design.

G — a missing key raises differently, so one except clause is not enough.

H — and a zero baseline makes the percentage impossible.

11. Exercises

Exercise 1 — two records. Produce context and metrics for two of your own runs.

Exercise 2 — the gate. List the fields your flow must pin, and say which chapter justifies each.

Exercise 3 — refuse, do not warn. Argue in two sentences for raising rather than annotating.

Exercise 4 — three failures. Reproduce the None, missing-key and string cases.

Exercise 5 — where the None comes from. Trace it back to Chapter 28.1's decision and say why that decision was right.

Exercise 6 — two exception types. Say what a caller catching only TypeError misses.

Exercise 7 — account for everything. Add the accounted check and break it deliberately.

Exercise 8 — six positive, two better. Build the table and count both.

Exercise 9 — direction as data. Write your own table and show an undeclared metric raising.

Exercise 10 — defend utilisation. Say which direction you declare it and why.

Exercise 11 — zero is unchanged. Say what folding it into a direction does to Chapter 28.6's counts.

Exercise 12 — the two orderings. Sort your own comparison by percentage and by absolute value, and describe the difference.

Exercise 13 — why WNS is near zero. Explain the structural reason a percentage misleads there.

Exercise 14 — per-metric rule. Decide which of your metrics get percentages.

Exercise 15 — the zero baseline. Construct a TNS that was zero and is now negative, and handle it.

Exercise 16 — one trade. Take a real comparison and group the moved metrics into the decisions behind them.

Exercise 17 — the expected co-movements. Write down three for your flow.

Exercise 18 — quiet when normal. Implement the surprising-combination check and show it silent on an expected trade.

Exercise 19 — too good. Say why an unusually good result deserves the same scrutiny, and which gate it usually means failed.

Exercise 20 — a sweep. Compare four runs against a common baseline rather than pairwise, and say how many rows each gives.

Exercise 21 — the marginal view. Compute the cost per picosecond between adjacent runs of your own sweep.

Exercise 22 — diminishing returns. Report the ratio between your first and last step.

Exercise 23 — a ratio beats a percentage. Say why a cost-per-picosecond sidesteps Section 6's problem.

Exercise 24 — cheapest that meets. Pick a target and find the cheapest run meeting it rather than the best.

**Exercise 25 — the rendered table.**Exercise 20 — the rendered table. Add units and em dashes, and say what each prevents.

12. Summary

  • A QoR record splits into context and metrics, and the comparison treats them differently: one is checked for equality, the other is subtracted.
  • The comparability gate comes first and raises — a delta across different libraries, corners or report switches measures the setup.
  • The annotation's comprehension fails three ways: a None, a missing key and a non-numeric value, as two different exception types.
  • Chapter 28.1 guarantees the None by refusing to fabricate a WNS, so the comparison raises on the run that most needs comparing.
  • Give every metric an outcome — a delta or a reason it is unavailable — and assert that they add up.
  • A raw delta carries no direction. Six positive deltas were two improvements.
  • Declare the direction per metric as data, so an undeclared metric raises instead of defaulting.
  • Zero is unchanged, not better and not worse.
  • A percentage is a ratio to the previous value, and WNS is always near zero — 33 picoseconds showed as +73.33%.
  • Report timing absolutely and area, cells, utilisation and power as percentages.
  • A zero baseline makes the percentage a ZeroDivisionError, and that case — TNS was zero, now it is not — is the most important regression there is.
  • Metrics move together: six movements were one trade of area for timing.
  • Flag the unexpected combinations instead of listing every delta, so the comparison is quiet when the design behaved normally.
  • An unusually good result deserves the same scrutiny as a bad one, and usually means the gate in Section 2 was not applied.
  • A sweep of four runs gives three meaningful deltas, not six — fix one baseline rather than comparing every pair.
  • The marginal view between adjacent runs is a different computation and answers what each step bought.
  • Cost per picosecond rose from 41 to 1942 µm² across one effort sweep — a curve no pair of runs can show.
  • A ratio of two deltas in their own units sidesteps the percentage problem entirely and is more actionable than either column.
  • The output of a sweep is the cheapest run that meets the target, not the best run — a constrained minimum rather than a maximum.
  • Carry the unit on every row, so the rendered table cannot be misread.

The last chapter of this module turns this comparison into an automatic verdict, which needs one more thing: a threshold that knows the difference between a regression and noise.

Where this fits

Part of the Python curriculum.