Efficient DRC
Why design rule checks struggle with curved layouts, and how Laylight checks them on the semantic geometry inside the compiler, pointing at the line of code, in a fraction of KLayout's time.
Design rule checking (DRC) verifies that a layout can be manufactured: features are wide enough, far enough apart, enclosed by the right margin, bent no tighter than the process allows. On Manhattan layouts this is routine. Photonic circuits are different: rings, bends, Euler curves, S-bends, tapers and grating arcs are everywhere, and that is where traditional DRC breaks down. Laylight runs its checks inside the compiler, on the geometry as it was written rather than as it was flattened, and that changes three things: what the checker knows, where it reports, and how fast it is.
Curves have no meaning in GDS
GDSII (and OASIS) store polygons and paths, nothing else. Every arc, bend or spline is written as a polygon whose vertices are snapped to the database grid. With a 1 nm grid and a chord error of at most 1 nm, a single ring of radius 10 µm already has about 450 vertices, and a photonic chip with thousands of rings, bends and routed waveguides carries millions of edges that are only a few tens of nanometers long. The DRC engine never sees an arc of radius 10 µm and width 500 nm; it sees those edges and has to rediscover the geometry from them.
This causes three kinds of trouble:
- False errors. A chord lies inside its arc, so along a bend the measured width and gap oscillate around the drawn value by up to the chord error. A waveguide drawn exactly at the minimum width, or a coupling gap drawn exactly at the minimum spacing, is reported as a violation at some vertices and not at others. Snapping vertices to the grid adds one-unit jogs and edges a hair off their angle, which come back as notches, acute angles or off-grid edges.
- Missed errors. The same oscillation works the other way: a gap that is slightly too small can measure as legal between two vertices. And a rule about the curve itself, such as a minimum bend radius, cannot be checked on short straight edges at all without reconstructing the curve.
- Slow runs. Width and spacing checks compare pairs of edges. Millions of tiny edges make curved layouts the slowest part of verification, and much of the result is noise.
In practice designers waive curved-geometry errors by the hundred, and every waiver is a place where a real violation can hide. The benchmark below measures exactly this: on rings, couplers, bends and rounded corners, between 39% and 99% of a polygon checker's reports are false positives.
Checks on semantic geometry
A rule deck is part of the program (see Design Rules):
define drc soi {
width si >= 400nm
space si >= 200nm
space si, metal >= 1um # exclusion between layers
enclosure si in slab >= 300nm # the margin of an inclusion
inside si in slab # the inclusion itself: no si outside slab
area si >= 200nm * 200nm
radius si >= 5um # the bend radius of every curved primitive
}In Laylight, curved geometry is not a list of points: it is a primitive with parameters. An arc knows its radius, width and angle; a ring its centerline radius and width; an euler bend its curvature law; an sbend its profile. The compiler discretizes them only when it writes the layout, with a chord error it controls (at most one database unit by default), so it always knows both the intended geometry and how far each polygon edge may deviate from it. When it draws a ring with 10um, 500nm, it keeps what the polygon loses: the width is 500 nm, the radius of curvature is 10 µm, and every edge of the polygon lies within 1 nm of the circle it approximates. The checker uses all three:
- An isolated shape passes or fails a
widthrule by its exact width. No vertex of a ring drawn at the minimum width measures a hair too narrow, because no vertex is measured. - The
radiusrule compares the tightest radius of an Euler bend, S-bend, Bézier or arc, computed on the curve. A polygon-based checker cannot express this rule at all. - Where edges do have to be compared, the edges of discretized curves carry their chord tolerance, so the gap between a ring and its bus drawn exactly at the minimum spacing is not an error, and a gap 50 nm too small still is.
On straight lines the checks are the classic edge-pair checks and give exactly KLayout's results, pair for pair. The hierarchy is kept too: every cell is checked once, however many times it is placed, and two instances are only compared where they come within a rule's limit of each other.
The compiler applies such checks to every curve it generates even without a deck. An Euler bend whose tightest radius would fold its inner edge over is rejected with the exact radius, computed on the continuous curve:
error: invalid euler: the tightest radius of this Euler bend is 706.09nm, which must exceed half the width 1.5um
--> bend.lyl:7:9
|
7 | shape euler with 1um, 3um, 90deg as e
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^The same holds for S-bends and Bézier curves (curvature checked along the whole curve), arcs, rings and racetracks (width against radius), paths with rounded corners (the bend radius has to fit between the points) and the min_radius of a cross-section. Connectivity is checked on the same model: laylight check reports dangling ports and connected ports whose type or width disagree, and laylight lvs compares a cell with a reference netlist.
Errors at the line of code
A violation is a compiler diagnostic. It names the rule, the measured value and the position, and points at the statement that drew the shape; between instances it also points at the place statements:
error: drc soi.space_si: space 150nm < 200nm on si at (0.072um, 0.325um) in Resonator
--> resonator.lyl:9:45
|
9 | shape ring with 10um, 500nm as ring at (0, 10.65um)
| ^^^^
note: the other shape, `bus` is drawn here
--> resonator.lyl:8:40
|
8 | shape rect with 30um, 500nm as bus at (0, 0)
| ^^^laylight check runs the decks with everything else, laylight drc runs them alone and writes a JSON report, and the language server shows the violations in the editor as you type. There is no round trip through GDS, a separate rule deck and marker files that have to be mapped back to the code by hand.
Writing the deck
The same seven rules, written for each tool (bench/drc/decks), counted as the syntax benchmark counts: lines that hold code and tokens, without comments, blank lines and lines that only close a bracket.
define drc soi {
width si >= 400nm
space si >= 200nm
space si, metal >= 1um
enclosure si in slab >= 300nm
inside si in slab
area si >= 200nm * 200nm
radius si >= 5um
}# KLayout DRC script (.lydrc)
source($input)
report("SOI rules", $report)
si = input(1, 0)
slab = input(2, 0)
metal = input(10, 0)
si.width(0.4.um, euclidian).output("width", "si width < 400nm")
si.space(0.2.um, euclidian).output("space", "si space < 200nm")
si.separation(metal, 1.0.um, euclidian).output("space si metal", "si to metal < 1um")
slab.enclosing(si, 0.3.um, projection).output("enclosure", "slab encloses si by < 300nm")
(si - slab).output("inside", "si outside slab")
si.with_area(nil, 0.04).output("area", "si area < 0.04um^2")| Deck | Rules | Lines | Tokens | |
|---|---|---|---|---|
| Laylight (deck only) | 7 of 7 | 8 | 39 | the design's layer table is not repeated |
| Laylight (with its layer table) | 7 of 7 | 12 | 64 | |
| KLayout DRC script | 6 of 7 | 11 | 125 | no bend-radius rule |
| gdsfactory (Python) | 6 of 7 | 22 | 317 | no bend-radius rule |
| gdstk (Python) | 6 of 7 | 26 | 314 | no bend-radius rule; rules as morphology |
A Laylight rule is a line: the kind, the layers, the limit. Nothing names the input, the output or the report, because the deck is part of the program and its violations are the compiler's diagnostics; nothing restates the layers, because the deck sees the design's layer table; and nothing chooses a metric per rule, because the defaults are the common ones. The KLayout script is the most compact of the others and still carries three times the tokens for one rule fewer: every rule names its output category and message, the layers are declared again by GDS number, and the metric is spelled out. The gdsfactory script drives the same engine from Python, with report-database plumbing around every check; the gdstk script has to express each rule as the morphology that isolates its violations, where a width check becomes an erosion, a dilation and a difference. None of the three can write the bend-radius rule: a polygon file no longer knows which of its edges were a curve.
Measured against KLayout, gdsfactory and gdstk
The benchmark in bench/drc plants violations into generated layouts, so that every tool can be scored on more than its speed:
- Speed: the time of the checks (Laylight: after compilation, as the report measures it; the others: after reading the GDS, including building their regions), and the throughput in vertices of the checked layers per second.
- Scaling: each case runs at five sizes; the slope of the time over the size on logarithmic axes is the exponent of the algorithm. Linear scaling is
n^1.00. - Missed violations: planted violations that a tool does not report, as a fraction of all planted ones. The goal is zero.
- False positives: reports that lie outside every planted violation, as a fraction of all reports. The lower the better; a tool that reports a correct layout as broken scores badly here.
The opponents: KLayout 0.30's Region checks on the GDS, in flat mode and in deep (hierarchical) mode; gdsfactory 9.51, which has no checker of its own and exposes KLayout's through Component.get_region; and gdstk 1.0, which has no checker either, with the rules written as the morphological operations a geometry library offers (a width check is A − open(A, d/2), a spacing check close(A, d/2) − A, an enclosure A − erode(B, d)). gdstk and KLayout cannot express the radius rule. All tools run single-threaded; times are from an Apple M3 Max.
Straight lines: the same answers, faster
| Tool | Largest (2M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 2.2 s | n^1.00 | 0% | 0% |
| KLayout (flat) | 4.8 s | n^1.03 | 0% | 0% |
| KLayout (deep) | 4.4 s | n^1.03 | 0% | 0% |
| gdsfactory | 4.7 s | n^1.03 | 0% | 0% |
| gdstk | 150 s at 720k, then stopped | n^1.34 | 0% | 18% |
The Manhattan case is a field of rectangles with slab rings around them, a width, a spacing and an enclosure rule, and violations of each planted in known places. Every edge-pair tool finds exactly the planted violations; Laylight is the fastest and scales linearly. gdstk's morphology finds them too, but the offsets it needs grow faster than linearly with the layout.
| Tool | Largest (2M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 350 ms | n^1.02 | 0% | 0% |
| KLayout (flat) | 840 ms | n^1.04 | 0% | 0% |
| KLayout (deep) | 640 ms | n^1.03 | 0% | 0% |
| gdsfactory | 790 ms | n^1.04 | 0% | 0% |
| gdstk | 22 s | n^1.16 | 0% | 0% |
Exclusion keeps a metal pad at least 1 µm from a waveguide (space si, metal), with pads planted too close.
| Tool | Largest (1.8M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 720 ms | n^0.94 | 0% | 0% |
| KLayout (flat) | 1.5 s | n^1.02 | 0% | 0% |
| KLayout (deep) | 1.6 s | n^1.01 | 0% | 0% |
| gdsfactory | 1.5 s | n^1.03 | 0% | 0% |
| gdstk | 9.5 s | n^1.41 | 0% | 40% |
Inclusion asks that a waveguide lies inside its slab, completely (inside) and with a margin (enclosure), with slabs planted too tight, shifted so the waveguide sticks out, or missing.
| Tool | Largest (2M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 370 ms | n^1.01 | 0% | 0% |
| KLayout (flat) | 1.4 s | n^1.03 | 0% | 31% |
| KLayout (deep) | 1.2 s | n^1.01 | 0% | 31% |
| gdsfactory | 1.4 s | n^1.02 | 0% | 31% |
| gdstk | 170 s at 720k, then stopped | n^1.51 | 0% | 31% |
Feature size checks the minimum width of rotated bars, tapers, crosses and necked polygons: shapes whose narrowest part is not a pair of axis-aligned edges. Every tool finds all the planted features. The false positives of the polygon checkers are zero-width "tips": the corners of rotated bars, which rounding to the grid makes a fraction of a degree sharper than a right angle, and the 85° corners at the wide end of every taper. Laylight knows the width of a taper and a bar from their parameters and allows a tenth of a degree before it calls a corner acute.
Curves: only the real violations
| Tool | Largest (4.5M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 1.3 s | n^1.01 | 0% | 0% |
| KLayout (flat) | 19 s | n^1.00 | 0% | 39% |
| KLayout (deep) | 19 s | n^1.01 | 0% | 39% |
| gdsfactory | 19 s | n^1.01 | 0% | 39% |
| gdstk | 130 s at 720k, then stopped | n^1.89 | 0% | 97% |
Rings: half of them drawn exactly at the minimum width, half 50 nm below it. KLayout reports every ring, because the chords of a ring at the minimum width measure up to a nanometre too narrow; only the rings below the minimum are planted, so about half of its reports are false positives, and it needs a thousand edge pairs to say so per ring. Laylight reads the width of each isolated ring and spends its time on the spacing check alone.
| Tool | Largest (4.6M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 1.3 s | n^1.03 | 0% | 0% |
| KLayout (flat) | 20 s | n^1.00 | 0% | 99% |
| KLayout (deep) | 18 s | n^0.99 | 0% | 99% |
| gdsfactory | 19 s | n^1.00 | 0% | 99% |
| gdstk | 100 s at 730k, then stopped | n^1.74 | 0% | 99% |
Couplers: a ring over a bus with the gap at, below and above the minimum spacing. The gaps drawn exactly at the minimum are the hard case for a polygon checker: nearly all of KLayout's reports are those, and the planted gaps hide among them.
| Tool | Largest (4.3M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 770 ms | n^1.02 | 0% | 0% |
| KLayout (flat) | 14 s | n^1.00 | 0% | 55% |
| KLayout (deep) | 13 s | n^1.00 | 0% | 55% |
| gdsfactory | 14 s | n^1.00 | 0% | 55% |
| gdstk | 120 s at 2.1M, then stopped | n^1.61 | 0% | 96% |
Bends: Euler bends, arcs and S-bends at and below the minimum width, with a bend-radius rule that only Laylight can run; the arcs that are too tight are found from their parameters.
| Tool | Largest (2.3M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 730 ms | n^1.02 | 0% | 0% |
| KLayout (flat) | 14 s | n^1.00 | 0% | 50% |
| KLayout (deep) | 13 s | n^1.00 | 0% | 50% |
| gdsfactory | 13 s | n^1.01 | 0% | 50% |
| gdstk | 74 s at 830k, then stopped | n^1.40 | 0% | 50% |
Rounded corners: rectangles with 200 nm corner radii, at and below the minimum width. The rounded corners alone make a polygon checker report every rectangle.
Hierarchy: every cell once
| Tool | Largest (6.3M vertices) | Scaling | Missed | False positives |
|---|---|---|---|---|
| Laylight | 2.6 s | n^0.99 | — | — |
| KLayout (flat) | 46 s | n^1.01 | — | 100% |
| KLayout (deep) | 42 s | n^1.00 | — | 100% |
| gdsfactory | 48 s | n^1.02 | — | 100% |
| gdstk | 96 s at 990k, then stopped | n^1.71 | — | 100% |
The sweep is the ring-resonator test chip of the geometry benchmark: n² devices, each with its own radius and gap, so every one is a distinct cell, and the deck finds no violations. KLayout's deep mode cannot reuse anything here, because no two cells are alike; Laylight checks each cell's own shapes once and the interactions between instances only near their boundaries. The width reports of the other tools are the waveguides drawn at the minimum width.
Summary
Each cell: peak throughput (vertices per second), scaling exponent, missed violations, false positives, over the five sizes of the case.
| Case | Laylight | KLayout flat | KLayout deep | gdsfactory | gdstk |
|---|---|---|---|---|---|
| manhattan | 936 k/s, n^1.00, 0%, 0% | 477 k/s, n^1.03, 0%, 0% | 505 k/s, n^1.03, 0%, 0% | 478 k/s, n^1.03, 0%, 0% | 17 k/s, n^1.34, 0%, 18% |
| exclusion | 6.5 M/s, n^1.02, 0%, 0% | 2.9 M/s, n^1.04, 0%, 0% | 3.7 M/s, n^1.03, 0%, 0% | 2.9 M/s, n^1.04, 0%, 0% | 196 k/s, n^1.16, 0%, 0% |
| inclusion | 2.8 M/s, n^0.94, 0%, 0% | 1.3 M/s, n^1.02, 0%, 0% | 1.3 M/s, n^1.01, 0%, 0% | 1.3 M/s, n^1.03, 0%, 0% | 1.3 M/s, n^1.41, 0%, 40% |
| feature | 5.7 M/s, n^1.01, 0%, 0% | 1.6 M/s, n^1.03, 0%, 31% | 1.8 M/s, n^1.01, 0%, 31% | 1.6 M/s, n^1.02, 0%, 31% | 21 k/s, n^1.51, 0%, 31% |
| rings | 3.7 M/s, n^1.01, 0%, 0% | 247 k/s, n^1.00, 0%, 39% | 246 k/s, n^1.01, 0%, 39% | 245 k/s, n^1.01, 0%, 39% | 64 k/s, n^1.89, 0%, 97% |
| couplers | 4.0 M/s, n^1.03, 0%, 0% | 236 k/s, n^1.00, 0%, 99% | 251 k/s, n^0.99, 0%, 99% | 234 k/s, n^1.00, 0%, 99% | 57 k/s, n^1.74, 0%, 99% |
| bends | 6.3 M/s, n^1.02, 0%, 0% | 306 k/s, n^1.00, 0%, 55% | 326 k/s, n^1.00, 0%, 55% | 308 k/s, n^1.00, 0%, 55% | 187 k/s, n^1.61, 0%, 96% |
| rounded | 3.7 M/s, n^1.02, 0%, 0% | 176 k/s, n^1.00, 0%, 50% | 174 k/s, n^1.00, 0%, 50% | 177 k/s, n^1.01, 0%, 50% | 56 k/s, n^1.40, 0%, 50% |
| sweep | 2.5 M/s, n^0.99, —, — | 142 k/s, n^1.01, —, 100% | 150 k/s, n^1.00, —, 100% | 144 k/s, n^1.02, —, 100% | 74 k/s, n^1.71, —, 100% |