Concepts

Auto Routing

Waveguides between devices as code. Cross-sections say what a waveguide is made of, ports say where it starts and ends, and routes are computed from them.

Much of a photonic circuit is not devices but the waveguides between them. Each of these waveguides joins two ports. It has to leave and arrive in the ports' directions, carry every layer of the waveguide it continues (core, slab, cladding opening, heater), never bend tighter than the process allows, sometimes have an exact length, and stay clear of its neighbors. Drawn by hand, routes are where layouts go wrong: a corner computed from a stale coordinate, a slab forgotten on one bend, a route that no longer reaches its port after a device has moved.

Laylight splits routing into two ideas. A cross-section says once what a waveguide is made of: everything drawn with it carries all of its layers, and the ports at its ends remember it. A route is geometry computed from the two ports it joins, so it follows the devices it connects. Both are part of the language today. The route statement, which will find the way between two ports by itself, is being designed.

define xsection rib(width = 700nm) {
    si   width
    slab width + 4um
    with min_radius = 10um
}

cell Link(dy = 30um) {
    shape straight with 10um, rib as a.start at (0, 0)
    shape straight with 10um, rib as b.start at (70um, dy)

    define p = a.end
    define q = b.start
    define x = (p.x + q.x) / 2
    define corners = [p, (x, p.y), (x, q.y), q]
    shape path with corners, rib, bend_radius = 10um as link at self.origin
}
Compiling…

The route between the two rib waveguides is a path through corners taken from their ends. It carries the slab along every bend, and its bends are checked against the 10 µm minimum radius of rib. When dy changes, the corners move with b.

Cross-sections

A cross-section lists the sections across a waveguide, one per line: a layer, a width, and optionally an offset from the centerline (positive to the left of the direction of travel). The first section is the main one, the waveguide proper. The others follow it: a slab, an opening in the cladding, a heater beside the core, the ground lines of an electrode. Properties such as the tightest bend allowed are set with with.

define xsection heated(width = 500nm, gap = 6.5um) {
    si    width
    metal 2um offset gap
    with min_radius = 10um
}

cell PhaseShifter {
    shape straight with 40um, heated as ps.start at self.origin
    shape arc with 15um, heated, 90deg as turn.start at ps.end
}
Compiling…
  • Drawn with any waveguide shape. Wherever a primitive takes a width (straight, arc, euler, sbend, bezier, path, ring, racetrack and both ends of a taper), it takes a cross-section, and every section follows the same centerline. Such shapes span several layers, so they are written outside layer blocks. Anchors and shape operations see only the main section, so adding a cladding never moves a placement.
  • Parameters. Cross-sections take parameters like cells: rib(width = 1um). A cell can take a cross-section as a parameter, so one Straight cell draws strip or rib waveguides.
  • Conversions. A taper between two cross-sections converts one into the other section by section. Sections that only one end has narrow into the main section, so taper with 15um, rib, strip is a complete rib-to-strip converter.
  • Ports carry them. A marker on the start or end of a shape drawn with a cross-section takes that cross-section, with its width and type, and an exported port keeps it. The connection checks compare them: a rib waveguide butted against a strip waveguide is an error that asks for a taper, just like ports of different widths.
  • Checked on the exact curve. min_radius is checked against the tightest radius of every shape drawn with the cross-section, computed on the continuous curve rather than estimated from polygon vertices (see Efficient DRC).

Cross-Sections in the Language Reference has the details.

Routes from ports

A port is a frame: a position and a direction that points downstream. Everything a route needs is in its two frames. Laylight therefore computes routes from ports instead of drawing them at coordinates, and today you write them with the shapes above:

  • Manhattan routes are paths through corners whose coordinates come from the ports (p.x, p.y), and bend_radius turns every corner into a circular bend, as in the first example. When a device moves or a parameter changes, the corners move with the ports.
  • S-bends shift a waveguide sideways and keep its direction: sbend with length, offset, xs placed at a port, with the offset computed from where the port has to go.
  • Bundles are loops. A marker group is a list of ports, so a loop can visit them and compute one route per port. Below, four channels 20 µm apart converge to 2 µm apart: the middle channels turn first and each outer one 2 µm later, so the routes nest without crossing.
