Design Rules
Write a process's design rules as a deck in Laylight and have every violation reported at the statement that drew the shape, by `laylight check`, `laylight drc` and the language server.
A DRC deck is a top-level item, define drc <name> { rules }. A PDK puts it next to its layer table and cross-sections, and every program that imports the file is checked against it. The rules' limits are ordinary expressions, so they can refer to global constants.
define layer {
si { gds 1/0 }
slab { gds 2/0 }
}
define min_width = 400nm
define drc soi {
with severity = "error" # the deck's defaults
width si >= min_width as min_width
space si >= 200nm
space si, metal >= 1um # between two layers
enclosure si in slab >= 300nm # slab reaches 300nm past si
overlap si, metal >= 500nm # where they meet, by at least 500nm
inside si in slab # no si outside slab at all
area si >= 200nm * 200nm
radius si >= 5um with severity = "warning", message = "bend too tight"
}laylight check chip.lyl # evaluation errors and the decks
laylight drc chip.lyl --report drc.json # only the decks, with a JSON report
laylight drc chip.lyl --lym drc.lym # the compact marker file; `laylight lym drc.lym` reads it
laylight drc chip.lyl --deck soi --top Chip --markers drc.gds # the layout plus a marker layer per rule
laylight drc received.gds --rules tech.lyl # a GDS from elsewhere, against the decks of tech.lylViolations are diagnostics like any other: they point at the shape ... as name statement that drew the shape, name the other shape and, between instances, the place statements, and give the measured value and the position in the cell. The language server reports them while you type. Rules with severity = "error" make check and drc exit with 1; warnings do not.
Rules
Each rule is one line: <kind> <layer>[, <layer> | in <layer>] [>= <limit> | <= <limit>] [with properties] [as name]. inside, empty and angle have no limit; <= is for area and density.
| Rule | Checks |
|---|---|
width L >= d | Opposite edges inside a polygon of L are at least d apart. Convex corners sharper than 90° (slivers) are reported too. |
space L >= d | Opposite edges outside the polygons of L are at least d apart: between polygons, and across a polygon's own notches. Concave corners sharper than 90° are reported too. |
notch L >= d, isolated L >= d | space within one polygon only, or between different polygons only. |
space A, B >= d | Where A and B do not overlap, they are at least d apart. |
enclosure A in B >= d | The boundary of B runs at least d outside the boundary of the A it contains. |
overlap A, B >= d | Where A and B overlap, they do so by at least d. |
inside A in B | Every part of A lies inside B; each region of A outside B is reported with its area. Checked through the hierarchy: a child's A may be covered by the parent's B. |
empty L | Every polygon of L is a violation, for a layer expression that should be empty: empty si & metal, empty si.not_inside(slab). |
area L >= a, area L <= a | Each polygon of L has an area of at least / at most a (200nm * 200nm). |
length L >= d | Edges shorter than d; the edges of discretized curves are not counted. |
angle L in [0deg, 90deg] | Edges whose direction (modulo 180°) is not one of the list, within 0.1°; curved shapes are not counted. |
corner L >= a | Corners sharper than a: convex ones by their interior angle, concave ones by the angle of the notch. |
grid L >= g | Vertices whose coordinates are not multiples of g. |
density L <= x with window = w[, step = s] | Windows of side w on a grid of pitch s (default w) anchored at the top cell's origin, in which L covers more (or, with >=, less) than the fraction x. |
radius L >= r | Every curved primitive drawn on L (arc, ring, racetrack, euler, sbend, bezier, a path with bend_radius) bends with a radius of at least r, taken from the primitive, not from its polygon. A path with corners and no bend_radius has radius 0. |
The edge-pair checks are made on the DBU grid, as in KLayout: distances are Euclidean (corner to corner counts, except for enclosures, which are measured where the outer edge runs along the inner one), with metric = "projection" only where two edges overlap in projection, or with metric = "square" as the Chebyshev distance. Overlapping shapes on the same layer of a cell are merged before they are checked.
Properties, on a rule or as a with line for the whole deck:
| Property | Values | Meaning |
|---|---|---|
severity | "error" (default), "warning" | Whether a violation fails the command |
metric | "euclidean" (default), "projection", "square" | How edge pairs are measured |
message | a string | Replaces the default width 350nm < 400nm on si text |
min_projection, max_projection | sizes | Report an edge pair only when the edges overlap in projection by at least / at most this much |
angle_limit | an angle, 90° by default | Edges face each other when their directions differ by more than 180° minus this; corners sharper than this are acute |
window, step | sizes | The window and grid pitch of a density rule |
as name names the rule (reports say soi.min_width); without it the name is the kind and the layers (width_si, enclosure_si_slab).
Derived layers
A deck can define layers from others with define name = <layer expression> and use them in later rules and definitions, the way a KLayout script builds intermediate layers:
define drc soi {
define clad = slab - si # booleans: + - & ^, grow / shrink: + 200nm, - 200nm
define narrow = si.with_width(0, 1um) # filters: with_area, with_perimeter, with_width, with_height, with_holes
define pads = metal.rectangles.with_area(16um * 1um)
define loose = si.not_inside(slab) # selections: inside, outside, overlapping, interacting, covering and their not_ forms
width clad >= 300nm
space narrow >= 200nm with metric = "square"
empty loose as uncovered
empty pads.not_interacting(si) as unconnected_pads
empty si.holes.with_area(0, 1um * 1um) # members: holes, hulls, extents, rectangles, non_rectangles, rectilinear, non_rectilinear
}Rules on derived layers, empty and density run flat per top cell, since derived geometry can combine shapes of different cells; their violations are reported in the top cell and still point at the statements that drew the shapes. Rules on plain layers stay hierarchical.
Curves
The checker uses what the compiler knows about each shape:
- A shape drawn from a primitive carries its exact minimum width: the width of a waveguide, the shorter side of a rectangle, the diameter of a circle. A shape that does not touch another shape of its layer passes or fails the
widthrule by that number alone, in constant time, without the discretization noise that makes a ring drawn exactly at the minimum width fail at every vertex in a polygon-based check. - Minimum radius comes from the primitive: the radius of an arc or ring, the tightest radius along an Euler bend, S-bend or Bézier curve, the
bend_radiusof a path. - Edges of discretized curves (circles, rings, arcs, bends, rounded corners) lie up to 1 DBU inside the true curve, so they get a tolerance of 1 DBU in every edge-pair check: a gap drawn exactly at the minimum spacing between a ring and its bus is not reported.
Hierarchy
Each cell is checked once, however many times it is placed. Interactions between a cell's instances, or between an instance and the cell's own shapes, are checked only where their bounding boxes come within a rule's limit, on the shapes near that window; identical pairs of array members (the same two cells at the same relative position) are checked once and counted. A violation found between instances is reported in the parent cell, in its coordinates, with the placements noted.
Marker files
--lym out.lym writes every violation to a marker file, Laylight's counterpart of KLayout's .lyrdb: a compact binary container (about 11 bytes per violation, sixty times smaller than the JSON report) with a string table, the decks and rules, the DSL statements, and the violations in chunks of one cell with their positions, values, edges and sources, each chunk compressed and indexed by its bounding box and its counts per rule. A reader opens the index alone for a summary, and decodes only the chunks that meet a window. laylight lym file.lym prints the summary, --list every violation, --json out.json the JSON report. The Laylight viewer opens marker files too (its markers prop; the reader runs in WebAssembly) and draws the violations over the layout with a Marker Explorer panel. The format is documented in the repository under docs/laylight-dsl/lym.md.
Report
laylight drc --report out.json writes every violation:
{"cells": 3, "seconds": 0.012,
"rules": [{"deck": "soi", "rule": "min_width", "kind": "width", "limit": 400, "violations": 1}],
"violations": [{"deck": "soi", "rule": "min_width", "cell": "Ring", "value": 350, "limit": 400,
"at": [0, -5000], "count": 1, "message": "drc soi.min_width: width 350nm < 400nm on si at (0um, -5um) in Ring",
"shapes": [{"cell": "Ring", "shape": 1, "name": "bus", "source": {"file": "ring.lyl", "line": 21, "column": 40}}]}]}Lengths are in nm (areas in nm²), positions in the reporting cell's coordinates; edge pairs carry both edges and, through an instance, the place statement as placed. The benchmark against KLayout, gdsfactory and gdstk is described under Efficient DRC.
Boolean and Morphological Operations
Combine shapes with union, intersection, difference and xor, grow and shrink them with dilate and erode, and the equivalent calls in gdsfactory, KLayout, gdstk, Luceda IPKISS and Nazca.
Blackboxes
Publish a cell as a blackbox (its outline and ports without its geometry), design with it in other projects and tools, and put the real cell back. The blackbox's record is kept in several places so that it survives other tools, and every check says what differs.