The risk of an ECO is how much it can break, and therefore how much must be re-verified. Putting the types in a ladder lets you pick the lowest rung that closes the problem and plan the verification up front.
Rung | Change | Must rerun |
|---|---|---|
1 | Footprint-compatible cell swap | Timing and SI on affected nets, power |
2 | Resize with placement legalisation | Rung 1 plus design rules and parasitic re-extraction (legalisation moves cells) |
3 | Buffer insertion | Rung 2 plus connectivity checks (new instances and new routes) |
4 | Rerouting or layer change | Rung 3 plus full SI and routing checks |
5 | Logic restructuring | Rung 4 plus equivalence checking |
6 | Clock tree or skew change | Rung 4 plus clock checks and full timing across all corners (equivalence only if logic also changed) |
ladder = [
("cell swap", {"timing", "si_local", "power"}),
("resize", {"timing", "si_local", "power", "drc", "extraction"}),
("buffer insertion", {"timing", "si_local", "power", "drc", "extraction", "lvs"}),
("reroute or layer", {"timing", "si_local", "power", "drc", "extraction", "lvs", "si_full"}),
("logic restructuring", {"timing", "si_local", "power", "drc", "extraction", "lvs", "si_full", "equivalence"}),
]
reroute_checks = dict(ladder)["reroute or layer"]
clock_checks = reroute_checks | {"clock_corners"}
sizes = [len(s) for _, s in ladder] + [len(clock_checks)]
assert sizes == [3, 5, 6, 7, 8, 8]
assert all(ladder[i][1] <= ladder[i + 1][1] for i in range(len(ladder) - 1)) # each rung includes all checks below it
assert reroute_checks <= clock_checks # a clock change builds on rerouting
assert "equivalence" not in clock_checks # no equivalence unless logic changed
extra = {ladder[i + 1][0]: sorted(ladder[i + 1][1] - ladder[i][1]) for i in range(len(ladder) - 1)}
assert extra["resize"] == ["drc", "extraction"] and extra["buffer insertion"] == ["lvs"]
assert extra["logic restructuring"] == ["equivalence"] and extra["reroute or layer"] == ["si_full"]
assert sorted(clock_checks - reroute_checks) == ["clock_corners"]
print("PASS: checks per rung", dict(zip([n for n, _ in ladder] + ["clock or skew"], sizes)))Each rung's check set contains the previous one's, growing from 3 checks for a swap to 8 for a logic change. Buffer insertion creates a new instance and new routes, so connectivity checks and extraction apply from that rung; resizing with legalisation moves cells and needs re-extraction a rung earlier. A clock or skew change builds on rerouting with clock checks and full timing across all corners, not equivalence, unless logic changed too. That is why the first question on any ECO is whether a lower rung will do. The table lists categories only; each flow has its own detailed list.
Each ECO states its rung and has the matching verification plan before it starts.
Skipping a check for a rung needs an explicit reason signed by the owner.
ECOs that cross several rungs are split where possible so the cheap ones are verified early.
Full SI and routing checks: connectivity checks and re-extraction already apply from buffer insertion, so rerouting adds the full-SI scope, because routes change and coupling moves.