Examples

Optical Phased Array

A 16-channel optical phased array after Doylend et al.; a tree of MMIs, heaters 100 µm apart and a routed fan-in onto grating emitters 3.5 µm apart.

Compiling…
PhasedArray from laylight/phased_array.lyl, compiled in your browserOpen in playground

An optical phased array steers a beam of light without moving parts. The light is split over many emitters, and the phase of each one sets the direction in which their emission adds up. Doylend et al. built a 16-channel phased array in silicon that steers in two dimensions: along its gratings by the wavelength, and across them by the relative phase of the channels. A tree of 1×2 MMIs splits the input into 16 channels. Each channel then passes a resistive heater, 470 µm long and 6.5 µm from the waveguide, that tunes its phase. At the heaters the channels are 100 µm apart, which limits the thermal crosstalk between them; after the heaters they converge on an array of surface gratings 3.5 µm apart and 200 µm long, which emit the beam.

The example draws this circuit. Most of it is routing: S-bends that spread the tree's outputs, waveguides that carry a heater along, and a bundle of 16 routes that brings the channels together without crossing. The paper's waveguides are 1 µm rib waveguides and its heaters a nickel-chromium film on the silicon. The example uses the 500 nm wires and the 1×2 MMI of the earlier examples, a 2 µm heater strip on the heater layer, and gratings shallow-etched across the full width of the wire, with the 600 nm period of the paper's test gratings. The wiring of the heaters to their pads and the trenches that isolate the channels thermally are left out.

References

  • J. K. Doylend, M. J. R. Heck, J. T. Bovington, J. D. Peters, L. A. Coldren and J. E. Bowers, "Two-dimensional free-space beam steering with an optical phased array on silicon-on-insulator," Optics Express 19(22), 21595–21604 (2011). doi:10.1364/OE.19.021595
  • Z. Sheng et al., "A compact and low-loss MMI coupler fabricated with CMOS technology," IEEE Photonics Journal 4(6), 2272–2277 (2012), for the splitters. doi:10.1109/JPHOT.2012.2230320

Parameters

ParameterValueSource
Channels16, from a tree of 1×2 MMIsDoylend 2011, Sec. 2
Heaters470 µm long, 6.5 µm from the waveguide (taken center to center), 2 µm wideDoylend 2011, Sec. 2; width assumed
Channel spacing at the heaters100 µmDoylend 2011, Sec. 2
Emitters3.5 µm apart, 200 µm long (333 periods), 50% duty cycle, grooves across the full widthDoylend 2011, Sec. 2
Grating period600 nmDoylend 2011, Sec. 3 (test gratings; assumed for the array)
Waveguides500 nm wires; S-bends as long as half the spread of their level; bends of 10 µm radius in the fan-inassumed
Fan-in60 µm from the heaters to the emittersassumed

How it is written

Two cross-sections describe the waveguides: wire, a 500 nm core with a minimum bend radius of 10 µm, and heated, the same core with a heater strip 6.5 µm to its left. A phase shifter is then a single straight drawn with heated: the heater is part of the waveguide and cannot drift away from it.

The tree is the splitter tree of the Tour's last project, with S-bends as shapes. Each level places an MMI on every port of the previous level and an S-bend on each of its outputs. The S-bend's offset is computed from the port so that the outputs land spread / 2 above and below the MMI's axis, and the S-bend ends become the ports of the next level. After the last level the 16 ports are 100 µm apart.

The fan-in is a bundle of 16 routes. Every channel runs on from its heater, turns towards the axis, runs down (or up) to its emitter's height and turns back. The middle channels turn first and each further channel 3.5 µm later, so the routes nest inside each other without crossing. Each route is a path drawn with wire through four corners computed in the loop, with bend_radius turning the corners into bends.

gdsfactory builds the tree with bend_s and connect, and finds the fan-in by itself with route_bundle. Its separation is the gap between neighboring waveguides, so it is the pitch minus the width. With the emitters only as far from the heaters as the outermost route needs (44.5 µm), route_bundle crossed routes and reported a collision; with 60 µm it nests them as above, which is why all four versions use 60 µm. gdsfactory's section offsets are positive to the right of the direction of travel and Laylight's to the left, so the heater's offset changes sign. gdstk and KLayout compute the tree from the MMI's length and output pitch, draw each heater as a rectangle beside its wire, and compute the corners of the fan-in as Laylight does. Laylight's planned route statement will build such a bundle from the two lists of ports; see Auto Routing.

