Examples

Arrayed Waveguide Grating

A 12-channel, 400 GHz arrayed waveguide grating with star couplers in Rowland mounting and 40 arms.

Compiling…
AWG from laylight/awg.lyl, compiled in your browserOpen in playground

An arrayed waveguide grating (AWG) separates wavelengths. Light from the input spreads in a slab, the first star coupler, and enters an array of waveguides whose lengths increase in steps of ΔL. At the second star coupler the light from all arms interferes again, and since the phase step between neighbouring arms depends on the wavelength, each wavelength focuses on a different output waveguide. Pathak et al. studied how the number of arms, the aperture widths and the grating order trade off in silicon AWGs.

The example is their 12-channel, 400 GHz design with 40 arms: star couplers with a focal length of 120 µm, array apertures 2.01 µm wide and 100 nm apart, grating order 26 and ΔL = 14.96 µm. The input and output apertures lie on the Rowland circle, of half the focal length, and point at the far focal point; the array apertures lie on the grating circle and point away from the focal point. The arms run straight out of the input coupler, bend, run parallel to each other, and return symmetrically to the output coupler. Following Pathak et al., the arms are 800 nm wide in the straight sections, where all of ΔL lies, and 450 nm wide in the bends. The star couplers are tilted by 60° so that the arms do not cross, and each arm's straight sections are solved for its length.

The paper's star-coupler apertures are 70 nm shallow-etched; the example etches them fully. The bends of different arms turn by different angles, which changes their lengths slightly; the example does not compensate this, which only shifts the center wavelength.

References

  • S. Pathak, D. Van Thourhout and W. Bogaerts, "Design trade-offs for silicon-on-insulator-based AWGs for (de)multiplexer applications," Optics Letters 38(16), 2961–2964 (2013). doi:10.1364/OL.38.002961
  • S. Pathak et al., "Comparison of AWGs and echelle gratings for wavelength division multiplexing on silicon-on-insulator," IEEE Photonics Journal 6(5) (2014), for the arm widths. doi:10.1109/JPHOT.2014.2361658
  • S. Pathak et al., "Optimized silicon AWG with flattened spectral response using an MMI aperture," Journal of Lightwave Technology 31(1), 87–93 (2013), for the output apertures. doi:10.1109/JLT.2012.2231399

Parameters

ParameterValueSource
Channels12 × 400 GHzPathak 2013, Table 1
Focal length120 µmPathak 2013, Table 1
Arms, order, ΔL40, 26, 14.96 µmPathak 2013, Table 1
Array apertures2.01 µm, 100 nm apart (2.11 µm pitch)Pathak 2013, Table 1, Sec. 2
Arm widths800 nm straight, 450 nm in bendsPathak 2014
Input and output apertures2 µm, 25 µm tapers to 450 nm, 2.5 µm pitchPathak, JLT 2013 (pitch derived)
MountingRowlandassumed
Bends, tapers5 µm radius; 5 µm tapers between 800 and 450 nmassumed
Layoutcouplers tilted by 60°, focal points 430 µm apart, 210 µm of straights in the shortest armassumed

How it is written

A StarCoupler cell places the aperture tapers in two loops, each at a frame computed from the circle it lies on, and collects the corners of their wide ends from their anchors into the outline of the slab, so the slab always meets the apertures exactly. An Arm cell is half an arm: a straight, a taper, a bend, a taper and a straight chained by their anchors. The AWG cell places the two couplers, the second one turned by 180deg - tilt, and in a loop over the arms computes where each arm starts and how long its straight sections are, then places the arm and its mirror image. gdstk is shorter here: its star-coupler function returns the positions of the array apertures, which Laylight cannot yet export as a list of ports.