define xsection wire(width = 500nm) {
    si width
    with min_radius = 4um
}

# Four channels 20 um apart brought together 2 um apart.
cell Bundle(n = 4, pitch = 20um, target = 2um, radius = 4um) {
    for i in 0..n {
        define y = ((n - 1) / 2 - i) * pitch
        define y2 = ((n - 1) / 2 - i) * target
        define x = radius + (abs(i - (n - 1) / 2) - 0.5) * target
        define corners = [(0, y), (x, y), (x, y2), (60um, y2)]
        shape path with corners, wire, bend_radius = radius as r at self.origin
    }
}
Compiling…
  • Checked like everything else. A route drawn with a cross-section has every bend checked against its min_radius, and a path whose bends do not fit between its corners is an error at the line that draws it.
  • Routes are shapes. Each section of a route is one polygon, with no seams between its straights and bends, in the cell that draws it. No bend cells are generated, and routes can be operands of shape operations, for example grown together into a keep-out region.

Routing in the examples

Two of the examples are routed circuits. Like all examples, each is written in Laylight, gdsfactory, gdstk and KLayout, and the four build the same layout.

  • Fiber-array test structure. The unbalanced Mach-Zehnder interferometer wired to four grating couplers at the 127 µm pitch of a fiber array, with a loopback between the inner two. It has three routes, two L-shaped and one U-shaped, each a path through corners taken from the two ports it joins.
  • Optical phased array. After Doylend et al.: a tree of MMIs joined by S-bends spreads the input over 16 channels 100 µm apart, a heated cross-section carries a heater beside each channel, and a nested bundle of 16 routes brings the channels together on grating emitters 3.5 µm apart.
Compiling…
PhasedArray from laylight/phased_array.lyl, compiled in your browserOpen in playground

gdsfactory's route_single and route_bundle find these routes by themselves, while the Laylight versions compute their corners. The Laylight versions are still the shortest: 19 lines and 232 tokens for the test structure against 23 lines and 259 tokens in gdsfactory, and 43 lines and 412 tokens for the phased array against 60 lines and 512 tokens. The cross-section that carries the heater, the S-bends placed on ports and the loop over the channels take less code than the calls to the router. Routers have their own surprises, too. With the emitters only as far from the heaters as the outermost route needs, gdsfactory's route_bundle crossed routes and reported a collision, so all four versions leave a little more room. gdstk and KLayout have neither ports nor routing, and compute every corner from the geometry of the devices.

Learn it in the Tour

The Tour chapter Cross-sections and routing teaches all of this in six exercises, each checked in your browser:

LessonWhat it teaches
Cross-sectionsDefine a cross-section and draw a straight and a bend with it.
Offsets and parametersSections beside the centerline, such as heaters, and cross-sections with parameters.
Changing cross-sectionA taper from a rib to a strip waveguide.
Ports and cross-sectionsPorts that take their cross-section, and the connection check that catches a mismatch.
Routing with pathsA route through corners computed from two ports, which still fits when a device moves.
Fan-outS-bends in a loop over a group of ports.

Earlier, Port to port chains instances by their ports. At the end, the last project, the splitter tree, builds the tree of the phased array level by level from a list of the ports still to be fed.

The route statement

Status

The route statement is designed but not yet implemented, and its syntax may still change. Until it is available, write routes with paths and S-bends as shown above. The cross-section properties it will use (radius, bend, p and pitch) can already be set and read.

Computing corners is fine for a few routes and for regular bundles, but most routes follow a handful of patterns, and their corners are boilerplate. The route statement computes them:

