Skip to content
VLSI Mentor

Python Part II · Module 28 · Report Parsing

Detecting a QoR Regression

Chapter 28.5 produced a table of differences and left the hard question open: which of them matter. Turning that into an automatic verdict is one comparison against a threshold, and the threshold is the entire chapter — because a place-and-route flow does not produce the same answer twice, so some difference between two runs of the identical design is guaranteed. A number chosen rather than measured is either too tight, in which case the check fires on identical designs, or too loose, in which case it misses the regression it was written for.

Advanced19 min readPythonPart IIAutomationQoRReport Parsing

Part II · Module 28 · Chapter 28.6 · Detecting a QoR Regression

1. The Flow Is Not Deterministic

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
SOURCES = [
    ("multi-threaded placement", "work is split by thread, and threads race"),
    ("timing-driven optimisation", "it stops on a wall-clock or effort budget"),
    ("machine load", "a busier machine reaches a different stopping point"),
    ("tool version", "a patch release changes a heuristic"),
]

for name, why in SOURCES:
    print(f"{name:<28} {why}")

print()
print("the design was identical in all four:", True)
print("the QoR numbers were             :", "not identical")
print("so a difference of zero is       :", "not the expected outcome")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
multi-threaded placement     work is split by thread, and threads race
timing-driven optimisation   it stops on a wall-clock or effort budget
machine load                 a busier machine reaches a different stopping point
tool version                 a patch release changes a heuristic

the design was identical in all four: True
the QoR numbers were             : not identical
so a difference of zero is       : not the expected outcome

Two runs of the same design produce different numbers, which is the premise everything else rests on. A simulation regression can be driven to Chapter 25.7's zero non-determinism; a place-and-route flow cannot, because the non-determinism is in the tool's own search.

So a comparison always shows differences, and a verdict needs to know which size of difference is ordinary.

2. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if delta["wns"] < -0.050:
    fail("timing regression beyond noise threshold")

The comment names exactly the right concept — beyond noise — and the chapter is about the number in front of it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
QUESTIONS = [
    ("where does 0.050 come from?", "measured, or chosen?"),
    ("what if wns is unavailable?", "Chapter 28.5 §4 produces that"),
    ("what about the other metrics?", "a verdict on one is not a verdict"),
    ("is an absolute threshold right?", "0.050 against a baseline of -0.010 or -2.000?"),
]

for q, note in QUESTIONS:
    print(f"{q:<34} {note}")

print()
print("the comment is right:", True)
print("the number is        :", "unjustified until somebody measures")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
where does 0.050 come from?        measured, or chosen?
what if wns is unavailable?        Chapter 28.5 §4 produces that
what about the other metrics?      a verdict on one is not a verdict
is an absolute threshold right?    0.050 against a baseline of -0.010 or -2.000?

the comment is right: True
the number is        : unjustified until somebody measures

3. Measure the Noise

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import statistics

SAME_DESIGN = [-0.045, -0.038, -0.052, -0.041, -0.049, -0.036, -0.047, -0.043]

print("eight runs, identical inputs:")
print("  ", SAME_DESIGN)
print()
print("mean          :", round(statistics.mean(SAME_DESIGN), 4))
print("stdev         :", round(statistics.stdev(SAME_DESIGN), 4))
print("min           :", min(SAME_DESIGN))
print("max           :", max(SAME_DESIGN))
print("observed spread:", round(max(SAME_DESIGN) - min(SAME_DESIGN), 4))
print("3 sigma       :", round(3 * statistics.stdev(SAME_DESIGN), 4))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
eight runs, identical inputs:
   [-0.045, -0.038, -0.052, -0.041, -0.049, -0.036, -0.047, -0.043]

mean          : -0.0439
stdev         : 0.0055
min           : -0.052
max           : -0.036
observed spread: 0.016
3 sigma       : 0.0164

The same design, eight times, spread across sixteen picoseconds. That is the number a threshold has to clear, and the only way to obtain it is the experiment above — Chapter 25.7's soak, pointed at an implementation flow instead of a simulation.

The sample is a constructed list here; in a real flow it is eight nightly runs of an unchanged commit, which costs nothing extra because the nightly runs anyway.