Pieces that meet at an angle are rounded to the 1 nm grid separately, so two waveguide pieces that should touch can end up a nanometer apart. All four versions therefore let each arm start 10 nm inside its aperture taper and its two halves overlap by 10 nm at the symmetry line, which leaves the Laylight and gdstk layouts free of gaps. The gdsfactory and KLayout versions still leave seams of up to 1 nm inside some arms, where gdsfactory snaps every piece it connects to the grid and KLayout's sampled bend ends with a face perpendicular to its last segment; they are within the comparison tolerance and do not change the shape, but a design-rule check would flag them.

Implementations

ImplementationLines of codeTokensLayout
Laylight76640reference
gdsfactory80 (1.05×)823 (1.29×)same within 5 nm
gdstk64 (0.84×)695 (1.09×)same within 3 nm
KLayout76 (1.00×)859 (1.34×)same within 3 nm
laylight/awg.lyl
import "tech.lyl"

# Pieces of an arm that meet at an angle overlap by this much, so that rounding
# the rotated pieces to the grid cannot open gaps between them.
define overlap = 10nm

# A star coupler in Rowland mounting, its focal point at the origin and its
# axis along +x. The input/output apertures lie on the Rowland circle and
# point at (radius, 0); the array apertures lie on the grating circle of
# `radius` and point away from the origin. The slab spans their wide ends.
cell StarCoupler(
    ports = 1,
    arms = 40,
    radius = 120um,
    port_pitch = 2.5um,
    array_pitch = 2.11um,
    port_width = 2um,
    array_width = 2.01um,
    wire_width = 450nm,
    arm_width = 800nm,
    taper_length = 25um,
) {
    layer si {
        define outline: [point] = []
        for i in 0..ports {
            define b = ((ports - 1) / 2 - i) * port_pitch / radius * 1rad
            define r = radius * sin(b)
            shape taper with taper_length, port_width, wire_width as port
            place port.start at (r * sin(b), r * cos(b), 180deg - b)
            outline.append(port.bottom_left)    # the corners of its wide end
            outline.append(port.top_left)
        }
        for j in 0..arms {
            define a = (j - (arms - 1) / 2) * array_pitch / radius * 1rad
            shape taper with taper_length, array_width, arm_width as aperture
            place aperture.start at (radius * cos(a), radius * sin(a), a)
            outline.append(aperture.bottom_left)
            outline.append(aperture.top_left)
        }
        shape polygon with outline as slab at self.origin
    }
}

# Half of an array arm, from the end of its aperture at the origin (heading
# +x) to the symmetry line of the AWG: a straight, a bend that turns right by
# `angle` and a straight, `width` wide but `bend_width` in the bend.
cell Arm(
    angle angle,
    radial size,  # from the aperture to the bend
    span size,  # from the bend to the end
    bend_radius = 5um,
    width = 800nm,
    bend_width = 450nm,
    transition = 5um,  # tapers to and from the bend
) {
    layer si {
        shape rect with radial - transition + overlap, width as first
        place first.left_center at (-overlap, 0)
        shape taper with transition, width, bend_width as narrow
        place narrow.start at first.right_center
        shape arc with bend_radius, bend_width, -angle as bend.start at narrow.end
        shape taper with transition, bend_width, width as widen.start at bend.end
        shape rect with span - transition, width as second.left_center at widen.end
    }
}

