Concepts

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 version

A 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") runs laylight build. Cells keep their hierarchy, polygons beyond the GDS vertex limit are split, and ports and cell parameters are written as text properties, so laylight decompile and laylight.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.lyir exports them as JSON, and laylight lvs chip.lyir --netlist ref.json checks 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.lyir publishes cells built in Python as blackboxes, and laylight unbox restores them.
  • The DSL itself. layout.write("cells.lyir") writes IR that DSL code imports with import "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 DSLPython 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 pathBox(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.gdslayout.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.

On this page