Now price every candidate threshold against it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import statistics

SAME_DESIGN = [-0.045, -0.038, -0.052, -0.041, -0.049, -0.036, -0.047, -0.043]
pairs = [(a, b) for i, a in enumerate(SAME_DESIGN)
         for j, b in enumerate(SAME_DESIGN) if i != j]

print(f"{'threshold':>10} {'fires on':>9} {'of':>5} {'false positives':>17}")
for t in (0.005, 0.010, 0.016, 0.020, 0.050):
    fires = sum(1 for a, b in pairs if b - a < -t)
    print(f"{t:>10.3f} {fires:>9} {len(pairs):>5} {100 * fires / len(pairs):>16.1f}%")

print()
print("observed spread :", round(max(SAME_DESIGN) - min(SAME_DESIGN), 4))
print("3 sigma         :", round(3 * statistics.stdev(SAME_DESIGN), 4))
print("the annotation's:", 0.050)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 threshold  fires on    of   false positives
     0.005        16    56             28.6%
     0.010         6    56             10.7%
     0.016         0    56              0.0%
     0.020         0    56              0.0%
     0.050         0    56              0.0%

observed spread : 0.016
3 sigma         : 0.0164
the annotation's: 0.05

A threshold of 0.005 fires on 28.6% of pairs of identical runs.

Every one of those is a false alarm: the design did not change, the flow did, and the check reports a timing regression. At one nightly a day that is a spurious failure roughly twice a week, which is Chapter 25.7 Section 6's noise floor in a new place — and once a team has seen three of them, the fourth is ignored.

Three observations about the table.

The three-sigma figure and the observed spread agree, at 0.0164 and 0.016. That is a coincidence of this sample and a useful sanity check: if they disagree badly, the distribution is not what the statistics assume and the observed spread is the one to trust.

The annotation's 0.050 is safe and three times larger than it needs to be. It will never false-alarm on this flow and it will miss any regression between 16 and 50 picoseconds — which, on a design at −0.045, is most of the ones worth catching.

Neither of those facts is knowable without the experiment. A number chosen by intuition can be wrong in either direction and there is no way to tell from inside the flow.

4. Unavailable Is Not Pass

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DELTA_OK = {"wns": -0.080, "total_area": 1200.0}
DELTA_MISSING = {"total_area": 1200.0}

for label, delta in (("wns present", DELTA_OK), ("wns unavailable", DELTA_MISSING)):
    try:
        verdict = "FAIL" if delta["wns"] < -0.050 else "PASS"
    except KeyError as exc:
        verdict = f"KeyError: {exc}"
    print(f"{label:<17} {verdict}")

print()
print("where the missing wns comes from:", "Chapter 28.5 §4 — unavailable metric")
print("why that run matters             :", "an unparseable report is itself a problem")
print()
for guard, name in ((lambda d: d.get("wns") is not None, "a .get() guard"),):
    print(f"{name:<16} present: {guard(DELTA_OK)}  missing: {guard(DELTA_MISSING)}")
print()
print("the honest verdict when it is missing:", "UNKNOWN")
print("not                                  :", "PASS")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wns present       FAIL
wns unavailable   KeyError: 'wns'

where the missing wns comes from: Chapter 28.5 §4 — unavailable metric
why that run matters             : an unparseable report is itself a problem

a .get() guard   present: True  missing: False

the honest verdict when it is missing: UNKNOWN
not                                  : PASS

The annotation raises on exactly the run a verdict matters for.

Chapter 28.1 Section 9 chose to report an unknown WNS as None rather than fabricating a zero, and Chapter 28.5 Section 4 carried that through as an unavailable metric. Both were right, and the consequence lands here: a check written as delta["wns"] crashes, and the obvious fix — delta.get("wns", 0.0) — is worse, because it says no change about a report nobody could read.

The verdict needs three outcomes rather than two:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def verdict(delta, threshold=-0.016):
    if "wns" not in delta or delta["wns"] is None:
        return "UNKNOWN", "wns unavailable — the timing report could not be read"
    if delta["wns"] < threshold:
        return "FAIL", f"wns regressed by {delta['wns']:+.4f}, beyond {threshold:+.4f}"
    return "PASS", f"wns moved {delta['wns']:+.4f}, within noise"