# A 12-channel, 400 GHz arrayed waveguide grating, after Pathak et al., Opt.
# Lett. 2013: two star couplers tilted towards each other, and arms that run
# straight out of the input coupler, bend to +x and return symmetrically.
cell AWG(
    channels = 12,
    arms = 40,
    radius = 120um,  # focal length R_a of the star couplers
    delta_length = 14.96um,  # for the grating order m = 26
    array_pitch = 2.11um,  # 2.01 um wide array apertures, 100 nm apart
    tilt = 60deg,  # axis of the input star coupler
    bend_radius = 5um,
    separation = 430um,  # between the focal points of the couplers
    length = 210um,  # straight sections of the shortest arm
) {
    place StarCoupler with {
        ports = 1, arms = arms, radius = radius, array_pitch = array_pitch
    } at self.origin rotate tilt as splitter
    place StarCoupler with {
        ports = channels, arms = arms, radius = radius, array_pitch = array_pitch
    } at (separation, 0) rotate 180deg - tilt

    # The straight sections of each half of arm j add up to `straight` and
    # cover `reach` along x; the output half mirrors the input half.
    place {
        define r = radius + splitter.taper_length    # to the ends of the apertures
        for j in 0..arms {
            define a = tilt + (j - (arms - 1) / 2) * array_pitch / radius * 1rad
            define straight = (length + j * delta_length) / 2
            define reach = separation / 2 - r * cos(a) - bend_radius * sin(a)
            define radial = (straight - reach) / (1 - cos(a))
            define span = straight - radial + overlap
            define start = (r * cos(a), r * sin(a), a)
            place Arm with {
                angle = a, radial = radial, span = span, bend_radius = bend_radius
            } at start
            place Arm with {
                angle = a, radial = radial, span = span, bend_radius = bend_radius
            } at (separation - start.x, start.y, 180deg - a) mirror
        }
    }
}
gdsfactory/awg.py
"""An arrayed waveguide grating, as laylight/awg.lyl."""

import gdsfactory as gf
import numpy as np

from tech import SI

# Pieces of an arm that meet at an angle overlap by this much, so that rounding
# the rotated pieces to the grid cannot open gaps between them.
OVERLAP = 0.01


@gf.cell(check_instances=False)  # instances at any angle
def star_coupler(
    ports=1,
    arms=40,
    radius=120,
    port_pitch=2.5,
    array_pitch=2.11,
    port_width=2,
    array_width=2.01,
    wire_width=0.45,
    arm_width=0.8,
    taper_length=25,
):
    """A star coupler in Rowland mounting, its focal point at the origin and its
    axis along +x. The input/output apertures lie on the Rowland circle and
    point at (radius, 0); the array apertures lie on the grating circle of
    `radius` and point away from the origin, with ports a0, a1, ... at their
    ends. The slab spans their wide ends."""
    c = gf.Component()
    outline = []

    def aperture(x, y, angle, w1, w2):
        """A taper from `w1` at (x, y) towards `angle` (radians) to `w2`."""
        taper = c << gf.components.taper(taper_length, w1, w2, layer=SI)
        taper.drotate(np.degrees(angle)).dmove((x, y))
        dx, dy = w1 / 2 * np.sin(angle), w1 / 2 * np.cos(angle)
        outline.extend([(x + dx, y - dy), (x - dx, y + dy)])  # its wide end
        return taper

    for i in range(ports):
        b = ((ports - 1) / 2 - i) * port_pitch / radius
        r = radius * np.sin(b)
        aperture(r * np.sin(b), r * np.cos(b), np.pi - b, port_width, wire_width)
    for j in range(arms):
        a = (j - (arms - 1) / 2) * array_pitch / radius
        taper = aperture(
            radius * np.cos(a), radius * np.sin(a), a, array_width, arm_width
        )
        c.add_port(f"a{j}", port=taper.ports["o2"])
    c.add_polygon(outline, layer=SI)
    return c


@gf.cell(check_instances=False)
def arm(angle, radial, span, bend_radius=5, width=0.8, bend_width=0.45, transition=5):
    """Half of an array arm, from the end of its aperture (port o1, heading +x)
    to the symmetry line of the AWG: a straight, a bend that turns right by
    `angle` and a straight, `width` wide but `bend_width` in the bend. `radial`
    runs from the aperture to the bend, `span` from the bend to the end;
    `transition` is the length of the tapers to and from the bend."""
    c = gf.Component()
    wide = gf.cross_section.cross_section(width=width, layer=SI)
    narrow = gf.cross_section.cross_section(width=bend_width, layer=SI)
    parts = [
        gf.components.straight(radial - transition + OVERLAP, cross_section=wide),
        gf.components.taper(transition, width, bend_width, layer=SI),
        gf.path.arc(bend_radius, -angle).extrude(narrow),
        gf.components.taper(transition, bend_width, width, layer=SI),
        gf.components.straight(span - transition, cross_section=wide),
    ]
    previous = c << parts[0]
    previous.dmovex(-OVERLAP)
    c.add_port("o1", center=(0, 0), width=width, orientation=180, layer=SI)
    for part in parts[1:]:
        ref = c << part
        ref.connect("o1", previous.ports["o2"])
        previous = ref
    return c


