Examples

Mach-Zehnder Interferometers

A balanced and an unbalanced Mach-Zehnder interferometer from two MMI splitters, with delay arms widened by parabolic tapers.

Compiling…
MZIs from laylight/mzi.lyl, compiled in your browserOpen in playground

A Mach-Zehnder interferometer (MZI) splits light into two arms and recombines it. With equal arms (balanced), all wavelengths leave through the same output, and a phase shifter on one arm turns it into a switch or a modulator. With one arm longer by ΔL (unbalanced), the output varies periodically with wavelength, with a free spectral range of λ²/(n_g ΔL): this is the basic wavelength filter, and cascades of them form the WDM (de)multiplexers of Horst et al.

The example builds a parametric MZI from two 1×2 MMI splitters, the second one used backwards as combiner, and two arms. Each arm leaves the splitter with a 90° Euler bend, runs up through a delay section, turns over the top with two bends, comes down through a second delay section and bends back onto the axis. Following Horst et al., the 500 nm wire is widened to 1 µm in the delay sections by 10 µm parabolic tapers: wide waveguides are less sensitive to width variations, so placing all of ΔL in them reduces phase errors. The lower arm is the upper one mirrored, with its delay sections longer by ΔL/2 each. The top cell places a balanced MZI (ΔL = 0) and an unbalanced one with ΔL = 20.65 µm, a quarter of the paper's ΔL_Base (its 1-stage filters).

References

  • F. Horst et al., "Cascaded Mach-Zehnder wavelength filters in silicon photonics for low loss and flat pass-band WDM (de-)multiplexing," Optics Express 21(10), 11652–11658 (2013). doi:10.1364/OE.21.011652
  • 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
Wire width500 nmHorst 2013, Sec. 2
Delay sectionswidened to 1 µm by 10 µm parabolic tapersHorst 2013, Sec. 2.3 (taper law: width squared linear in length)
ΔL20.65 µm = ΔL_Base / 4, ΔL_Base = λ²/(2 δλ n_g) = 82.6 µmHorst 2013, Table 1; λ = 1.49 µm, δλ = 3.2 nm; n_g = 4.2 assumed
Wide sections of the upper arm20 µm in totalassumed
Bends90° Euler bends, minimum radius 10 µmassumed
Splitters1×2 MMISheng 2012

How it is written

The MZI is three cells connected by ports. A Widened cell is the delay section (parabolic taper, wide straight, taper back), an Arm places two of them between four Euler bends, and MZI places the splitter, connects an arm to each of its outputs (the lower one with mirror), and turns the second MMI around with rotate 180deg so that its output port lands on the end of the upper arm. No coordinate is computed anywhere: every position follows from the ports. gdsfactory's version does the same with ports and connect; gdstk and KLayout compute where the arms start and end from the MMI's size.

The paper uses directional couplers as splitters; the example uses the MMI of the previous page, which makes the balanced MZI the standard MMI-based interferometer.

Implementations

ImplementationLines of codeTokensLayout
Laylight39370reference
gdsfactory49 (1.26×)489 (1.32×)same within 3 nm
gdstk42 (1.08×)480 (1.30×)same within 2 nm
KLayout45 (1.15×)498 (1.35×)same within 3 nm
laylight/mzi.lyl
import "mmi.lyl"

# Mach-Zehnder interferometers after Horst et al., Opt. Express 2013: 500 nm
# wires widened to 1 um in the delay sections, between two MMIs (mmi.lyl), the
# second one used backwards as combiner.

define width = 500nm
define wide_width = 1um
define taper_length = 10um
define radius = 10um   # minimum radius of the Euler bends (assumed)

# A delay section: the wire widened to `wide_width` for `length`.
cell Widened(length = 10um) {
    marker {
        input  { at widen.start }
        output { at narrow.end }
    }

    layer si {
        shape taper with taper_length, width, wide_width, "parabolic" as widen
        place widen.start at self.origin
        shape rect with length, wide_width as wide.left_center at widen.end
        shape taper with taper_length, wide_width, width, "parabolic" as narrow
        place narrow.start at wide.right_center
    }
}

# An arm: Euler bends away from the axis and back, around two delay sections
# whose wide parts are `length` long together.
cell Arm(length = 20um) {
    marker {
        input  { at up.start }
        output { at back.end }
    }

    layer si {
        shape euler with min_radius = radius, width = width, angle = 90deg as up
        place up.start at self.origin
        shape euler with min_radius = radius, width = width, angle = -90deg as over
        place over.start at rise.marker.output[0]
        clone over as down.start at over.end
        clone up as back.start at fall.marker.output[0]
    }

    place Widened with length = length / 2 at up.end as rise
    place Widened with length = length / 2 at down.end as fall
}

