Design rule checking

Configure DRC rules in rosette.toml and catch foundry violations before tapeout.

Design rule checking (DRC) catches geometry that will not fabricate correctly: features that are too narrow, polygons that are too close together, illegal edge angles, overlapping shapes on a single-patterning layer, and so on. Every foundry publishes a DRC deck you must pass before taping out. Rosette lets you encode a subset of those rules in rosette.toml and check them from Python or the CLI.

See Project configuration for the complete rosette.toml schema, including layers, DFM, design checks, and snapshots.

When to run DRC

  • While you design, to catch mistakes early: run uv run rosette build designs/my_design.py --check or run_drc in your script.
  • Before tapeout, to enforce the full deck: run uv run rosette drc and gate your build on a clean result. Foundry tools will still be the final source of truth, but Rosette catches most of the cheap mistakes.

Configure rules in rosette.toml

DRC lives under the [drc] table. Two shapes of rule:

  • Per-layer rules live in [drc.layers.<name>], one table per layer, with one key per rule.
  • Inter-layer rules live in [[drc.rules]], an array of tables, each with a type and rule-specific fields.

Layer keys accept either a semantic name from [layers] (recommended) or the traditional "number/datatype" format. For example, [drc.layers.silicon] and [drc.layers."1/0"] are equivalent.

# rosette.toml

[layers.silicon]
number = 1
datatype = 0

[layers.p_doping]
number = 20
datatype = 0

[layers.n_doping]
number = 21
datatype = 0

# Per-layer rules on the silicon waveguide layer.
[drc.layers.silicon]
min_width = 0.12           # minimum feature width (um)
min_spacing = 0.13         # minimum same-layer spacing (um)
min_area = 0.01            # minimum polygon area (um^2)
angles = [0, 90]           # allowed edge angles (degrees)
no_overlap = true          # forbid overlapping polygons on same layer
no_self_intersection = true

# Inter-layer rule: keep P+ and N+ apart.
[[drc.rules]]
type = "spacing"
layer1 = "p_doping"
layer2 = "n_doping"
min_spacing = 0.50
name = "PN_SPC"

[[drc.rules]]
type = "forbid_overlap"
layer1 = "p_doping"
layer2 = "n_doping"
name = "PN_NOOVLP"

Supported per-layer rules

KeyMeaning
min_widthMinimum feature width, in um.
max_widthMaximum feature width.
min_spacingMinimum same-layer spacing between polygons.
min_areaMinimum polygon area, in um^2.
min_edge_lengthShortest allowed polygon edge.
anglesAllowed edge angles in degrees (e.g. [0, 45, 90, 135]).
acute_angleMinimum allowed convex interior angle in degrees.
snap_to_gridManufacturing grid pitch in design units (for example, 0.001 for 1 nm).
no_overlapForbid overlapping polygons on the same layer.
no_self_intersectionForbid self-intersecting polygons.
densityGrouped subtable defining density bounds, window size, step, and optional region layer.

Supported inter-layer rules

Each [[drc.rules]] entry has a type and either a (layer1, layer2) pair or an (inner, outer) pair. All accept an optional name used in violation messages.

TypeFieldsMeaning
spacinglayer1, layer2, min_spacingMinimum inter-layer spacing.
enclosureinner, outer, min_enclosureInner layer must be enclosed by outer layer.
require_overlaplayer1, layer2Two layers must overlap.
forbid_overlaplayer1, layer2Two layers must not overlap.
not_insideinner, outerInner layer must not sit fully inside outer layer (keep-out zone).

Near-threshold warnings

The top-level [drc] table accepts warning_margin in design units, typically microns. Length-based numeric violations within that absolute margin are downgraded to warnings instead of errors. This is useful while you iterate on a design and don't want to treat a 0.115 um wire (spec: 0.12) as a tapeout-blocking error.

[drc]
warning_margin = 0.01   # within 0.01 um of a length threshold -> warning

See DrcRules.warning_margin for details.

Run DRC from Python

from rosette import Cell, Layer, Point, Polygon
from rosette.drc import load_drc_rules, run_drc

cell = Cell("top")
cell.add_polygon(Polygon.rect(Point(0, 0), 10, 0.08), Layer(1, 0))  # too narrow

rules = load_drc_rules()             # reads rosette.toml
result = run_drc(cell, rules)

if result.passed:
    print(
        f"DRC clean ({result.polygons_checked} polygons, "
        f"{result.rules_checked} rules)"
    )
else:
    print(f"{result.error_count} error(s), {result.warning_count} warning(s):")
    for v in result.violations:
        (x0, y0), (x1, y1) = v.bbox
        print(
            f"  [{v.severity}] {v.rule_name or v.rule_type}: {v.message} "
            f"@ ({x0:.3f}, {y0:.3f})-({x1:.3f}, {y1:.3f})"
        )

run_drc returns a DrcResult. Check result.passed for a quick pass/fail, and iterate result.violations for details. Each DrcViolation carries rule_name, rule_type, severity, a human-readable message, and a bbox (((x0, y0), (x1, y1)) in um) pointing at the offending geometry so you can render it in the viewer or jump to it by coordinate.

Cells assembled with public Instance placements track their children, so run_drc can auto-collect the hierarchy. For a cell obtained from an imported or explicitly managed hierarchy, pass its Library via library= so DRC can resolve all referenced cells.

Explicit skips and waivers

Use a DrcPolicy when a run should trust an entire cell subtree or waive an intentional violation in a bounded region. Policy is supplied explicitly to run_drc; it is not stored on Cell.

from rosette import BBox, Point
from rosette.drc import DrcPolicy, run_drc

policy = DrcPolicy()
policy.skip_cell("trusted_pdk_cell")
policy.waive_region("taper", BBox(Point(9, -1), Point(11, 1)))

result = run_drc(cell, rules, policy=policy)

skip_cell(name) suppresses a violation only when every named source cell is inside that skipped subtree. Violations without cell provenance, including density-window violations, remain visible. waive_region(name, bbox) uses the named cell's local coordinates and requires full containment; Rosette transforms the region for each placement before filtering.

result.suppressed_violations, result.skipped_cells, and result.waived_violations report effects of the explicit policy for that run. If both a cell skip and a waiver match, the violation counts as suppressed by the cell skip rather than being counted twice.

Run DRC from the CLI

Three CLI entry points hit DRC:

# Just DRC, against the cell defined in the script.
uv run rosette drc designs/my_design.py

# All enabled design checks (DRC, connectivity, bend radius, ...).
uv run rosette check designs/my_design.py

# Build to GDS with a DRC pre-check. The build always proceeds; DRC
# results are printed to stderr.
uv run rosette build designs/my_design.py --check

Both uv run rosette drc and uv run rosette check accept --json to emit a single machine-readable object on stdout instead of prose — see the agent workflows guide.

--check does not gate the build

uv run rosette build --check runs DRC and reports violations, but it still writes the GDS file. If you want a hard gate for CI, run uv run rosette drc and check the exit code, or call run_drc in your script and raise on result.passed == False.

See also

On this page