@gf.cell(check_instances=False)
def awg(
    channels=12,
    arms=40,
    radius=120,  # focal length R_a of the star couplers
    delta_length=14.96,  # for the grating order m = 26
    array_pitch=2.11,  # 2.01 um wide array apertures, 100 nm apart
    tilt=60,  # axis of the input star coupler
    bend_radius=5,
    separation=430,  # between the focal points of the couplers
    length=210,  # straight sections of the shortest arm
):
    """A 12-channel, 400 GHz arrayed waveguide grating, after Pathak et al.,
    Opt. Lett. 2013: two star couplers tilted towards each other, and arms that
    run straight out of the input coupler, bend to +x and return symmetrically."""
    c = gf.Component()
    splitter = c << star_coupler(1, arms, radius, array_pitch=array_pitch)
    splitter.drotate(tilt)
    combiner = c << star_coupler(channels, arms, radius, array_pitch=array_pitch)
    combiner.drotate(180 - tilt).dmove((separation, 0))
    # The straight sections of each half of arm j add up to `straight` and
    # cover `reach` along x; the output half mirrors the input half.
    for j in range(arms):
        port = splitter.ports[f"a{j}"]
        a = np.radians(port.orientation)
        straight = (length + j * delta_length) / 2
        reach = separation / 2 - port.dx - bend_radius * np.sin(a)
        radial = (straight - reach) / (1 - np.cos(a))
        half = arm(port.orientation, radial, straight - radial + OVERLAP, bend_radius)
        (c << half).connect("o1", port)
        (c << half).connect("o1", combiner.ports[f"a{arms - 1 - j}"], mirror=True)
    return c


if __name__ == "__main__":
    import sys

    awg().write_gds(sys.argv[1])
gdstk/awg.py
"""An arrayed waveguide grating, as laylight/awg.lyl."""

import gdstk
import numpy as np

from tech import SI
from waveguides import taper

# Pieces of an arm that meet at an angle overlap by this much, so that rounding
# the rotated pieces to the grid cannot open gaps between them.
OVERLAP = 0.01


def star_coupler(
    ports=1,
    arms=40,
    radius=120,
    port_pitch=2.5,
    array_pitch=2.11,
    port_width=2,
    array_width=2.01,
    wire_width=0.45,
    arm_width=0.8,
    taper_length=25,
):
    """A star coupler in Rowland mounting, its focal point at the origin and its
    axis along +x. The input/output apertures lie on the Rowland circle and
    point at (radius, 0); the array apertures lie on the grating circle of
    `radius` and point away from the origin. The slab spans their wide ends.
    Returns the cell and the ends of the array apertures (complex)."""
    cell = gdstk.Cell(f"StarCoupler_{ports}")
    outline, ends = [], []
    for i in range(ports):
        b = ((ports - 1) / 2 - i) * port_pitch / radius
        r = radius * np.sin(b)
        port = taper(taper_length, port_width, wire_width, SI)
        cell.add(port.rotate(np.pi - b).translate(r * np.sin(b), r * np.cos(b)))
        outline += [port.points[0], port.points[3]]  # its wide end
    for j in range(arms):
        a = (j - (arms - 1) / 2) * array_pitch / radius
        aperture = taper(taper_length, array_width, arm_width, SI)
        cell.add(aperture.rotate(a).translate(radius * np.cos(a), radius * np.sin(a)))
        outline += [aperture.points[0], aperture.points[3]]
        ends.append((radius + taper_length) * np.exp(1j * a))
    cell.add(gdstk.Polygon(outline, **SI))
    return cell, np.array(ends)