CASES = [("clean", {"wns": 0.003}), ("regressed", {"wns": -0.080}),
         ("marginal", {"wns": -0.010}), ("unreadable", {"wns": None}),
         ("absent", {})]

for label, delta in CASES:
    v, why = verdict(delta)
    print(f"{label:<12} {v:<8} {why}")

print()
outcomes = sorted({verdict(d)[0] for _, d in CASES})
print("distinct outcomes:", outcomes)
print("UNKNOWN blocks a merge the same as FAIL:", "yes — it is not evidence of success")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clean        PASS     wns moved +0.0030, within noise
regressed    FAIL     wns regressed by -0.0800, beyond -0.0160
marginal     PASS     wns moved -0.0100, within noise
unreadable   UNKNOWN  wns unavailable — the timing report could not be read
absent       UNKNOWN  wns unavailable — the timing report could not be read

distinct outcomes: ['FAIL', 'PASS', 'UNKNOWN']
UNKNOWN blocks a merge the same as FAIL: yes — it is not evidence of success

Three outcomes, and UNKNOWN is not a pass. This is Chapter 26.1 Section 5's vocabulary argument: a gate with two states forces every uncertain case into one of them, and the comfortable direction is always the permissive one.

Note the marginal row. A WNS that dropped by 10 picoseconds is PASS under a measured threshold of 16, and that is the threshold working — not a near miss. Reporting it as "within noise" rather than silently is what lets somebody disagree with the calibration.

5. One Metric Is Not a Verdict

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DELTA = {"wns": 0.010, "tns": 0.200, "total_area": 9000.0,
         "cells": 2400.0, "power_w": 0.030}

annotation_verdict = "FAIL" if DELTA["wns"] < -0.050 else "PASS"
print("the annotation says:", annotation_verdict)
print()
print("what else happened:")
BASELINE = {"total_area": 128456.3, "power_w": 0.2343}
for k in ("total_area", "power_w"):
    pct = DELTA[k] / BASELINE[k] * 100
    print(f"   {k:<12} {DELTA[k]:>+10.4f}  {pct:+.2f}%")

print()
print("area grew by      :", f"{DELTA['total_area'] / BASELINE['total_area'] * 100:.1f}%")
print("power grew by     :", f"{DELTA['power_w'] / BASELINE['power_w'] * 100:.1f}%")
print("checked by the gate:", "neither")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the annotation says: PASS

what else happened:
   total_area   +9000.0000  +7.01%
   power_w         +0.0300  +12.80%

area grew by      : 7.0%
power grew by     : 12.8%
checked by the gate: neither

A seven percent area growth and a thirteen percent power increase, reported as a pass.

A gate on one metric is a gate on one metric, and the one chosen is usually timing because timing is what blocks a tape-out. Area and power block one later and more expensively.

Every metric needs its own measured threshold, in its own units:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import statistics

NOISE_SAMPLES = {
    "wns": [-0.045, -0.038, -0.052, -0.041, -0.049, -0.036, -0.047, -0.043],
    "total_area": [128456.3, 128502.1, 128399.7, 128511.0, 128460.2,
                   128433.8, 128478.9, 128444.5],
    "power_w": [0.2343, 0.2351, 0.2338, 0.2349, 0.2340, 0.2346, 0.2342, 0.2347],
}


def threshold_from(samples, sigmas=3):
    """A regression must exceed the observed spread AND the sigma estimate."""
    spread = max(samples) - min(samples)
    sigma = sigmas * statistics.stdev(samples)
    return round(max(spread, sigma), 6)


for metric, samples in NOISE_SAMPLES.items():
    spread = round(max(samples) - min(samples), 6)
    sigma = round(3 * statistics.stdev(samples), 6)
    print(f"{metric:<12} spread {spread:>12} 3-sigma {sigma:>12} "
          f"threshold {threshold_from(samples):>12}")

