Placer Chain API
Understand how Placer composes devices by chaining transforms and placements together.
Placer is the composition layer for laylight devices. A device knows how to generate its own geometry and expose its ports. Placer answers the next question: how do several devices become one assembled path or block?
Its main interface is still a fluent chain API. You start from a Placer, append devices with then(...), adjust transforms with methods like move() or rotate(), and finally materialize the result with into(...) or wrap it back into a reusable device with as_dev(...).
Internally, modern Placer is no longer limited to a single linked list. It is graph-backed and can represent several single-input/single-output chains that branch from, or attach into, a larger composite device.
Why Placer exists
The low-level contract of a device is geometric. Each device exposes input and output transforms. That is enough information to connect one structure to the next, but writing those transform calculations by hand would be repetitive and error-prone.
Placer turns that transform math into a readable assembly flow:
- start a chain
- add a device
- align the next device to the previous output
- optionally adjust position, rotation, or mirroring
- place the whole chain into a cell
This is why many structure implementations read like a sequence of building blocks instead of a sequence of matrix operations.
The chain model
Each call to then(...) returns a new chain cursor. That cursor represents one single-input/single-output path through the larger Placer graph.
In the default case, then(child) means:
- take the current device's output transform
- take the child's first input transform
- compute the alignment transform
- carry that result forward as the new chain state
This makes the chain API read left to right in the same order that geometry is assembled. That linear style remains the default mental model even though the underlying data structure now supports branching and attachment.
The racetrack example
The Racetrack device is a good example because its layout is a simple closed loop made from two straights and two 180-degree arcs. Its implementation uses Placer directly:
(
Placer()
.move(-self.straight_length / 2, -self.radius)
.then(straight)
.then(arc)
.then(straight)
.then(arc)
.into(cell)
)This chain says:
- start from an empty placer
- shift the origin so the racetrack is centered around the cell origin
- place the first straight
- attach an arc to its output
- attach another straight to the arc's output
- attach the final arc to close the loop
- insert the resulting instances into
cell
What matters here is not only brevity. The chain reflects the topology of the shape. The source code follows the same order as the physical path in the layout.
Starting a chain
There are two common starting points:
Placer()
Placer(device)Placer() starts from an empty origin. This is useful when the first thing you want to do is set an absolute shift or add the first device explicitly with then(...).
Placer(device) starts with a concrete device already attached to the chain. This is useful when a structure has a natural first element and the rest of the chain extends from that starting point.
Adding devices with then(...)
then(...) is the center of the API.
next_placer = current.then(device)By default, this aligns the child's first input to the current output port 0. That is the common case for waveguide-like chains.
When needed, placement can be made explicit:
current.then(device, at=Placer.OutPort(1))
current.then(device, into=Placer.InPort(1))
current.then(device, at=Placer.Position(20, 0))
current.then(device, at=Placer.AbsPos(100, 50))
current.then(device, at=Placer.Trans(transform))OutPort(index)connects to one of the current device's output portsInPort(index)selects which input of the child device the chain consumesPosition(x, y)applies a relative shift from the current chain stateAbsPos(x, y)places a device at an absolute positionTrans(...)injects a full transform directly
These modes let the same API cover both strict port chaining and looser layout assembly.
Branching and revisiting earlier nodes
Multi-output devices can be used without leaving the chain-first style.
Use out(index) when you want a specific output:
current.out(1).then(branch_device)Use branch() when you want one chain handle per output:
upper, lower = current.branch()
upper.then(upper_arm)
lower.then(lower_arm)Use anchor="name" plus ref("name") when you want to branch from a device after the main chain has already advanced:
current = (
Placer()
.then(splitter, anchor="split")
.then(main_path)
)
current.ref("split").out(1).then(side_path)The main chain can stay readable while side branches are added later.
Detached chains and attach(...)
Multi-input devices are handled by combining one main chain with one or more detached auxiliary chains.
Create the extra path with chain():
current = Placer().then(src).then(combiner, anchor="comb")(
current.chain()
.then(aux_stage_1)
.then(aux_stage_2)
.attach(anchor="comb", port=1)
)attach(...) rigidly aligns the detached chain's terminal output to the target input port. It does not auto-route or generate additional waveguides between the two endpoints.
Adjusting the current node
The fluent transform methods modify the current node before it is materialized:
x(...)andy(...)set offsetsmove(dx, dy)adds a relative offsetrotate(angle)adds rotationmirror()mirrors the placed device
Because these methods return the current Placer, they can be inserted anywhere in the chain. In the racetrack example, the initial move(...) offsets the whole chain before any device is attached.
Materializing the chain
Placer does not insert geometry immediately. The chain becomes real only when you call into(top_cell).
outputs = chain.into(top)At that point, Placer walks up the parent chain, places each device once, and returns the transformed output ports of the final device.
This delayed placement matches the rest of laylight: the chain is a description of assembly until a caller asks for the instances to be inserted.
Turning a chain back into a device
For larger assemblies, as_dev(layout, name) is often the key step.
compound = chain.as_dev(layout, "MY_BLOCK")This creates an AdhocDevice whose cell contains the assembled chain. The returned object still behaves like a device, with a cell plus tracked inputs and outputs. That means a compound block can be reused in later Placer chains just like any other device.
This is how the chain API scales from simple shapes to larger reusable subsystems.
Exposing ports and anchors
then(...) also supports metadata that becomes useful in composite blocks:
as_input=Truerecords the placed device's input ports as public inputs of the chainas_output=Truerecords the placed device's output ports as public outputs of the chainanchor="name"stores a named handle to that node in the chaindummy=Trueskips physical insertion while still participating in transform computation
These options matter less for a simple racetrack, but they matter a lot when a chain is being promoted into a reusable multi-port device.
When Placer is the right abstraction
Use Placer when you want the source code to describe connectivity and assembly order, not just instance coordinates.
It is especially useful when:
- one structure is attached directly to another through ports
- a repeated path should read as a left-to-right chain
- a composite block should later become a reusable device
- the transform logic would otherwise be scattered across manual
DCplxTranscalculations
If the layout is just a few independent absolute placements, direct cell insertion can still be simpler. Placer becomes most valuable when the geometry is fundamentally connected.
Related concepts
For the device contract that makes Placer possible, see Device Abstraction.