# A Mach-Zehnder interferometer whose lower arm is `delta_length` longer (by
# default dL_base / 4 of Horst et al.); the wide sections of the upper arm are
# `length` long together.
cell MZI(delta_length = 20.65um, length = 20um) {
    marker {
        input  { at splitter.marker.input[0] }
        output { at combiner.marker.input[0] with angle = 0deg }
    }

    place MMI1x2 at self.origin as splitter
    place Arm with length = length at splitter.marker.output[0] as upper
    place Arm with length = length + delta_length at splitter.marker.output[1] mirror
    place MMI1x2.marker.output[1] at upper.marker.output[0] rotate 180deg as combiner
}

# A balanced and an unbalanced MZI side by side.
cell MZIs {
    place MZI with delta_length = 0um at self.origin
    place MZI at (200um, 0)
}
gdsfactory/mzi.py
"""Balanced and unbalanced Mach-Zehnder interferometers, as laylight/mzi.lyl."""

import gdsfactory as gf
import numpy as np

from mmi import mmi1x2
from tech import SI

WIDTH = 0.5
WIDE_WIDTH = 1
TAPER_LENGTH = 10
RADIUS = 10  # minimum radius of the Euler bends (assumed)


def parabolic(t, w1, w2):
    """A taper law: the square of the width changes linearly."""
    return np.sqrt(w1**2 + (w2**2 - w1**2) * t)


@gf.cell
def arm(length=20):
    """An arm: Euler bends away from the axis and back, around two delay
    sections in which the wire is widened to WIDE_WIDTH, for `length` in total."""
    wire = gf.cross_section.cross_section(width=WIDTH, layer=SI)
    wide = gf.cross_section.cross_section(width=WIDE_WIDTH, layer=SI)
    taper = gf.path.straight(TAPER_LENGTH, npoints=50)
    delay = [
        gf.path.extrude_transition(taper, gf.path.transition(wire, wide, parabolic)),
        gf.components.straight(length / 2, cross_section=wide),
        gf.path.extrude_transition(taper, gf.path.transition(wide, wire, parabolic)),
    ]
    left = gf.components.bend_euler(
        RADIUS, with_arc_floorplan=False, cross_section=wire
    )
    right = gf.components.bend_euler(
        RADIUS, angle=-90, with_arc_floorplan=False, cross_section=wire
    )
    c = gf.Component()
    refs = [c << part for part in [left, *delay, right, right, *delay, left]]
    for previous, ref in zip(refs, refs[1:]):
        ref.connect("o1", previous.ports["o2"])
    c.add_port("o1", port=refs[0].ports["o1"])
    c.add_port("o2", port=refs[-1].ports["o2"])
    return c


@gf.cell
def mzi(delta_length=20.65, length=20):
    """A Mach-Zehnder interferometer whose lower arm is `delta_length` longer
    (by default dL_base / 4 of Horst et al.); `length` is the total length of
    the wide sections of the upper arm."""
    c = gf.Component()
    splitter = c << mmi1x2()
    upper = c << arm(length)
    upper.connect("o1", splitter.ports["o2"])
    lower = c << arm(length + delta_length)
    lower.connect("o1", splitter.ports["o3"], mirror=True)
    combiner = c << mmi1x2()
    combiner.connect("o3", upper.ports["o2"])
    c.add_port("o1", port=splitter.ports["o1"])
    c.add_port("o2", port=combiner.ports["o1"])
    return c


@gf.cell
def mzis():
    """A balanced and an unbalanced MZI side by side."""
    c = gf.Component()
    c << mzi(delta_length=0)
    (c << mzi()).dmovex(200)
    return c


if __name__ == "__main__":
    import sys

    mzis().write_gds(sys.argv[1])
gdstk/mzi.py
"""Balanced and unbalanced Mach-Zehnder interferometers, as laylight/mzi.lyl."""

from functools import cache

import gdstk
import numpy as np

from mmi import mmi1x2
from tech import SI
from waveguides import euler

WIDTH = 0.5
WIDE_WIDTH = 1
TAPER_LENGTH = 10
RADIUS = 10  # minimum radius of the Euler bends (assumed)
MMI = mmi1x2()  # one cell for all splitters and combiners


@cache
def arm(length=20):
    """An arm from (0, 0) towards +x: Euler bends away from the axis and back,
    around two delay sections in which the wire is widened to WIDE_WIDTH by
    parabolic tapers, for `length` in total."""
    bend = euler(RADIUS, 90)
    a = bend[-1, 0]  # a bend advances by a along x and y
    y = np.linspace(0, TAPER_LENGTH, 50)
    w = np.sqrt(WIDTH**2 + (WIDE_WIDTH**2 - WIDTH**2) * y / TAPER_LENGTH) / 2
    edge = np.column_stack([np.r_[w, w[::-1]], np.r_[y, y + TAPER_LENGTH + length / 2]])
    delay = np.r_[edge, edge[::-1] * (-1, 1)]
    top = a + 2 * TAPER_LENGTH + length / 2  # where the upper bend starts
    half = [
        gdstk.FlexPath(bend, WIDTH, tolerance=1e-3, **SI),
        gdstk.Polygon(delay + (a, a), **SI),
        gdstk.FlexPath(bend[:, ::-1] + (a, top), WIDTH, tolerance=1e-3, **SI),
    ]
    cell = gdstk.Cell(f"Arm_{length}")
    cell.add(*half, *[shape.copy().mirror((2 * a, 0), (2 * a, 1)) for shape in half])
    return cell


