drc-fix · git:20260613.86cacb4 · 2026-06-13 · sha256 404cf844969259e6
drc-fix git:20260613.86cacb4A
Immutable. This exact content is served forever at /api/v1/blob/404cf844969259e6.
---
name: drc-fix
description: Diagnose and fix Design Rule Check (DRC) violations in a layout or GDS. Use when the user says "fix DRC", "DRC clean", "resolve spacing errors", "my layout fails DRC", or shares a DRC report from Calibre, Klayout, or Magic.
---
# DRC Fix
Take a DRC report and a layout, and produce a targeted fix plan — which rules are violated, where, why, and the minimal edits to clean them. Handles common rule families: spacing, width, density, via enclosure, antenna, and metal fill.
## When to use
Trigger when the user:
- Has a DRC report with non-zero violations
- Is near sign-off and needs layout clean
- Asks which violations are real vs waiver candidates
- Needs help interpreting cryptic rule names
## Inputs to gather
1. The DRC report (Calibre, KLayout, Magic, or equivalent)
2. The layout file (GDS/OAS) or at least the affected cells
3. The PDK DRC manual or rule deck name
4. Sign-off target: zero violations or rule-by-rule exceptions allowed
## Fix workflow
Steps 1-3 are a deterministic table lookup — **enforced by `programs/drc_fix_planner.py`**, not chosen by hand each run. Feed it `{rule_id: count}` and it returns the per-rule category, severity band, fix strategy, and the canonical fix order:
```bash
python3 programs/drc_fix_planner.py --counts-json counts.json --out-json plan.json --out-md plan.md
# counts.json: {"met2.SP.1": 47, "ANTENNA.5": 12, ...}
```
1. **Group by rule** — 1000 violations are usually 5 root causes — `build_plan` groups by `classify_rule(rule_id)` category.
2. **Classify severity** — hard (will fail fab) vs soft (waiverable) — `severity_for(count)` returns the `SEVERITY_BAND` label (MAJOR/SIGNIFICANT/NOTABLE/MINOR).
3. **Map rule to fix pattern** — spacing/width/density/antenna/enclosure/via/overhang/minarea → fix strategy is the `RULE_CATEGORY` → `FIX_STRATEGY` table in `drc_fix_planner.py` (do NOT re-derive it here). The fix-apply order is its `FIX_ORDER` tuple.
4. **Propose minimal edit** — smallest layout change that clears the rule (geometry judgment — see § below).
5. **Check for collateral damage** — does the fix create a new violation or break LVS?
6. **Emit fix script** — KLayout Python, SKILL, or TCL snippet when applicable.
## Output format
The Root-causes table + Fix-order + Expected-residual sections are emitted
deterministically by `drc_fix_planner.py --out-md` (its `plan_to_markdown`).
Do NOT re-type the Rule/Count/Severity/Category/Fix-strategy table by hand —
run the program and append your judgment-only sections (per-cause root-cause
narrative, collateral-damage notes, per-residual waiver rationale, the
affected-cell list to re-run DRC on) around it:
```
# DRC fix plan ← from drc_fix_planner.py --out-md
- Total violations: N
- Expected residual after plan: ~<n>
## Root causes ← table generated by the program
| # | Rule | Count | Severity | Category | Fix strategy |
| 1 | met2.SP.1 | 47 | NOTABLE | spacing | add jog or move sink to clear spacing |
| 2 | ANTENNA.5 | 12 | MINOR | antenna | add diode at gate sink OR insert jumper |
## Fix order ← from the program's FIX_ORDER
## Root-cause narrative (YOUR judgment — why each cause arises in THIS layout)
## Expected residual (YOUR judgment — per-residual waiver rationale or further fix)
## Verification
Re-run DRC on <list of affected cells>.
```
## Technical basis
Grounded in DRC-Coder and LLM-assisted layout repair research. Key insight: DRC reports are structured logs, so the rule-family → fix-strategy mapping is a fixed table — owned by `programs/drc_fix_planner.py` (deterministic), not re-derived by the LLM. The LLM's residual job is the geometry judgment the table cannot supply: the *minimal* edit for THIS layout, collateral-damage / LVS impact, and per-residual waiver rationale.
## Do not
- Do not propose fixes that break LVS (especially for antenna diodes — maintain connectivity)
- Do not waive hard rules without explicit user approval
- Do not touch cells outside the block boundary without flagging it
## Canonical loop infrastructure (mandatory — shared with all *-fix loops)
When the DRC fix workflow iterates (re-run DRC → residual still > 0 → adjust
spacing/jog/fill → re-run), that loop MUST be driven by the two shared
closed-loop primitives so every fix loop in Vibe-IC obeys one
convergence / plateau / regression policy and one runaway / dedup guard —
do **not** hand-roll a bespoke retry counter or duplicate-fix check.
**1. `programs/iterative_search.py` — the parameter sweep.**
Model the per-rule fix knobs as a typed `SearchSpace`; `IterativeSearch`
proposes the next layout-edit trial and `ConvergenceChecker` classifies the
residual-violation history (`CONVERGED` / `PLATEAU` / `REGRESSION` /
`EXHAUSTED` / `CONTINUE`):
```python
import iterative_search as it
space = it.SearchSpace([
it.Dimension("jog_tracks", "integer", lo=0, hi=8), # extra routing tracks
it.Dimension("spacing_nm", "continuous", lo=0.0, hi=400.0),
it.Dimension("fill_density", "continuous", lo=0.0, hi=1.0),
it.Dimension("strategy", "enumerate", choices=["jog", "widen", "fill", "diode"]),
])
# target=0 residual violations; minimize the count
checker = it.ConvergenceChecker(target=0.0, tolerance=0.0, patience=4)
search = it.IterativeSearch(space, checker, maximize=False, seed=7, max_rounds=20)
def evaluate(point): # caller runs KLayout/Magic DRC here
return residual_violation_count # lower is better
outcome = search.run(evaluate) # outcome.status / best_point / rounds
```
`IterativeSearch` constructs an `AdmissionGuard(bounds=space.bounds(),
max_iterations=max_rounds)` internally, so each proposed edit is already
runaway- and dedup-guarded when you use `search.propose()` / `search.run()`.
**2. `programs/loop_admission_guard.py` — admit each iteration BEFORE the DRC run.**
A DRC re-run is expensive; gate every proposed edit through
`AdmissionGuard.admit()` first:
```python
import loop_admission_guard as g
guard = g.AdmissionGuard(
bounds={"spacing_nm": (0.0, 400.0), "fill_density": (0.0, 1.0)},
caps={"jog_tracks": 8}, # REJECT a runaway jog count
max_iterations=20) # hard RUNAWAY iteration budget
res = guard.admit({"jog_tracks": 1, "spacing_nm": 90.0, "strategy": "jog"})
if res.admitted:
rerun_drc(res.proposal) # res.proposal is post-clamp / safe
# else res.reason in {DUPLICATE, RUNAWAY_CAP, RUNAWAY_ITERATION_BUDGET}
```
CLI one-shot decision (exit 0 = ADMITTED, 1 = REJECTED):
```bash
python3 programs/loop_admission_guard.py decision.json
# decision.json: {"bounds":{...},"caps":{...},"max_iterations":20,
# "history":[...prior edits...],"proposal":{...}}
```
`canonical_fingerprint(proposal)` is the dedup key — re-proposing a fix
combination already tried this session is rejected with `reason="DUPLICATE"`
instead of wasting a KLayout/Magic DRC pass. This is ADDITIVE: it enforces a
budget + plateau/regression exit around the existing "Fix order → Expected
residual → Verification" steps without changing any of them.
## ⛔ ECO spare-cell preservation (mandatory)
> ⛔ **ECO spare-cell preservation:** cells/gates/pads carrying the `dont_touch` /
> `keep` attribute (or otherwise tagged spare/ECO) are RESERVED for a future
> metal-only ECO. NEVER delete, resize, re-purpose, or optimize them away while
> clearing DRC. In particular: a density/metal-fill fix must stay **ECO-aware**
> — do NOT delete spare cells/pads to clear spacing, and do NOT lock metal fill
> over the tracks above spares/reserved pads (use slottable/removable fill there
> so a future metal-only ECO can still route to them). No `opt_clean` /
> `clean -purge` / `remove_buffers` on keep-marked instances to "clean up"
> geometry. After your DRC fix, `spare_cell_preservation_check.py` MUST still
> PASS (spare set + keep attrs intact, 0 removed); a dropped spare is a
> regression — restore it and re-run the checker. See the `design-for-eco` skill.
## Detailed-route ABORT triage (TritonRoute DRT-0305 / DRT-0085 + the silent-unrouted trap)
A DRC report assumes the design routed. A more dangerous class is when
`detailed_route` itself **aborts** and the runner swallows it — producing a GDS,
a "clean" DRC, and a PASS on a design with **zero signal-routed nets**. These
patterns (captured v0.2.14; now AUTOMATED by `phase3_one_shot_runner.py`, but the
*judgment* is here for any non-sky130 PDK or fresh triage):
- **`[ERROR DRT-0305] Net <n> of signal type GROUND/POWER is not routable …
Move to special nets.`** A non-special POWER/GROUND-typed net sitting in the
regular `NETS` section — typically a dangling `zero_`/`one_` constant-tie stub
left by Yosys `setundef`/`hilomap` — makes TritonRoute abort **all** detailed
routing. Triage **structurally** (never by net-name literal): delete the net if
it is dangling (0 iterm/0 bterm — no electrical role), else reclassify it to
`SIGNAL` so it routes. Real PG nets are `SPECIAL` (in `SPECIALNETS`) and are
left alone. Automated by `_pg_net_cleanup_tcl` before `global_route`.
- **`[ERROR DRT-0085] Valid access pattern combination not found for <inst>`**
on a probe / `lpflow_*` / DRC-failed cell. Provenance heuristic: if that cell
master appears **only in the PnR netlist** (`grep` the original RTL and the
post-synth netlist — 0 there, N>0 in `*_pnr.v`), then an **optimizer step
inserted it** (e.g. `repair_design` picked a probe cell as a slew-fix buffer,
named `load_slew*`), not the designer. Fix: restrict the resizer/CTS/repair
cell pool with `set_dont_use` fed from the **PDK's OWN** `drc_exclude.cells`
(the `PNR_EXCLUDED_CELL_FILE` the reference flow uses). **Read the PDK file —
do NOT hand-curate a list**: a hand list easily over-reaches (globbing
`clkbuf_*` would wrongly kill the CTS clock buffers; `buf_16`/`mux4_4` are
PDK-marked DRC-failed and legitimately excluded). Apply it after `link_design`,
before any opt. Automated by `_dont_use_tcl` + `PdkConfig.pnr_exclude_cell_file`.
- **The silent-unrouted trap (doctrine, applies to ANY best-effort route step).**
A `catch {detailed_route}` that only logs a NONFATAL warning will let a
fully-unrouted design flow downstream and pass. **Always pair a NONFATAL route
guard with a routing-completeness check**: a genuinely routed DEF has `+ ROUTED`
geometry on its signal `NETS` (not just on `SPECIALNETS` PDN), and the session
log carries no detailed-route abort marker (`DETAILED_ROUTE_NONFATAL`,
`[ERROR DRT-0305/0085]`, `[ERROR ANT-0008]`). On incompleteness report **FAIL**,
never a clean pass — a sign-off check that can only flip PASS→FAIL is honest;
one that can inflate FAIL→PASS is the silicon-DOA hazard. Automated as the
`routing_incomplete` flag in `_emit_antenna_report`.
- **Antenna repair is a routing operation, not a post-hoc edit.** OpenROAD
`repair_antennas <diode_cell>` (plural; the diode is a **positional** arg, not a
`-diode_cell` flag) fixes antennas chiefly by **jumper insertion** (layer
hopping), which needs a **fresh global-route graph** — run it as
`global_route → repair_antennas → detailed_route`; placed *after* the main
detailed_route it degrades to diode-only (~no effect). And `check_antennas`
cannot read routing from a re-`read_def` (`[ERROR ANT-0008]`): a separate
measurement pass that re-`global_route`s **discards the jumpers** and mis-reports
the repaired design — so the antenna result must be captured **in-session**, on
the realized routing. Automated by `_antenna_repair_tcl` + the in-session read in
`_emit_antenna_report`. **Cost note:** the repair's `detailed_route` is a full
second route pass — gate it behind a cheap read-only `check_antennas` on the main
route first (it reads detailed routing directly, no `global_route` needed) and
skip the repair entirely when the design is already antenna-clean (0 net
violations ⇒ 0 pin violations); the skip path must run no `global_route` so it
cannot disturb the realized route.
## Compliance gate (mandatory)
After producing your output, save it to a file and run:
```bash
python3 plugins/vibe-ic/_shared/skill_compliance_check.py \
--requirements plugins/vibe-ic/skills/drc-fix/compliance.yaml \
<your_output_file>
```
Exit 0 = PASS, exit 1 = FAIL with specific missing elements listed.
`compliance.yaml` in the corresponding skill directory enumerates
every required element of your output: section headers, metadata fields,
handoff lines, tool invocations.
**Your task is not complete until the audit returns PASS.** Missing
elements are the single largest source of skill-execution non-determinism
across different agents.