def arm(angle, radial, span, bend_radius=5, width=0.8, bend_width=0.45, transition=5):
    """Half of an array arm, from the end of its aperture at the origin (heading
    +x) to the symmetry line of the AWG: a straight, a bend that turns right by
    `angle` (radians) and a straight, `width` wide but `bend_width` in the
    bend. `radial` runs from the aperture to the bend, `span` from the bend to
    the end; `transition` is the length of the tapers to and from the bend."""
    path = gdstk.FlexPath((-OVERLAP, 0), width, tolerance=1e-3, **SI)
    path.horizontal(radial - transition).horizontal(radial, bend_width)
    path.turn(bend_radius, -angle)
    direction = np.exp(-1j * angle)
    path.segment(transition * direction, width, relative=True)
    return path.segment((span - transition) * direction, relative=True)


def awg(
    channels=12,
    arms=40,
    radius=120,  # focal length R_a of the star couplers
    delta_length=14.96,  # for the grating order m = 26
    array_pitch=2.11,  # 2.01 um wide array apertures, 100 nm apart
    tilt=60,  # axis of the input star coupler
    bend_radius=5,
    separation=430,  # between the focal points of the couplers
    length=210,  # straight sections of the shortest arm
):
    """A 12-channel, 400 GHz arrayed waveguide grating, after Pathak et al.,
    Opt. Lett. 2013: two star couplers tilted towards each other, and arms that
    run straight out of the input coupler, bend to +x and return symmetrically."""
    cell = gdstk.Cell("AWG")
    splitter, ends = star_coupler(1, arms, radius, array_pitch=array_pitch)
    combiner, _ = star_coupler(channels, arms, radius, array_pitch=array_pitch)
    t = np.radians(tilt)
    cell.add(gdstk.Reference(splitter, rotation=t))
    cell.add(gdstk.Reference(combiner, (separation, 0), rotation=np.pi - t))
    # The straight sections of each half of arm j add up to `straight` and
    # cover `reach` along x; the output half mirrors the input half.
    for j, start in enumerate(ends * np.exp(1j * t)):
        a = np.angle(start)
        straight = (length + j * delta_length) / 2
        reach = separation / 2 - start.real - bend_radius * np.sin(a)
        radial = (straight - reach) / (1 - np.cos(a))
        half = arm(a, radial, straight - radial + OVERLAP, bend_radius)
        half.rotate(a).translate(start.real, start.imag)
        cell.add(half, half.copy().mirror((separation / 2, 0), (separation / 2, 1)))
    return cell


if __name__ == "__main__":
    import sys

    top = awg()
    lib = gdstk.Library()
    lib.add(top, *top.dependencies(True))
    lib.write_gds(sys.argv[1])
klayout/awg.py
"""An arrayed waveguide grating, as laylight/awg.lyl."""

import klayout.db as kdb
import numpy as np

from tech import SI, layout
from waveguides import path, taper

# Pieces of an arm that meet at an angle overlap by this much, so that rounding
# the rotated pieces to the grid cannot open gaps between them.
OVERLAP = 0.01


def star_coupler(
    ports=1,
    arms=40,
    radius=120,
    port_pitch=2.5,
    array_pitch=2.11,
    port_width=2,
    array_width=2.01,
    wire_width=0.45,
    arm_width=0.8,
    taper_length=25,
):
    """A star coupler in Rowland mounting, its focal point at the origin and its
    axis along +x. The input/output apertures lie on the Rowland circle and
    point at (radius, 0); the array apertures lie on the grating circle of
    `radius` and point away from the origin. The slab spans their wide ends.
    Returns the cell and the ends of the array apertures (as transformations)."""
    cell = layout.create_cell("StarCoupler")
    si = cell.shapes(layout.layer(SI))
    apertures = []  # placement, wide and narrow width
    for i in range(ports):
        b = ((ports - 1) / 2 - i) * port_pitch / radius
        r = radius * np.sin(b)
        t = kdb.DCplxTrans(1, 180 - np.degrees(b), False, r * np.sin(b), r * np.cos(b))
        apertures.append((t, port_width, wire_width))
    for j in range(arms):
        a = (j - (arms - 1) / 2) * array_pitch / radius
        x, y = radius * np.cos(a), radius * np.sin(a)
        t = kdb.DCplxTrans(1, np.degrees(a), False, x, y)
        apertures.append((t, array_width, arm_width))
    outline = []
    for t, w1, w2 in apertures:
        si.insert(taper(taper_length, w1, w2).transformed(t))
        outline += [t * kdb.DPoint(0, -w1 / 2), t * kdb.DPoint(0, w1 / 2)]
    si.insert(kdb.DPolygon(outline))
    return cell, [t * kdb.DCplxTrans(taper_length, 0) for t, _, _ in apertures[ports:]]