def mzi(delta_length=20.65, length=20):
    """A Mach-Zehnder interferometer whose lower arm is `delta_length` longer
    (by default dL_base / 4 of Horst et al.); `length` is the total length of
    the wide sections of the upper arm."""
    upper, lower = arm(length), arm(length + delta_length)
    x, y = MMI.bounding_box()[1][0], 0.9  # the outputs of MMI1x2 (pitch 1.8)
    end = x + upper.bounding_box()[1][0]
    cell = gdstk.Cell(f"MZI_{delta_length}")
    cell.add(
        gdstk.Reference(MMI),
        gdstk.Reference(upper, (x, y)),
        gdstk.Reference(lower, (x, -y), x_reflection=True),
        gdstk.Reference(MMI, (end + x, 0), rotation=np.pi),
    )
    return cell


def mzis():
    """A balanced and an unbalanced MZI side by side."""
    cell = gdstk.Cell("MZIs")
    cell.add(gdstk.Reference(mzi(delta_length=0)), gdstk.Reference(mzi(), (200, 0)))
    return cell


if __name__ == "__main__":
    import sys

    top = mzis()
    lib = gdstk.Library()
    lib.add(top, *top.dependencies(True))
    lib.write_gds(sys.argv[1])
klayout/mzi.py
"""Balanced and unbalanced Mach-Zehnder interferometers, as laylight/mzi.lyl."""

import klayout.db as kdb
import numpy as np

from mmi import mmi1x2
from tech import SI, layout
from waveguides import euler, path, polygon

WIDTH = 0.5
WIDE_WIDTH = 1
TAPER_LENGTH = 10
RADIUS = 10  # minimum radius of the Euler bends (assumed)


def arm(length=20):
    """An arm from (0, 0) towards +x: Euler bends away from the axis and back,
    around two delay sections in which the wire is widened to WIDE_WIDTH by
    parabolic tapers, for `length` in total."""
    bend = euler(RADIUS, 90)
    a = bend[-1, 0]  # a bend advances by a along x and y
    y = np.linspace(0, TAPER_LENGTH, 50)
    w = np.sqrt(WIDTH**2 + (WIDE_WIDTH**2 - WIDTH**2) * y / TAPER_LENGTH) / 2
    edge = np.column_stack([np.r_[w, w[::-1]], np.r_[y, y + TAPER_LENGTH + length / 2]])
    delay = np.r_[edge, edge[::-1] * (-1, 1)]
    top = a + 2 * TAPER_LENGTH + length / 2  # where the upper bend starts
    half = [
        path(bend, WIDTH),
        polygon(delay + (a, a)),
        path(bend[:, ::-1] + (a, top), WIDTH),
    ]
    cell = layout.create_cell("Arm")
    si = cell.shapes(layout.layer(SI))
    for shape in half:
        si.insert(shape)
        si.insert(shape.transformed(kdb.DCplxTrans(1, 180, True, 4 * a, 0)))
    return cell


def mzi(delta_length=20.65, length=20):
    """A Mach-Zehnder interferometer whose lower arm is `delta_length` longer
    (by default dL_base / 4 of Horst et al.); `length` is the total length of
    the wide sections of the upper arm."""
    cell = layout.create_cell("MZI")
    mmi = mmi1x2()
    upper, lower = arm(length), arm(length + delta_length)
    x, y = mmi.dbbox().right, 0.9  # the outputs of MMI1x2 (pitch 1.8)
    end = x + upper.dbbox().right
    for child, trans in [
        (mmi, kdb.DCplxTrans()),
        (upper, kdb.DCplxTrans(x, y)),
        (lower, kdb.DCplxTrans(1, 0, True, x, -y)),
        (mmi, kdb.DCplxTrans(1, 180, False, end + x, 0)),
    ]:
        cell.insert(kdb.DCellInstArray(child.cell_index(), trans))
    return cell


def mzis():
    """A balanced and an unbalanced MZI side by side."""
    cell = layout.create_cell("MZIs")
    for child, x in [(mzi(delta_length=0), 0), (mzi(), 200)]:
        cell.insert(kdb.DCellInstArray(child.cell_index(), kdb.DCplxTrans(x, 0)))
    return cell


if __name__ == "__main__":
    import sys

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

On this page