print()
print("each metric has its own unit:", True)
print("a single threshold for all  :", "meaningless — ns against um^2 against W")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wns          spread        0.016 3-sigma     0.016388 threshold     0.016388
total_area   spread        111.3 3-sigma    109.35959 threshold        111.3
power_w      spread       0.0013 3-sigma     0.001351 threshold     0.001351

each metric has its own unit: True
a single threshold for all  : meaningless — ns against um^2 against W

Three metrics, three thresholds, three units. Area's noise is 111 µm² — so the 9000 µm² growth in the previous example is eighty times the noise and unambiguously real.

Note which estimate won for each metric: wns and power_w took the three-sigma figure and total_area took the observed spread, because its sigma estimate (109.4) came out below what actually happened (111.3). That is exactly the case max(spread, sigma) exists for — a sigma estimate that under-predicts the real range is the one that would have produced false alarms.

max(spread, sigma) is deliberately the more conservative of the two estimates. The observed spread is what actually happened and the sigma figure extrapolates; taking the larger means a few extra missed marginal regressions and no false alarms, which is the right trade for a gate people have to trust.

6. Absolute or Relative

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BASELINES = (-0.010, -0.500, -2.000)
ABSOLUTE = 0.050

print(f"{'baseline':>10} {'absolute T':>12} {'as a fraction of baseline':>27}")
for b in BASELINES:
    print(f"{b:>10.3f} {ABSOLUTE:>12.3f} {abs(ABSOLUTE / b) * 100:>26.1f}%")

print()
print("the same threshold means:", "very different things")
print()
RELATIVE = 0.10
print(f"{'baseline':>10} {'relative T':>12} {'in ns':>10}")
for b in BASELINES:
    print(f"{b:>10.3f} {RELATIVE:>11.0%} {abs(b) * RELATIVE:>10.4f}")
print()
print("the relative threshold on a design at -0.010:", f"{abs(-0.010) * RELATIVE:.4f} ns")
print("which is below the measured noise of        :", 0.0164)
print("so it would fire constantly                 :", abs(-0.010) * RELATIVE < 0.0164)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  baseline   absolute T   as a fraction of baseline
    -0.010        0.050                      500.0%
    -0.500        0.050                       10.0%
    -2.000        0.050                        2.5%

the same threshold means: very different things

  baseline   relative T      in ns
    -0.010         10%     0.0010
    -0.500         10%     0.0500
    -2.000         10%     0.2000

the relative threshold on a design at -0.010: 0.0010 ns
which is below the measured noise of        : 0.0164
so it would fire constantly                 : True

Neither form works alone. An absolute threshold is 500% of one baseline and 2.5% of another; a relative one collapses below the noise floor as the design closes timing.

Chapter 28.5 Section 6 made the same point about reporting a change and the resolution here is the same shape — the floor comes from the noise and the ceiling from the proportion:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NOISE_T = 0.0164
RELATIVE = 0.10


def timing_threshold(baseline):
    """At least the noise floor; more for a design with a lot of slack to lose."""
    return max(NOISE_T, abs(baseline) * RELATIVE)


for baseline in (-0.010, -0.100, -0.500, -2.000):
    t = timing_threshold(baseline)
    binding = "noise" if t == NOISE_T else "proportion"
    print(f"baseline {baseline:>+7.3f}  threshold {t:>7.4f}  set by {binding}")

print()
print("below -0.164, the floor binds:", timing_threshold(-0.010) == NOISE_T)
print("above it, the proportion does:", timing_threshold(-2.000) > NOISE_T)
print("the crossover                :", f"{NOISE_T / RELATIVE:.3f} ns of baseline slack")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
baseline  -0.010  threshold  0.0164  set by noise
baseline  -0.100  threshold  0.0164  set by noise
baseline  -0.500  threshold  0.0500  set by proportion
baseline  -2.000  threshold  0.2000  set by proportion

below -0.164, the floor binds: True
above it, the proportion does: True
the crossover                : 0.164 ns of baseline slack

A floor at the measured noise and a proportion above it, crossing over at 0.164 ns of baseline. A design close to meeting timing is gated on the noise floor, because every picosecond matters there; a design far out is gated proportionally, because a 10 picosecond move on a −2.0 ns path is not the problem.

7. The Gate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import statistics