route a.marker.output[0] to b.marker.input[0]
route a.marker.output[0] to b.marker.input[0] with radius = 20um, bend = "euler" as r
route bank.marker.output to gcs with style = "sbend", sort = true as fan
route m.marker.output[0] to n.marker.input[0] through [(80um, 0), (80um, 60um)]
route m.marker.output[0] to n.marker.input[0] through { x = 300um; y = 120um }
  • Ports point downstream. A route leaves its start in the start's direction and arrives at its end in the end's direction, so an output is routed to an input as it is. A port used backwards, such as the output of an MMI that serves as a combiner, is written p.reversed.
  • Fixed rules instead of a search. In the default manhattan style, every bend turns by 90°, and a route takes the fewest bends that fit: none for ports that face each other on a line, one for an L, two for a U or a jog, three or four otherwise, and the shortest of the routes with that many. The same ports always give the same route. Where the rules find no route that fits, the statement is an error that says what to change, such as style = "sbend" or a through, instead of a guess.
  • Defaults from the cross-section. The cross-section of the ports gives the route its layers, its bend radius (radius, by default the smallest that respects min_radius), its kind of bend ("arc", "euler", or "corner" for metal wires) and the pitch of bundles. Ports with different cross-sections cannot be routed to each other without a taper.
  • Waypoints. through takes a list of corners, or steps that change one coordinate at a time (x = 300um, dy = -10um). After the last one, the route finds its way to the end by itself.
  • Bundles. Two lists of ports are routed as a bundle, the i-th to the i-th. Parallel segments are pitch apart and the inner routes turn first, so that no two routes cross. A pairing that would cross is an error that suggests sort = true.
  • Length. Every route has a length, which expressions and attributes can read. length = L makes a route exactly L long by adding a detour, for delay lines and interferometer arms. widen runs the long straights in a wider cross-section, with tapers at both ends, to reduce loss and phase errors.
  • A route is a shape. As with routes today, every section is one seamless polygon in the cell that draws it, with start and end anchors, and a route can be an operand of shape operations. A later output option may write repeated bends as shared cells, for layouts with so many bends that the file size matters.
  • Routes are connections. A route between two ports records a connection, so laylight netlist and laylight lvs see every route.
  • Not in the first version: routing around obstacles, and vias inserted where a route changes layer.

With it, the routes of the fiber-array test structure become three statements, the loopback with its two corners:

route gcs[0] to mzi.marker.input[0] with wire
route mzi.marker.output[0] to gcs[3].reversed with wire
route gcs[1] to gcs[2].reversed through [(gcs[1].x, y), (gcs[2].x, y)] with wire

and a bundle such as the fan-in of the phased array becomes one statement over the two lists of ports, route tuners to emitters with wire, pitch = emitter_pitch.

Reference: equivalents in gdsfactory

gdsfactoryLaylight todayLaylight route (designed)
gf.cross_section.cross_section(width, layer, sections=[gf.Section(width, offset, layer)])define xsection, one line per section
radius, radius_minwith radius, with min_radius
gf.path.transition with gf.path.extrude_transitiontaper with length, xs1, xs2
gf.routing.route_single(c, port1, port2, cross_section=xs)a path drawn with xs through corners computed from the portsroute port1 to port2 with xs
waypoints=[...]the corners of the paththrough [...]
steps=[{"x": ...}, {"dy": ...}]—through { x = ...; dy = ... }
gf.routing.route_bundle(c, ports1, ports2, separation=..., sort_ports=True)a loop over the two lists of portsroute ports1 to ports2 with pitch = ..., sort = true
gf.components.bend_s, gf.routing.route_single_sbendsbend with length, offset, xsstyle = "sbend"

Two conventions differ. gdsfactory's section offsets are positive to the right of the direction of travel, Laylight's to the left. And gdsfactory's separation is the gap between the edges of neighboring waveguides, while Laylight's pitch is measured between their centerlines.

On this page