Arrayed Waveguide Grating
A 12-channel, 400 GHz arrayed waveguide grating with star couplers in Rowland mounting and 40 arms.
AWG from laylight/awg.lyl, compiled in your browserOpen in playgroundAn 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
| Parameter | Value | Source |
|---|---|---|
| Channels | 12 × 400 GHz | Pathak 2013, Table 1 |
| Focal length | 120 µm | Pathak 2013, Table 1 |
| Arms, order, ΔL | 40, 26, 14.96 µm | Pathak 2013, Table 1 |
| Array apertures | 2.01 µm, 100 nm apart (2.11 µm pitch) | Pathak 2013, Table 1, Sec. 2 |
| Arm widths | 800 nm straight, 450 nm in bends | Pathak 2014 |
| Input and output apertures | 2 µm, 25 µm tapers to 450 nm, 2.5 µm pitch | Pathak, JLT 2013 (pitch derived) |
| Mounting | Rowland | assumed |
| Bends, tapers | 5 µm radius; 5 µm tapers between 800 and 450 nm | assumed |
| Layout | couplers tilted by 60°, focal points 430 µm apart, 210 µm of straights in the shortest arm | assumed |
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
| Implementation | Lines of code | Tokens | Layout |
|---|---|---|---|
| Laylight | 76 | 640 | reference |
| gdsfactory | 80 (1.05×) | 823 (1.29×) | same within 5 nm |
| gdstk | 64 (0.84×) | 695 (1.09×) | same within 3 nm |
| KLayout | 76 (1.00×) | 859 (1.34×) | same within 3 nm |
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
}
}
}
"""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])
"""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])
"""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])