Skip to content

Optimal 90-degree waveguide bend

A photonic chip routes light in waveguides, narrow strips of silicon that hold light in by total internal reflection, as an optical fibre does. Light travels along a waveguide as a mode, a field pattern that keeps its shape; the simplest, TE0, has its electric field across the strip. Every layout turns its waveguides, and a 90-degree bend joins two guides at right angles. The usual bend is a circular arc, but its curvature jumps from zero to one over the radius at each end: there the straight guide's mode does not fit the bend, and part of the light is lost, more as the bend shrinks. An optimal bend lets its curvature grow smoothly from zero at each end to a peak in the middle, shaped so that it radiates the least for its footprint, the distance the turn takes up. The figure of merit is the loss per bend in decibels (dB), a logarithmic scale on which 0.01 dB is about 0.2 % of the power; bends a few micrometres across lose a few thousandths to a few hundredths of a dB.

This notebook builds the bend of Bahadori, Nikdast, Cheng and Bergman, J. Lightwave Technol. 37, 3044 (2019) on a 400 x 220 nm silicon strip with a 5 µm footprint, beside a circular bend of the same footprint: 0.0017 dB per bend at 1550 nm against the paper's 0.0019 dB, and 0.0117 dB for the circular bend against 0.0125 dB.

Parameters

The design numbers are the paper's; the shape needs only the exponent b of its bend-loss fit. The band is the paper's C-band. A uniform mesh gives the circular bend the same cells. The leads, five mode widths, run on through the PML, the absorbing layers at the walls; the clearances keep five decay lengths of the mode's tail. TE0 is even about the slab's mid-plane, so a mirror plane there halves the run. A loss this small needs the field to decay to 1e-9.

import matplotlib.pyplot as plt
import numpy as np

import photonhub as ph
# design, the paper's symbols (Sec. II and III)
w = 0.40  # strip width (µm)
h = 0.22  # silicon thickness (µm)
R0 = 5.0  # footprint: the guide axes are R0 apart (µm)
b = 2.49  # exponent of the loss fit a R^-b (Sec. II)
R0_sweep = (3.0, 4.0, 5.0, 8.0)  # footprints of the sweep (µm)
paper_optimal_db = 0.0019  # paper, stated 3D FDTD loss at R0 (dB)
paper_circular_db = 0.0125  # paper, stated circular loss at R0 (dB)
# simulation choices
wlen_min = 1.50  # band start (µm)
wlen_max = 1.60  # band end (µm)
n_wlen = 21  # wavelengths read out
cells_per_wlen = 12  # mesh cells per wavelength in silicon
lead = 2.0  # straight guide before each port (µm)
overshoot = 1.5  # leads drawn on past the ports (µm)
clearance = 0.8  # silicon edge to PML, in plane (µm)
clearance_z = 0.8  # core to PML, above and below (µm)
pml = 12  # PML layers
symmetry = (0, 0, 1)  # mirror plane through the slab centre
shutoff = 1e-9  # field decay that ends the run
dft_shutoff = 0.0  # 0 = no spectrum-settling check
n_path = 40000  # integration steps along the bend
keep = 200  # the outline keeps one step in this many

wlen = np.linspace(wlen_min, wlen_max, n_wlen)  # wavelengths (µm)
wlen0 = 0.5 * (wlen_min + wlen_max)  # band centre (µm)
f0 = ph.c0 / (wlen0 * 1e-6)  # band-centre frequency (Hz)

Geometry

The bend joins a guide along x to one along y whose axes are R0 apart. The paper's Eq. (15) and (17) set the curvature by the direction of travel θ: A sin(θ)^(1/b) up to 45 degrees and A cos(θ)^(1/b) after, zero at both ends; R0 fixes A. Integrating gives the centreline; each point moved half the width along the normal gives the sidewalls of a ph.Polygon, an outline in the chip plane extruded through the silicon, and ph.Structure makes it silicon. The circular bend follows a quarter circle. The sweep redraws both, so the outline is a function of the kind and the footprint.