def arm(angle, radial, span, bend_radius=5, width=0.8, bend_width=0.45, transition=5):
    """Half of an array arm, from the end of its aperture at the origin (heading
    +x) to the symmetry line of the AWG: a straight, a bend that turns right by
    `angle` and a straight, `width` wide but `bend_width` in the bend. `radial`
    runs from the aperture to the bend, `span` from the bend to the end;
    `transition` is the length of the tapers to and from the bend."""
    cell = layout.create_cell("Arm")
    si = cell.shapes(layout.layer(SI))
    x = radial - transition
    si.insert(path([(-OVERLAP, 0), (x, 0)], width))
    si.insert(taper(transition, width, bend_width).moved(x, 0))
    phi = np.radians(np.linspace(0, angle, 200))
    bend = bend_radius * np.column_stack([np.sin(phi), np.cos(phi) - 1]) + (radial, 0)
    si.insert(path(bend, bend_width))
    t = kdb.DCplxTrans(1, -angle, False, *bend[-1])
    si.insert(taper(transition, bend_width, width).transformed(t))
    straight = kdb.DBox(transition, -width / 2, span, width / 2)
    si.insert(kdb.DPolygon(straight).transformed(t))
    return cell


def awg(
    channels=12,
    arms=40,
    radius=120,  # focal length R_a of the star couplers
    delta_length=14.96,  # for the grating order m = 26
    array_pitch=2.11,  # 2.01 um wide array apertures, 100 nm apart
    tilt=60,  # axis of the input star coupler
    bend_radius=5,
    separation=430,  # between the focal points of the couplers
    length=210,  # straight sections of the shortest arm
):
    """A 12-channel, 400 GHz arrayed waveguide grating, after Pathak et al.,
    Opt. Lett. 2013: two star couplers tilted towards each other, and arms that
    run straight out of the input coupler, bend to +x and return symmetrically."""
    cell = layout.create_cell("AWG")
    splitter, ends = star_coupler(1, arms, radius, array_pitch=array_pitch)
    combiner, _ = star_coupler(channels, arms, radius, array_pitch=array_pitch)
    rotation = kdb.DCplxTrans(1, tilt, False, 0, 0)
    flip = kdb.DCplxTrans(1, 180, True, separation, 0)  # mirror at the symmetry line
    cell.insert(kdb.DCellInstArray(splitter.cell_index(), rotation))
    cell.insert(kdb.DCellInstArray(combiner.cell_index(), flip * rotation))
    # The straight sections of each half of arm j add up to `straight` and
    # cover `reach` along x; the output half mirrors the input half.
    for j, end in enumerate(ends):
        start = rotation * end
        a = np.radians(start.angle)
        straight = (length + j * delta_length) / 2
        reach = separation / 2 - start.disp.x - bend_radius * np.sin(a)
        radial = (straight - reach) / (1 - np.cos(a))
        half = arm(start.angle, radial, straight - radial + OVERLAP, bend_radius)
        for trans in [start, flip * start]:
            cell.insert(kdb.DCellInstArray(half.cell_index(), trans))
    return cell


if __name__ == "__main__":
    import sys

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

On this page