Python Library

Device Abstraction

Understand what a device is in Laylight and how it relates to cells, ports, and placement.

A device is the reusable unit of the Python library. It is more than a cell: it combines the parameters that shape the geometry, the code that draws it, and the ports other devices attach to.

This keeps layout assembly explicit. You construct devices, inspect their ports, connect them into larger chains, and only then compile the result.

A device defines a small contract

Every structure is a dataclass that derives from AbstractDevice:

from dataclasses import dataclass

import laylight


@dataclass
class Taper(laylight.AbstractDevice):
    length: float = 10
    width_in: float = 0.5
    width_out: float = 2.0

    def output(self) -> list[laylight.Trans]:
        return [laylight.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(
            laylight.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
  • The dataclass fields are the parameters. Give each a default, so that the device can be built with defaults alone (render_all and PCells rely on it).
  • layout and tech are keyword-only fields of the base class: the Layout the cell goes into and the Technology whose LAYER_* attributes name the process layers.
  • impl() creates the cell, draws into it and returns it. Devices written for the KLayout version of the library named it impl_klayout(); that name still works.
  • input() and output() return the ports. input() defaults to one port at the origin.
  • cell() returns the device's cell, building it on first use.

Callers do not need to know how a device draws itself. They need a cell to place and a set of ports to connect.

One cell per parameter set

cell() builds the cell once per parameter set: two devices of one class with equal parameters share a cell, whichever was built first. There is nothing to hash or cache by hand.

The cell is named after the class when every parameter has its default (Taper) and gets a hash of the parameters otherwise (Taper(length=20) makes Taper$c70843a0), as the Laylight DSL names cells. Override cell_name() to choose another name; a name already in the layout gets a $1, $2, ... suffix.

The cell also records what made it, so that the Laylight IR is self-describing: its source is the class name, its parameters are the fields whose values the IR can hold (numbers, booleans, strings and lists of them), and its ports become the markers input and output.

Geometry is drawn in micrometres

Box, Polygon, Path (flush ends), Point, Vector and Trans are the library's geometry types, in micrometres. They follow KLayout's floating-point types: Box(left, bottom, right, top) accepts its corners in any order, trans * point and point * trans both transform a point, and Trans(mag, rot, mirror, x, y) is a complex transformation. Rotations by a multiple of 90 degrees are exact.

A cell rounds coordinates to integer database units as they are inserted, half away from zero, and drops repeated points and points in the middle of straight edges, as KLayout does. cell.transform(trans) transforms everything already in the cell. laylight.text("label", size) gives text as boxes in the DSL's 5×7 font.

Ports are transforms

Ports are laylight.Trans values: the displacement is the port position and the rotation its direction. Inputs and outputs both point downstream, so a straight waveguide has input() == [Trans()] and output() == [Trans(1, 0, False, length, 0)]. The DSL follows the same convention, so ports line up when a Python cell is imported into DSL code.

Because a port is a transform, connecting two devices is geometry:

parent_output = parent.output()[0]
child_input = child.input()[0]
placement = parent_output * child_input.inverted()

A device can have several inputs and outputs; composition code chooses which to use.

Some devices only know their ports after placing their parts, such as an MMI fan-out tree. They record the ports on the cell while building it (cell.set_ports(outputs=...), which Placer.as_dev(...) does for you) and read them back in output() with return self.cell().outputs. Ports read from the cell stay correct when another device with the same parameters built the shared cell.

Composition happens above the device

Devices describe themselves. Placement logic composes them.

Placer turns device contracts into assembled layouts. When you call then(...), it reads the parent's output, reads the child's input, computes the placement, and records the connection between the two ports. See Placer Chain API.

A device can also place cells directly: cell.insert(child.cell(), trans) places one instance, and cell.insert(laylight.CellInstArray(child.cell(), trans, a, b, na, nb)) a regular array.

Composite blocks can become devices again

After several devices have been chained together, Placer.as_dev(layout, name) wraps the result back into a device: an AdhocDevice around the new cell, with the chain's open ports as its ports.

AdhocDevice(cell, input=..., output=...) does the same for any cell, for example one read from a GDS file with laylight.read(...).

When to think in devices

Define a device when a piece of geometry needs to be:

  • parameterized
  • reused in multiple places
  • connected to downstream structures through ports
  • composed into a larger assembly

Draw directly into a cell when the geometry is local scaffolding that no other code needs to connect to or reuse.

For the minimal workflow, start with the Python Library overview. For chaining devices into larger assemblies, see Placer Chain API.

On this page