DSL

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.

Boolean and morphological operations are shapes too: a shape statement whose primitive is an operation computes a derived shape from other shapes of the cell.

shape rect with 10um, 6um as body
shape circle with 2um as hole
place body at (0, 0)
place hole at body.right_center
shape difference with body, hole as notched
Compiling…
OperationSignatureResult
unionunion(a, b?)The region covered by a or b.
intersectionintersection(a, b)The region covered by both.
differencedifference(a, b)The region of a outside b.
xorxor(a, b)The region covered by exactly one of them.
dilatedilate(a, distance, join?, segments?)a grown by distance.
erodeerode(a, distance, join?, segments?)a shrunk by distance.

As on Shape Primitives, the examples are compiled in your browser. Examples with several layers draw si in blue, slab in amber and metal in violet.

Derived shapes

Operands. a and b are a shape or a list of shapes; a list counts as the union of its shapes. Operands may be on any layer and may themselves be derived shapes. A list can be collected in a loop with a local variable:

shape rect with 14um, 3um as bar
place bar at (0, 0)
define holes = []
for k in 0..6 {
    shape circle with 600nm as hole
    place hole at bar.center + ((k - 2.5) * 2um, 0)
    holes.append(hole)
}
shape difference with bar, holes as grating
Compiling…

Position. The operands are evaluated where they are placed, and the operation runs in cell coordinates, so the result lies where its operands are. A derived shape cannot be placed. Its bounding-box anchors (center, bottom_left, …) and width / height work as for any shape, so other shapes and markers can be placed relative to it. To move or repeat a result, clone it: the copy is an ordinary shape with the same geometry, whose local origin is the cell origin, and it must be placed.

Replacement. An operand on the same layer as the derived shape is replaced by it and is not written on its own. Operands on other layers stay, so a cladding on slab can be derived from a waveguide on si without removing the waveguide. A shape can be an operand of several derived shapes; operands must still be placed.

layer si {
    shape rect with 16um, 800nm as core
    place core at (0, 0)
}
layer slab {
    shape dilate with core, 2um as cladding
    shape difference with cladding, core as trench
}
Compiling…

Here cladding replaces nothing (its operand is on si), trench replaces cladding (both on slab), and core stays on si.

Evaluation. A derived shape is computed when one of its anchors is read, or at the end of the cell. A dependency cycle between derived shapes is reported like a placement cycle. Reading an anchor of an empty result, such as the intersection of disjoint shapes, is an error. A result without area writes nothing.

Fill rule. Each polygon covers the region of its nonzero winding: self-crossing and self-touching polygons, and keyhole polygons, mean what they draw. paths take part as their flush, mitered outline.

Booleans

union

union(a, b?): the region covered by a or b. With a list, b can be left out.

shape rect with 10um, 10um as a
shape circle with 6um as b
place a at (0, 0)
place b at a.top_right
shape union with a, b as result
Compiling…

intersection

intersection(a, b): the region covered by both.

shape rect with 10um, 10um as a
shape circle with 6um as b
place a at (0, 0)
place b at a.top_right
shape intersection with a, b as result
Compiling…

difference

difference(a, b): the region of a outside b. A b that lies inside a leaves a hole; see output for how holes are written.

shape rect with 10um, 10um as a
shape circle with 6um as b
shape rect with 4um, 3um as window
place a at (0, 0)
place b at a.top_right
place window at a.center offset (-1um, -1um)
shape difference with a, [b, window] as result
Compiling…

xor

xor(a, b): the region covered by exactly one of a and b.

shape rect with 10um, 10um as a
shape circle with 6um as b
place a at (0, 0)
place b at a.top_right
shape xor with a, b as result
Compiling…

Morphology

dilate

dilate(a, distance, join?, segments?): a grown by distance, the Minkowski sum with a disk of radius distance (a mitered square for join = "miter"). Holes and gaps narrower than 2 · distance close.