def bend(kind, radius):
    theta = np.linspace(0.0, np.pi / 2, n_path + 1)  # direction of travel
    if kind == "circular":
        x, y = radius * np.sin(theta), radius * (1 - np.cos(theta))
    else:
        mid = 0.5 * (theta[1:] + theta[:-1])
        curv = np.where(mid <= np.pi / 4, np.sin(mid), np.cos(mid)) ** (1 / b)  # over A, Eq. (17)
        ds = np.diff(theta) / curv  # path length per step
        x = np.concatenate([[0.0], np.cumsum(np.cos(mid) * ds)])
        y = np.concatenate([[0.0], np.cumsum(np.sin(mid) * ds)])
        x, y = x * radius / x[-1], y * radius / x[-1]  # the scale A that fits the footprint
    x, y, theta = x[::keep], y[::keep], theta[::keep]
    x = np.concatenate([[-lead - overshoot], x, [radius]]) - radius / 2  # a lead in, one out
    y = np.concatenate([[0.0], y, [radius + lead + overshoot]]) - radius / 2
    theta = np.concatenate([[0.0], theta, [np.pi / 2]])
    left = list(zip(x - w / 2 * np.sin(theta), y + w / 2 * np.cos(theta)))
    right = list(zip(x + w / 2 * np.sin(theta), y - w / 2 * np.cos(theta)))
    shape = ph.Polygon(axis="z", vertices_um=right + left[::-1], slab_bounds_um=(-h / 2, h / 2))
    return [ph.Structure(geometry=shape, medium=ph.materials.Si)]

Simulation

A ph.Port is a plane across a guide: its centre, the axis the light travels along, and the guide's width and thickness. The simulation solves the TE0 mode there, launches it at in on the x guide and reads the modal power at out on the y guide. The SDK sizes the window the mode is read in so the mode is 30 dB down at its edge, with a 20 % margin.

ports = [
    ph.Port("in", (-lead - R0 / 2, -R0 / 2, 0.0), "x", w, thickness_um=h),
    ph.Port("out", (R0 / 2, R0 / 2 + lead, 0.0), "y", w, thickness_um=h),
]

ph.Simulation holds the setup. ph.Domain fits the box around the bend and continues each guide through the PML; ph.Mesh sets cells_per_wlen cells per wavelength in the silicon, uniform here, with subpixel averaging at the interfaces. The field monitor records the chip plane at the band centre.

field = ph.ProfileMonitor.plane(
    "field",
    "z",
    0.0,
    wlens_um=wlen0,
    fields=("Ex", "Ey", "Ez", "Hz"),  # Hz follows the light round the turn
)
sim = ph.Simulation(
    domain=ph.Domain(clearance_um=(clearance, clearance, clearance_z)),
    mesh=ph.Mesh(cells_per_wlen=cells_per_wlen, uniform=True),
    background=ph.materials.SiO2,  # oxide cladding
    pml_num_layers=pml,
    structures=bend("optimal", R0),
    symmetry=symmetry,
    wlens_um=wlen,  # wavelengths read out at the ports
    ports=ports,
    source="in",  # the port that launches TE0
    run=ph.RunSpec(shutoff=shutoff, dft_shutoff=dft_shutoff),
    monitors=[field],
)
sim.plot_index(z=0.0)
plt.show()

figure 1 of optimal-waveguide-bend

ph.run_local runs the solver on this computer; ph.cloud.run_quoted runs it on a cloud GPU.

data = ph.run_local(sim, quiet=True)
# On a cloud GPU instead, with GPU and SOLVER from your closed-test guide:
# data = ph.cloud.run_quoted(sim, device=GPU, solver=SOLVER)

Field

The mode follows the bend with no step at either end: the curvature starts at zero, so the straight guide's mode is already the local bend mode where they meet.