NOISE_SAMPLES = {
    "wns": [-0.045, -0.038, -0.052, -0.041, -0.049, -0.036, -0.047, -0.043],
    "total_area": [128456.3, 128502.1, 128399.7, 128511.0, 128460.2,
                   128433.8, 128478.9, 128444.5],
    "power_w": [0.2343, 0.2351, 0.2338, 0.2349, 0.2340, 0.2346, 0.2342, 0.2347],
}
DIRECTION = {"wns": "higher", "total_area": "lower", "power_w": "lower"}
UNITS = {"wns": "ns", "total_area": "um^2", "power_w": "W"}


def thresholds(samples, sigmas=3):
    return {m: round(max(max(v) - min(v), sigmas * statistics.stdev(v)), 6)
            for m, v in samples.items()}


def gate(before, after, limits):
    findings, unknown = [], []
    for metric, limit in sorted(limits.items()):
        b, a = before.get(metric), after.get(metric)
        if b is None or a is None:
            unknown.append(metric)
            continue
        delta = a - b
        worse = delta < 0 if DIRECTION[metric] == "higher" else delta > 0
        if worse and abs(delta) > limit:
            findings.append({"metric": metric, "delta": round(delta, 6),
                             "limit": limit, "factor": round(abs(delta) / limit, 1),
                             "unit": UNITS[metric]})
    if unknown:
        return "UNKNOWN", findings, unknown
    return ("FAIL" if findings else "PASS"), findings, unknown


LIMITS = thresholds(NOISE_SAMPLES)
BEFORE = {"wns": -0.045, "total_area": 128456.3, "power_w": 0.2343}

for label, after in (
    ("clean", {"wns": -0.043, "total_area": 128470.0, "power_w": 0.2345}),
    ("timing regressed", {"wns": -0.120, "total_area": 128470.0, "power_w": 0.2345}),
    ("area regressed", {"wns": -0.043, "total_area": 137456.3, "power_w": 0.2345}),
    ("report unreadable", {"wns": None, "total_area": 128470.0, "power_w": 0.2345}),
):
    v, findings, unknown = gate(BEFORE, after, LIMITS)
    print(f"{label:<19} {v}")
    for f in findings:
        print(f"      {f['metric']} {f['delta']:+.4f} {f['unit']} "
              f"(limit {f['limit']:.4f}, {f['factor']}x)")
    if unknown:
        print(f"      unavailable: {unknown}")

print()
print("thresholds in use:", {k: round(v, 4) for k, v in LIMITS.items()})
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clean               PASS
timing regressed    FAIL
      wns -0.0750 ns (limit 0.0164, 4.6x)
area regressed      FAIL
      total_area +9000.0000 um^2 (limit 111.3000, 80.9x)
report unreadable   UNKNOWN
      unavailable: ['wns']

thresholds in use: {'wns': 0.0164, 'total_area': 111.3, 'power_w': 0.0014}

Six decisions, and the fourth is what makes the verdict usable rather than merely correct.

Thresholds are derived from the noise samples, not written as literals, so recalibrating is a matter of adding runs to the sample rather than editing a constant.

Direction decides what "worse" means, per Chapter 28.5 Section 5 — so area growing and WNS shrinking are both regressions despite opposite signs.

UNKNOWN dominates. One unavailable metric makes the whole verdict unknown, rather than passing on the strength of the metrics that happened to parse.

Every finding reports the factor — 4.6× and 78.8× — which is the number that tells a reviewer whether this is marginal or catastrophic. A bare "exceeded the threshold" does not distinguish a 1.1× from a 78×.

The thresholds in use are printed, so the verdict can be argued with. Chapter 24.5's pattern: a decision that explains itself.

Findings are sorted by metric name, so two runs of the gate produce comparable output.

8. What the Gate Cannot Know

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
LIMITS = [
    ("a regression smaller than the noise", "invisible by construction",
     "run more samples, or accept it"),
    ("a regression in a metric not measured", "congestion, hold timing, IR drop",
     "add the metric"),
    ("a gradual drift", "each night within noise, a month out of spec",
     "trend, not a pairwise gate"),
    ("a change that is correct but costly", "a deliberate area-for-timing trade",
     "a human decides"),
]