Implementations

ImplementationLines of codeTokensLayout
Laylight43412reference
gdsfactory60 (1.40×)512 (1.24×)same within 3 nm
gdstk51 (1.19×)553 (1.34×)same within 2 nm
KLayout58 (1.35×)668 (1.62×)same within 3 nm
laylight/phased_array.lyl
import "mmi.lyl"

# A 16-channel optical phased array after Doylend et al., Opt. Express 2011: a
# tree of 1x2 MMIs (mmi.lyl) spreads the input over channels `pitch` apart, a
# heater beside each channel tunes its phase, and the channels converge on an
# array of grating emitters. The wiring of the heaters is left out.

define xsection wire(width = 500nm) {
    si width
    with min_radius = 10um
}

# A wire with a heater strip `gap` to its left (center to center).
define xsection heated(width = 500nm, heater_width = 2um, gap = 6.5um) {
    si     width
    heater heater_width offset gap
}

# A grating emitter: shallow trenches across a wire, half a period wide.
cell Emitter(periods = 333, period = 600nm) {
    marker { input { at wg.start } }
    shape straight with periods * period, wire as wg.start at self.origin

    layer shallow {
        for i in 0..periods {
            shape rect with period / 2, wire.width as trench at ((i + 0.5) * period, 0)
        }
    }
}

cell PhasedArray(
    levels = 4,
    pitch = 100um,  # between the channels at the heaters
    heater_length = 470um,
    emitter_pitch = 3.5um,
    fan_length = 60um,  # from the heaters to the emitters
    radius = 10um,
) {
    # Each level splits every port in two, with S-bends to spread / 2 above
    # and below it.
    define spread = pitch * pow(2, levels - 1)
    define ports: [point] = [self.origin]
    for level in 0..levels {
        define next: [point] = []
        for port in ports {
            place MMI1x2 at port as mmi
            for k in 0..2 {
                define o = mmi.marker.output[k]
                define shift = port.y + (0.5 - k) * spread - o.y
                shape sbend with spread / 2, shift, wire as s.start at o
                next.append(s.end)
            }
        }
        ports = next
        spread = spread / 2
    }

    # After its heater, every channel turns towards the axis and back onto its
    # emitter; the inner channels turn first, so that no routes cross.
    define n = len(ports)
    define x_e = ports[0].x + heater_length + fan_length
    for i in 0..n {
        shape straight with heater_length, heated as tuner.start at ports[i]
        define p = tuner.end
        define x = p.x + radius + (abs(i - (n - 1) / 2) - 0.5) * emitter_pitch
        define y = ((n - 1) / 2 - i) * emitter_pitch
        define corners = [p, (x, p.y), (x, y), (x_e, y)]
        shape path with corners, wire, bend_radius = radius as fan at self.origin
        place Emitter at (x_e, y)
    }
}
gdsfactory/phased_array.py
"""A 16-channel optical phased array, as laylight/phased_array.lyl."""

import gdsfactory as gf

from mmi import mmi1x2
from tech import HEATER, SHALLOW, SI

WIRE = gf.cross_section.cross_section(width=0.5, layer=SI, radius_min=10)


@gf.cell
def emitter(periods=333, period=0.6):
    """A grating emitter: shallow trenches across a wire, half a period wide."""
    c = gf.Component()
    wg = c << gf.components.straight(periods * period, cross_section=WIRE)
    trench = gf.components.rectangle((period / 2, 0.5), layer=SHALLOW, centered=True)
    c.add_ref(trench, columns=periods, column_pitch=period).dmovex(period / 2)
    c.add_port("o1", port=wg.ports["o1"])
    return c


