Between yesterday's good run and today's bad one dozens of things changed: constraints, libraries, netlist, parasitics, scripts. Testing them one at a time costs n runs. Bisecting the ordered list costs about log2 n runs, which for 32 changes is five.
Step | Action |
|---|---|
1 | List the changes between the good and bad runs in the order they were applied |
2 | Apply the first half to the good run and re-run |
3 | If it fails, the culprit is in the first half; otherwise in the second; repeat on that half |
4 | When one change remains, confirm by reverting only it from the bad run |
changes = [f"change_{i:02d}" for i in range(32)]
culprit = "change_21"
runs = 0
def fails_with(applied):
global runs
runs += 1
return culprit in applied # stands for a full re-run that either shows the failure or not
lo, hi = 0, len(changes) # culprit is in changes[lo:hi]
while hi - lo > 1:
mid = (lo + hi) // 2
if fails_with(changes[:mid]): # applying only the first mid changes already fails
hi = mid
else:
lo = mid
assert changes[lo] == culprit
assert runs == 5
print("PASS: found", changes[lo], "in", runs, "runs out of", len(changes), "changes")The loop finds change_21 among 32 changes in five re-runs. The toy 'run' is a membership test; in practice each run is a complete timing run, so five runs instead of up to 32 is a large saving. The method assumes a single culprit; when two changes interact, bisection finds the first one that is sufficient to fail, and the second shows up after reverting it.
Runs are reproducible: the same inputs give the same numbers, otherwise bisection chases noise.
The change list is complete and ordered, including tool and script versions.
The final confirmation reverts the suspected change on the bad run and shows the failure disappears.
About 7 (log2 100 is between 6 and 7), compared with up to 100 for one-at-a-time testing.