print(f"{'what it misses':<40} {'why':<38} what helps")
for what, why, fix in LIMITS:
    print(f"{what:<40} {why:<38} {fix}")

print()
print("misses that more samples fix :", 1)
print("misses that need a trend     :", 1)
print("misses that need a person    :", 1)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
what it misses                           why                                    what helps
a regression smaller than the noise      invisible by construction              run more samples, or accept it
a regression in a metric not measured    congestion, hold timing, IR drop       add the metric
a gradual drift                          each night within noise, a month out of spec trend, not a pairwise gate
a change that is correct but costly      a deliberate area-for-timing trade     a human decides

misses that more samples fix : 1
misses that need a trend     : 1
misses that need a person    : 1

The third row is the one a pairwise gate is structurally incapable of catching:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NIGHTLY = [-0.045, -0.050, -0.056, -0.061, -0.068, -0.074, -0.081, -0.088]
THRESHOLD = 0.0164

print("night-to-night deltas:")
fires = 0
for i in range(1, len(NIGHTLY)):
    delta = NIGHTLY[i] - NIGHTLY[i - 1]
    flagged = abs(delta) > THRESHOLD and delta < 0
    fires += flagged
    print(f"   night {i}: {delta:+.4f}  flagged: {flagged}")

total = NIGHTLY[-1] - NIGHTLY[0]
print()
print("nights flagged     :", fires)
print("total drift        :", f"{total:+.4f} ns")
print("as multiples of the threshold:", round(abs(total) / THRESHOLD, 1))
print()
print("every single step was within noise:", fires == 0)
print("the week was not                  :", abs(total) > THRESHOLD)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
night-to-night deltas:
   night 1: -0.0050  flagged: False
   night 2: -0.0060  flagged: False
   night 3: -0.0050  flagged: False
   night 4: -0.0070  flagged: False
   night 5: -0.0060  flagged: False
   night 6: -0.0070  flagged: False
   night 7: -0.0070  flagged: False

nights flagged     : 0
total drift        : -0.0430 ns
as multiples of the threshold: 2.6

every single step was within noise: True
the week was not                  : True

Seven nights, no flags, and 43 picoseconds lost.

Each step is within the noise and the week is 2.6 times the threshold. A pairwise gate comparing last night to tonight cannot see it, by construction, and the fix is not a tighter threshold — that would false-alarm at the rate Section 3 measured.

The fix is a second check against a fixed reference: last week's run, or the last known-good tag, so the accumulated drift is compared in one step. Chapter 26.7 Section 9 made the same argument about failure buckets — a night-to-night comparison answers a different question from a comparison against a baseline — and a QoR gate needs both.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import statistics

S = [-0.045, -0.038, -0.052, -0.041, -0.049, -0.036, -0.047, -0.043]
pairs = [(a, b) for i, a in enumerate(S) for j, b in enumerate(S) if i != j]

# A
print("A:", round(statistics.stdev(S), 4))

# B
print("B:", round(max(S) - min(S), 4))

# C
print("C:", round(3 * statistics.stdev(S), 4))

# D
print("D:", sum(1 for a, b in pairs if b - a < -0.005), len(pairs))

# E
print("E:", sum(1 for a, b in pairs if b - a < -0.016))

# F
d = {"tns": -0.2}
print("F:", d.get("wns") is None)

# G
print("G:", max(0.0164, abs(-0.010) * 0.10), max(0.0164, abs(-2.000) * 0.10))

# H
seq = [-0.045, -0.050, -0.056, -0.061, -0.068, -0.074, -0.081, -0.088]
print("H:", all(abs(b - a) < 0.0164 for a, b in zip(seq, seq[1:])),
      abs(seq[-1] - seq[0]) < 0.0164)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 0.0055
B: 0.016
C: 0.0164
D: 16 56
E: 0
F: True
G: 0.0164 0.2
H: True False

A — the run-to-run standard deviation.

B — and the observed spread, which is what a threshold must clear.

C — three sigma agrees with it closely on this sample.

D — a 0.005 threshold fires on sixteen of fifty-six identical-design pairs.

E — a measured threshold fires on none.