@gf.cell
def phased_array(
    levels=4,
    pitch=100,  # between the channels at the heaters
    heater_length=470,
    emitter_pitch=3.5,
    fan_length=60,  # from the heaters to the emitters
    radius=10,
):
    """A tree of 1x2 MMIs spreads the input over channels `pitch` apart, a
    heater beside each channel tunes its phase, and the channels converge on an
    array of grating emitters `emitter_pitch` apart."""
    c = gf.Component()
    heated = gf.cross_section.cross_section(
        width=0.5, layer=SI, sections=[gf.Section(width=2, offset=-6.5, layer=HEATER)]
    )
    spread = pitch * 2 ** (levels - 1)
    ports = [None]
    for _ in range(levels):
        next_ports = []
        for port in ports:
            mmi = c << mmi1x2()
            if port:
                mmi.connect("o1", port)
            for name, side in [("o2", 1), ("o3", -1)]:
                out = mmi.ports[name]
                shift = mmi.ports["o1"].y + side * spread / 2 - out.y
                bend = gf.components.bend_s((spread / 2, shift), 2000, WIRE)
                s = c << bend
                s.connect("o1", out)
                next_ports.append(s.ports["o2"])
        ports = next_ports
        spread /= 2
    n = len(ports)
    x_e = ports[0].x + heater_length + fan_length
    tuner = gf.components.straight(heater_length, cross_section=heated)
    ends, inputs = [], []
    for i, port in enumerate(ports):
        t = c << tuner
        t.connect("o1", port)
        e = c << emitter()
        e.dmove((x_e, ((n - 1) / 2 - i) * emitter_pitch))
        ends.append(t.ports["o2"])
        inputs.append(e.ports["o1"])
    gf.routing.route_bundle(
        c,
        ends,
        inputs,
        WIRE,
        bend="bend_circular",
        radius=radius,
        separation=emitter_pitch - 0.5,
    )
    return c


if __name__ == "__main__":
    import sys

    phased_array().write_gds(sys.argv[1])
gdstk/phased_array.py
"""A 16-channel optical phased array, as laylight/phased_array.lyl."""

import gdstk

from mmi import mmi1x2
from tech import HEATER, SHALLOW, SI

WIDTH = 0.5
MMI = mmi1x2()  # its outputs are 0.9 um above and below its axis


def emitter(periods=333, period=0.6):
    """A grating emitter: shallow trenches across a wire, half a period wide."""
    cell = gdstk.Cell("Emitter")
    cell.add(gdstk.rectangle((0, -WIDTH / 2), (periods * period, WIDTH / 2), **SI))
    for i in range(periods):
        x = (i + 0.25) * period
        trench = gdstk.rectangle(
            (x, -WIDTH / 2), (x + period / 2, WIDTH / 2), **SHALLOW
        )
        cell.add(trench)
    return cell


def phased_array(
    levels=4,
    pitch=100,  # between the channels at the heaters
    heater_length=470,
    emitter_pitch=3.5,
    fan_length=60,  # from the heaters to the emitters
    radius=10,
):
    """A tree of 1x2 MMIs spreads the input over channels `pitch` apart, a
    heater beside each channel tunes its phase, and the channels converge on an
    array of grating emitters `emitter_pitch` apart."""
    cell = gdstk.Cell("PhasedArray")
    length = MMI.bounding_box()[1][0]
    spread = pitch * 2 ** (levels - 1)
    x, ys = 0, [0]
    for _ in range(levels):
        next_ys = []
        for y in ys:
            cell.add(gdstk.Reference(MMI, (x, y)))
            for side in [1, -1]:
                h = side * (spread / 2 - 0.9)
                s = gdstk.FlexPath(
                    (x + length, y + side * 0.9), WIDTH, tolerance=1e-3, **SI
                )
                control = [(spread / 4, 0), (spread / 4, h), (spread / 2, h)]
                cell.add(s.bezier(control, relative=True))
                next_ys.append(y + side * spread / 2)
        x += length + spread / 2
        ys, spread = next_ys, spread / 2
    n = len(ys)
    x_h, x_e = x + heater_length, x + heater_length + fan_length
    grating = emitter()
    for i, y in enumerate(ys):
        cell.add(gdstk.rectangle((x, y - WIDTH / 2), (x_h, y + WIDTH / 2), **SI))
        cell.add(gdstk.rectangle((x, y + 5.5), (x_h, y + 7.5), **HEATER))
        y_e = ((n - 1) / 2 - i) * emitter_pitch
        corner = x_h + radius + (abs(i - (n - 1) / 2) - 0.5) * emitter_pitch
        points = [(x_h, y), (corner, y), (corner, y_e), (x_e, y_e)]
        cell.add(
            gdstk.FlexPath(points, WIDTH, bend_radius=radius, tolerance=1e-3, **SI)
        )
        cell.add(gdstk.Reference(grating, (x_e, y_e)))
    return cell


