Python Dialect
For those who would rather stay in Python than use the DSL, a Python front end that builds Laylight IR directly and compiles it with the same back end.
The Laylight language is the shortest way to describe a layout and the one the compiler checks most, but it is not the only way in. If you would rather stay in Python, for a device library you already have, a parameter sweep, or a script that computes geometry, Laylight has a Python dialect: the laylight Python package, a second front end of the same compiler.
pip install laylight # also installs the laylight command line of the same versionA front end that builds IR
The Python dialect does not generate Laylight source and does not draw with another layout library. Its devices build the Laylight IR directly, in Python, and hand it to the back end that compiles the DSL:
.lyl source ─── parse, check, evaluate ───┐
├──► Laylight IR ──► laylight build ──► GDSII
Python devices ── build the IR in Python ─┘The IR is the boundary. The Python dialect writes the same text the compiler reads: laylight ir prints a file written from Python back unchanged. So everything after the IR is shared, not reimplemented:
- GDSII.
layout.write("chip.gds")runslaylight build. Cells keep their hierarchy, polygons beyond the GDS vertex limit are split, and ports and cell parameters are written as text properties, solaylight decompileandlaylight.read()recover them. - Dummy fill.
layout.write("chip.gds", dummy=["wg:size=2um"])passes the rules to--dummy. - Netlists and LVS. The IR records the connections made by placing devices port to port.
laylight netlist chip.lyirexports them as JSON, andlaylight lvs chip.lyir --netlist ref.jsonchecks the ports and compares the design with a reference netlist. Ports that meet without a declared connection, such as the ports a cell exports, are connected by position. - Blackboxes.
laylight blackbox chip.lyirpublishes cells built in Python as blackboxes, andlaylight unboxrestores them. - The DSL itself.
layout.write("cells.lyir")writes IR that DSL code imports withimport "cells.lyir". Its top cells are placed like cells written in Laylight, port to port, and appear in the design's netlist with the connections recorded in Python.
The IR carries what a DSL cell carries, so the back end cannot tell the two front ends apart. Devices with equal parameters share one cell, named the way the DSL names cell instantiations: Taper when every parameter has its default, Taper$c70843a0 for Taper(length=20). Each cell records its source and parameters. Ports are the input and output markers, pointing downstream as in the DSL. Placing a device on another's port records a connection between the two ports.
KLayout is not needed. It remains an optional extra, laylight[klayout], for PNG previews and KLayout PCells.
The syntax in brief
A device is a dataclass. Its fields are the parameters, impl() draws its cell, and input() / output() return its ports. A Placer puts devices end to end:
from dataclasses import dataclass
import laylight
from laylight import Box, Polygon, Trans
@dataclass
class Taper(laylight.AbstractDevice):
length: float = 10
width_in: float = 0.5
width_out: float = 2.0
def output(self) -> list[Trans]:
return [Trans(1, 0, False, self.length, 0)]
def impl(self) -> laylight.Cell:
cell = self.layout.create_cell(self.cell_name())
cell.shapes(self.tech.LAYER_WG).insert(
Polygon([(0, -self.width_in / 2), (self.length, -self.width_out / 2),
(self.length, self.width_out / 2), (0, self.width_in / 2)])
)
return cell
@dataclass
class Bar(laylight.AbstractDevice):
length: float = 20
width: float = 2.0
def output(self) -> list[Trans]:
return [Trans(1, 0, False, self.length, 0)]
def impl(self) -> laylight.Cell:
cell = self.layout.create_cell(self.cell_name())
cell.shapes(self.tech.LAYER_WG).insert(Box(0, -self.width / 2, self.length, self.width / 2))
return cell
layout, top, tech = laylight.new_layout("Chain")
laylight.Placer(Taper(layout=layout, tech=tech)).then(Bar(layout=layout, tech=tech), anchor="bar").into(top)
top.set_ports(inputs=[Trans()], outputs=[Trans(1, 0, False, 30, 0)])
layout.write("chain.lyir") # Laylight IR
layout.write("chain.gds") # GDSII, through `laylight build`The IR it writes has the two device cells, with their parameters and ports, and the cell that connects them:
cell Taper source=Taper {
param length 10
param width_in 0.5
param width_out 2.0
polygon wg (0,-250) (10000,-1000) (10000,1000) (0,250)
marker input (0,0,0deg)
marker output (10000,0,0deg)
}
...
cell Chain source=Chain {
inst Taper_0 Taper at (0,0)
inst bar Bar at (10000,0)
marker input (0,0,0deg)
marker output (30000,0,0deg)
connect bar.input[0] Taper_0.output[0]
}The constructs map onto the DSL's:
| Laylight DSL | Python dialect |
|---|---|
cell Taper(length = 10um, ...) | @dataclass class Taper(laylight.AbstractDevice) with a field length: float = 10 |
a process layer, layer si { ... } | cell.shapes(tech.LAYER_WG); the layers come from a Technology |
shape rect, shape polygon, shape path | Box(left, bottom, right, top), Polygon(points), Path(points, width) |
marker { input { ... } output { ... } } | input() and output(), returning Trans(mag, rotation, mirror, x, y) |
place B.marker.input[0] at A.marker.output[0] | Placer(a).then(b); at=Placer.OutPort(i) and into=Placer.InPort(j) pick other ports |
laylight build file.lyl -o out.gds | layout.write("out.gds") |
Two differences follow from Python being a general-purpose language. Lengths are bare numbers in micrometres rather than quantities with units, and mistakes show up when the code runs rather than when it is checked.
Choosing, and mixing
Use the DSL for new devices: units and types are checked before anything is drawn, the editor completes and formats it, and boolean operations, cross-sections and routing are part of the language. Use the Python dialect where Python is already the environment: existing device code, numeric libraries, or a flow that drives the layout from other tools.
The two meet in the IR. Cells built in Python can be written to a .lyir file and used from Laylight code like any other cell:
import "tech.lyl"
import "chain.lyir"
cell Top {
place Chain at self.origin as first
place Chain.marker.input[0] at first.marker.output[0] as second
}The Python Library section documents the dialect: devices, Placer, the built-in device catalog and the KLayout integration.