F — .get distinguishes unavailable from a value, which delta["wns"] cannot.

G — the floor binds near zero and the proportion binds far from it.

H — every step within noise and the total outside it. Section 8's drift, in one line.

10. Exercises

Exercise 1 — four sources. List the reasons your implementation flow is non-deterministic.

Exercise 2 — not zero. Say why a difference of zero is not the expected outcome.

Exercise 3 — four questions. Ask all four of your own gate and record the answers.

Exercise 4 — measure it. Run your flow eight times on an unchanged commit and report the spread.

Exercise 5 — the cost. Say why that experiment is nearly free in a flow that runs nightly.

Exercise 6 — price the thresholds. Compute the false-positive rate for four candidate thresholds.

Exercise 7 — too tight. Say what a 28% false-positive rate does to a team over a month.

Exercise 8 — too loose. Compute the range of regressions your current threshold would miss.

Exercise 9 — spread or sigma. Say what it means when the two disagree badly.

Exercise 10 — unavailable. Reproduce the KeyError and say which run produces it.

Exercise 11 — the worse fix. Explain why delta.get("wns", 0.0) is worse than crashing.

Exercise 12 — three outcomes. Implement UNKNOWN and say why it must block.

Exercise 13 — the marginal row. Say why reporting "within noise" beats silence.

Exercise 14 — one metric. Construct a run that passes a timing gate while area grows 7%.

Exercise 15 — a threshold each. Measure the noise for three of your metrics and report all three with units.

Exercise 16 — the conservative estimate. Justify max(spread, sigma) and say what it trades.

Exercise 17 — neither alone. Show an absolute threshold meaning 500% of one baseline, and a relative one below the noise floor.

Exercise 18 — the crossover. Compute where the floor stops binding for your flow.

Exercise 19 — the factor. Add it to your findings and say what it tells a reviewer.

Exercise 20 — the drift. Construct seven within-noise nights that lose more than the threshold in total.

Exercise 21 — two comparisons. Implement the fixed-reference check alongside the pairwise one, and say which question each answers.

11. Summary

  • An implementation flow is not deterministic, so two runs of one design always differ and a verdict needs to know which size of difference is ordinary.
  • The annotation's comment is right and its number is unjustified until somebody measures.
  • Measure the spread by running the same design repeatedly — Chapter 25.7's soak, pointed at a P&R flow.
  • A threshold of 0.005 fired on 28.6% of identical-run pairs, which is a false alarm roughly twice a week.
  • A threshold below the observed spread fires on identical designs, at a measurable rate.
  • The annotation's 0.050 is safe and three times larger than needed, missing every regression between 16 and 50 picoseconds.
  • Check the three-sigma estimate against the observed spread; a bad disagreement means the distribution is not what the statistics assume.
  • Re-measure after a tool upgrade or a thread-count change — a cluster of marginal regressions is usually a widened spread.
  • delta["wns"] raises on exactly the run a verdict matters for, because Chapter 28.1 refused to fabricate a WNS.
  • delta.get("wns", 0.0) is worse than crashing — it claims no change about a report nobody could read.
  • A gate needs three outcomes, and UNKNOWN blocks exactly as FAIL does.
  • One metric is not a verdict: a 7% area growth and a 13% power increase passed a timing gate.
  • Every metric needs its own measured threshold in its own unit; area's noise here is 111 µm².
  • Take max(spread, sigma) — the conservative estimate, trading a few missed marginal regressions for no false alarms.
  • Neither an absolute nor a relative threshold works alone; use a floor at the noise and a proportion above it.
  • Report the factor by which a finding exceeds its threshold, which distinguishes 1.1× from 78×.
  • A pairwise gate cannot see gradual drift — seven within-noise nights lost 2.6 thresholds' worth.
  • Run two comparisons: last night for "did this commit regress", and a fixed reference for "is the design still where it should be".

That completes Module 28. Three kinds of implementation report now become records of numbers whose provenance travels with them, two records compare only when they describe the same experiment, and a difference becomes a verdict only when it exceeds a threshold somebody measured. Module 29 turns from timing and area to coverage.

Where this fits

Part of the Python curriculum.