if __name__ == "__main__":
    import sys

    top = phased_array()
    lib = gdstk.Library()
    lib.add(top, *top.dependencies(True))
    lib.write_gds(sys.argv[1])
klayout/phased_array.py
"""A 16-channel optical phased array, as laylight/phased_array.lyl."""

import klayout.db as kdb
import numpy as np

from mmi import mmi1x2
from tech import HEATER, SHALLOW, SI, layout
from waveguides import path

WIDTH = 0.5


def sbend(length, offset, n=2000):
    """The centerline of an S-bend from (0, 0) towards +x to (length, offset):
    a cubic Bezier curve with its control points halfway along."""
    u = np.linspace(0, 1, n)[:, None]
    control = np.array(
        [(0, 0), (length / 2, 0), (length / 2, offset), (length, offset)]
    )
    weights = np.hstack([(1 - u) ** 3, 3 * u * (1 - u) ** 2, 3 * u**2 * (1 - u), u**3])
    return weights @ control


def emitter(periods=333, period=0.6):
    """A grating emitter: shallow trenches across a wire, half a period wide."""
    cell = layout.create_cell("Emitter")
    cell.shapes(layout.layer(SI)).insert(
        kdb.DBox(0, -WIDTH / 2, periods * period, WIDTH / 2)
    )
    shallow = cell.shapes(layout.layer(SHALLOW))
    for i in range(periods):
        x = (i + 0.25) * period
        shallow.insert(kdb.DBox(x, -WIDTH / 2, x + period / 2, WIDTH / 2))
    return cell


def phased_array(
    levels=4,
    pitch=100,  # between the channels at the heaters
    heater_length=470,
    emitter_pitch=3.5,
    fan_length=60,  # from the heaters to the emitters
    radius=10,
):
    """A tree of 1x2 MMIs spreads the input over channels `pitch` apart, a
    heater beside each channel tunes its phase, and the channels converge on an
    array of grating emitters `emitter_pitch` apart."""
    cell = layout.create_cell("PhasedArray")
    si = cell.shapes(layout.layer(SI))
    heater = cell.shapes(layout.layer(HEATER))
    mmi = mmi1x2()  # its outputs are 0.9 um above and below its axis
    length = mmi.dbbox().right
    spread = pitch * 2 ** (levels - 1)
    x, ys = 0, [0]
    for _ in range(levels):
        next_ys = []
        for y in ys:
            cell.insert(kdb.DCellInstArray(mmi.cell_index(), kdb.DCplxTrans(x, y)))
            for side in [1, -1]:
                bend = sbend(spread / 2, side * (spread / 2 - 0.9))
                si.insert(path(bend + (x + length, y + side * 0.9), WIDTH))
                next_ys.append(y + side * spread / 2)
        x += length + spread / 2
        ys, spread = next_ys, spread / 2
    n = len(ys)
    x_h, x_e = x + heater_length, x + heater_length + fan_length
    grating = emitter()
    for i, y in enumerate(ys):
        si.insert(kdb.DBox(x, y - WIDTH / 2, x_h, y + WIDTH / 2))
        heater.insert(kdb.DBox(x, y + 5.5, x_h, y + 7.5))
        y_e = ((n - 1) / 2 - i) * emitter_pitch
        corner = x_h + radius + (abs(i - (n - 1) / 2) - 0.5) * emitter_pitch
        points = [(x_h, y), (corner, y), (corner, y_e), (x_e, y_e)]
        si.insert(path(points, WIDTH).round_corners(radius, 500, 0.0005))
        trans = kdb.DCplxTrans(x_e, y_e)
        cell.insert(kdb.DCellInstArray(grating.cell_index(), trans))
    return cell


if __name__ == "__main__":
    import sys

    phased_array()
    layout.write(sys.argv[1])

On this page