ParameterTypeDefaultMeaning
ashape or [shape]requiredThe shapes to grow.
distancesizerequiredGrowth, not negative.
joinstring"miter"Convex corners: "miter" keeps them sharp, squared off where they would reach further than twice the distance (turns of more than 120°); "round" makes circular arcs (a true disk dilation); "bevel" cuts straight between the two offset edges.
segmentsintegerautoSegments per full circle of the round corners (auto: 1 DBU chord error).
layer si {
    shape cross with 10um, 3um as a
    shape cross with 10um, 3um as b
    shape cross with 10um, 3um as c
    place a at (0, 0)
    place b at (18um, 0)
    place c at (36um, 0)
}
layer slab {
    shape dilate with a, 2um as miter
    shape dilate with b, 2um, join = "round" as round
    shape dilate with c, 2um, join = "bevel" as bevel
}
Compiling…

Concave corners stay sharp for every join: every point within the distance of the shape is covered, and no more.

erode

erode(a, distance, join?, segments?): a shrunk by distance, the set of points whose disk of radius distance fits inside a. Parts narrower than 2 · distance disappear. The parameters are those of dilate; join applies to concave corners, which erosion rounds ("round"), keeps sharp ("miter") or cuts ("bevel"), while convex corners stay sharp.

layer si {
    shape polygon with [(0, 0), (14um, 0), (14um, 5um), (5um, 5um), (5um, 14um), (0, 14um)] as l
    place l at (0, 0)
}
layer slab {
    shape erode with l, 1500nm, join = "round" as inner
}
Compiling…

Opening and closing

Two steps with the same distance make the classic morphological filters. An opening (erode, then dilate) rounds convex corners and removes parts narrower than twice the distance; a closing (dilate, then erode) rounds concave corners and fills gaps. With round joins the corners become arcs of that radius:

shape polygon with [(0, 0), (14um, 0), (14um, 5um), (5um, 5um), (5um, 14um), (0, 14um)] as l
place l at (0, 0)
shape erode with l, 2um as core
shape dilate with core, 2um, join = "round" as opened
Compiling…
shape polygon with [(0, 0), (14um, 0), (14um, 5um), (5um, 5um), (5um, 14um), (0, 14um)] as l
place l at (0, 0)
shape dilate with l, 2um, join = "round" as grown
shape erode with grown, 2um, join = "round" as closed
Compiling…

Both results are on si, so every intermediate shape is replaced by the next one.

Recipes

  • Cladding and exclusion layers. dilate the waveguide shapes onto another layer, as in the example above. With join = "round" the cladding keeps a constant distance around bends. Dilation also extends past the waveguide ends; intersection with a rectangle trims it.
  • Trenches for rib waveguides: difference of the dilated core and the core.
  • Holes, gratings and photonic crystals: collect the holes in a loop and subtract the list, as in the grating above.
  • Rounded corners of any polygon: an opening or closing with round joins, or corner_radius on the primitive itself.
  • Merging overlapping shapes into one polygon: union of the list.

Precision and output

The operations run on the database grid (1 nm by default). All edges are first snap-rounded: every edge is routed through the centers of the grid pixels around the vertices and crossings it passes, so edges meet only at vertices and no new crossings appear. A sweep then finds the winding number of each operand on both sides of every edge and keeps the edges where the result changes. Results deviate from exact geometry by less than one grid unit.

GDS polygons cannot have holes. A result with holes is written as a keyhole polygon: each hole is joined to the outer boundary by a zero-width cut from its rightmost vertex towards +x (straight when the cut meets the boundary on the grid). Axis-aligned rectangles are written as boxes. When writing GDS, polygons of more than 8000 vertices are halved along their longer side until every piece fits.

Reference: equivalents in other tools

Versions: gdsfactory 9, KLayout 0.30, gdstk 1.0, Luceda IPKISS 3 (import ipkiss3.all as i3) and Nazca Design 0.6 (nazca.clipper).

gdsfactory

gdsfactory works on layers of components and flattens the hierarchy; A and B are components or instances.