data.plot_field("field", field="E", z=0.0)
plt.show()

figure 2 of optimal-waveguide-bend

Result

The loss is −10 log₁₀ of the TE0 modal power at the output over the launched power, as the paper reports its 3D FDTD result. The circular bend of the same footprint runs on the same mesh. The dashed lines are the paper's stated values; its licence does not allow re-plotting its curves.

loss_db = -10 * np.log10(data.transmission("out"))  # optimal bend (dB)
circular = ph.run_local(sim.with_changes(structures=bend("circular", R0)), quiet=True)
loss_circular_db = -10 * np.log10(circular.transmission("out"))  # circular bend (dB)
ph.viz.plot_comparison(
    loss_db.wlen_um * 1e3,
    {"optimal bend": loss_db, "circular bend": loss_circular_db},
    stated=[
        (paper_optimal_db, "paper, stated (3D FDTD, optimal)"),
        (paper_circular_db, "paper, stated (3D FDTD, circular)"),
    ],
    ylabel="loss per 90-degree bend (dB)",
    ylim=(0, 0.025),
)
plt.show()
opt = float(loss_db.sel(f=f0, method="nearest"))
circ = float(loss_circular_db.sel(f=f0, method="nearest"))
print(f"optimal bend {opt:.4f} dB | circular bend {circ:.4f} dB | reduction {1 - opt / circ:.0%}")

figure 3 of optimal-waveguide-bend

output
optimal bend 0.0017 dB | circular bend 0.0117 dB | reduction 85%

At 12 cells per wavelength the optimal bend's loss reads 0.0017 dB at 1550 nm; converged, 0.0008 dB; the paper's 3D FDTD, 0.0019 dB. The circular bend reads 0.0117 dB; converged, 0.0118 dB; the paper's, 0.0125 dB. Both converged values subtract a straight guide read on the same mesh. The runs use a uniform mesh, subpixel averaging on, non-dispersive silicon and oxide, 12 PML layers and one mirror plane through the slab.

Sweep

The footprint is the knob. Each point is a full run of both bends at one R0, with the ports moved to its guides. At 8 µm the optimal bend reads little below 5 µm: at this mesh numerical scattering at the stepped sidewalls holds both up, so each is an upper bound. The squares are the paper's stated values at 5 µm.

loss_opt, loss_circ = [], []
for radius in R0_sweep:
    at_in = (-lead - radius / 2, -radius / 2, 0.0)  # the ports move with the guides
    at_out = (radius / 2, radius / 2 + lead, 0.0)
    moved = [
        ph.Port("in", at_in, "x", w, thickness_um=h),
        ph.Port("out", at_out, "y", w, thickness_um=h),
    ]
    for kind, loss in (("circular", loss_circ), ("optimal", loss_opt)):
        out = ph.run_local(sim.with_changes(structures=bend(kind, radius), ports=moved), quiet=True)
        loss.append(-10 * np.log10(float(out.transmission("out").sel(f=f0, method="nearest"))))
ph.viz.plot_comparison(
    np.array(R0_sweep),
    {"optimal bend": np.array(loss_opt), "circular bend": np.array(loss_circ)},
    stated=[
        (R0, paper_optimal_db, "paper, stated (3D FDTD, optimal)"),
        (R0, paper_circular_db, "paper, stated (3D FDTD, circular)"),
    ],
    xlabel="R0 (µm)",
    ylabel="loss per 90-degree bend (dB)",
)
plt.show()
pairs = zip(R0_sweep, loss_opt, loss_circ)
print(" | ".join(f"R0 {r:.0f}: {o:.4f} vs {c:.4f} dB" for r, o, c in pairs))

figure 4 of optimal-waveguide-bend

output
R0 3: 0.0117 vs 0.0449 dB | R0 4: 0.0024 vs 0.0200 dB | R0 5: 0.0017 vs 0.0117 dB | R0 8: 0.0015 vs 0.0059 dB