---
title: "Design rule checking"
description: "Configure DRC rules in rosette.toml and catch foundry violations before tapeout."
canonical_url: "https://www.rosette.dev/docs/guides/design-rule-checking"
markdown_url: "https://www.rosette.dev/docs/guides/design-rule-checking.md"
source_url: "https://github.com/PreFab-Photonics/rosette/blob/f086d7670645fd36c05362d696d442a2b2e74850/www/content/docs/guides/design-rule-checking.mdx"
docs_channel: "main"
docs_revision: "f086d7670645fd36c05362d696d442a2b2e74850"
---

# Design rule checking

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](/docs/guides/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`](/docs/api-reference#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.

```toml
# 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

| Key                    | Meaning                                                                                 |
| ---------------------- | --------------------------------------------------------------------------------------- |
| `min_width`            | Minimum feature width, in um.                                                           |
| `max_width`            | Maximum feature width.                                                                  |
| `min_spacing`          | Minimum same-layer spacing between polygons.                                            |
| `min_area`             | Minimum polygon area, in um^2.                                                          |
| `min_edge_length`      | Shortest allowed polygon edge.                                                          |
| `angles`               | Allowed edge angles in degrees (e.g. `[0, 45, 90, 135]`).                               |
| `acute_angle`          | Minimum allowed convex interior angle in degrees.                                       |
| `snap_to_grid`         | Manufacturing grid pitch in design units (for example, `0.001` for 1 nm).               |
| `no_overlap`           | Forbid overlapping polygons on the same layer.                                          |
| `no_self_intersection` | Forbid self-intersecting polygons.                                                      |
| `density`              | Grouped 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.

| Type              | Fields                            | Meaning                                                            |
| ----------------- | --------------------------------- | ------------------------------------------------------------------ |
| `spacing`         | `layer1`, `layer2`, `min_spacing` | Minimum inter-layer spacing.                                       |
| `enclosure`       | `inner`, `outer`, `min_enclosure` | Inner layer must be enclosed by outer layer.                       |
| `require_overlap` | `layer1`, `layer2`                | Two layers must overlap.                                           |
| `forbid_overlap`  | `layer1`, `layer2`                | Two layers must not overlap.                                       |
| `not_inside`      | `inner`, `outer`                  | Inner 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.

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

See [`DrcRules.warning_margin`](/docs/api-reference/DrcRules) for details.

## Run DRC from Python

```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`](/docs/api-reference#run_drc) returns a
[`DrcResult`](/docs/api-reference/DrcResult). Check `result.passed` for
a quick pass/fail, and iterate `result.violations` for details. Each
[`DrcViolation`](/docs/api-reference/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`](/docs/api-reference/Library) via `library=` so DRC can resolve all
referenced cells.

## Explicit skips and waivers

Use a [`DrcPolicy`](/docs/api-reference/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`.

```python
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:

```bash
# 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](/docs/guides/agent-workflows#machine-readable-output).



> **Note: --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

* [`DrcRules`](/docs/api-reference/DrcRules): construct and inspect rule
  sets programmatically
* [`DrcPolicy`](/docs/api-reference/DrcPolicy): explicit per-run cell skips
  and local region waivers
* [`DrcResult`](/docs/api-reference/DrcResult): result aggregate with
  statistics and `passed` flag
* [`DrcViolation`](/docs/api-reference/DrcViolation): individual
  violations
* [`load_drc_rules`](/docs/api-reference#load_drc_rules) / [`run_drc`](/docs/api-reference#run_drc): the main entry points
* [Core concepts](/docs/getting-started/core-concepts): layer, cell, and
  port fundamentals