gdsfactorylaylight
gf.boolean(A, B, "or", layer) (also "|")union with a, b
gf.boolean(A, B, "and", layer) ("&")intersection with a, b
gf.boolean(A, B, "not", layer) ("-", "A-B")difference with a, b
gf.boolean(A, B, "xor", layer) ("^")xor with a, b
c.offset(layer, distance, corner_mode=2)dilate with shapes, distance (see the corner conventions below)
c.offset(layer, -distance)erode with shapes, distance
c.over_under(layer, distance)dilate, then erode (a closing)
c.get_region(layer, merge=True)union with shapes

KLayout (Region)

KLayoutlaylight
r1 | r2 (+ joins without merging)union with a, b
r1 & r2intersection with a, b
r1 - r2difference with a, b
r1 ^ r2xor with a, b
r.sized(d, mode)dilate with a, d; KLayout distances are in DBU
r.sized(-d, mode)erode with a, d
r.minkowski_sum(circle)dilate with a, d, join = "round"
r.merged()union with a
r.rounded_corners(r_inner, r_outer, n)an opening / closing with round joins

gdstk

gdstklaylight
gdstk.boolean(a, b, "or")union with a, b
gdstk.boolean(a, b, "and")intersection with a, b
gdstk.boolean(a, b, "not")difference with a, b
gdstk.boolean(a, b, "xor")xor with a, b
gdstk.offset(p, d, join="miter", tolerance=2)dilate with p, d
gdstk.offset(p, d, join="round")dilate with p, d, join = "round"
gdstk.offset(p, -d, ...)erode with p, d, ...

Luceda IPKISS

IPKISSlaylight
shape1 | shape2, boundary1 | boundary2union with a, b
shape1 & shape2intersection with a, b
shape1 - shape2difference with a, b
shape1 ^ shape2xor with a, b
i3.get_elements_for_generated_layers(elements, {L0 - L1: Lout})a derived shape on the output layer with the shapes of both layers as operands
i3.ShapeGrow(shape, amount), i3.ShapeOffset(shape, offset)dilate with a, amount
i3.ShapeGrow(shape, -amount)erode with a, amount
i3.merge_elements(elements, layers)union with shapes

Nazca Design

Nazcalaylight
clipper.merge_polygons(paths)union with shapes
clipper.clip_polygons(A, B)intersection with a, b
clipper.diff_polygons(A, B)difference with a, b
clipper.xor_polygons(A, B)xor with a, b
clipper.grow_polygons(paths, grow, jointype="round")dilate with shapes, grow, join = "round"
nd.Polygon(...).grow(grow, jointype="square")dilate with a, grow
clipper.grow_polygons(paths, -d)erode with shapes, d
add_layer2xsection(xs, layer, growx=d)dilate of the waveguide onto the cladding layer

Conventions that differ

  • Miter limit. laylight's "miter" keeps a corner sharp up to twice the distance and squares it off beyond: the same as gdstk's join="miter", tolerance=2 (Clipper). KLayout's sized modes cut corners by how far the miter extends along the edges. The default mode 2 keeps right angles and cuts sharper corners; modes 4 and 5 are close to an unlimited miter. IPKISS's ShapeGrow is an unlimited miter.
  • Bevel. laylight's "bevel" cuts straight between the two offset edges, like KLayout mode 0. gdstk's join="bevel" is Clipper's square join, which cuts the corner at the distance, perpendicular to its bisector.
  • Round. laylight's "round" is a true disk dilation or erosion. KLayout has no round sizing mode; use minkowski_sum with a circle. Nazca's grow_polygons defaults to round joins, but Polygon.grow defaults to square.
  • Scope. gdsfactory, KLayout layer operations and IPKISS generated layers work on whole layers and flatten the cell hierarchy. laylight operates on named shapes of one cell and does not look into instances.
  • Grid. laylight computes on the DBU grid with snap rounding. gdstk rounds to precision (1 nm by default); Nazca's grow_polygons uses accuracy (0.1 µm by default).

On this page