Photonic integrated circuit: ring resonator#
Overview#
We simulate small silicon ring resonator coupled to parallel waveguides buried in silica cladding. Compare with Lumerical example
Geometry#
import luminescent as lumi
from gdsfactory.technology import LogicalLayer, LayerLevel, LayerStack
import gdsfactory as gf
import numpy as np
import os
import matplotlib.pyplot as plt
# simulation folder
path = os.path.join("runs", "ring")
name='photonic ring resonator'
# length units are arbitrary so long as they are consistent, in this case [um]. everything gets normalized around origin wavelength and period at backend.
wavelength = 1.55 # characteristic wavelength [um]
bandwidth = 0.1 # wavelength bandwidth [um]
wavelengths = np.linspace(wavelength - bandwidth / 2, wavelength + bandwidth / 2, 401)
r = 3.1 # radius of ring
w_wg = 0.4 # width of waveguide
gap = 0.1 # gap between waveguide and ring
th = 0.18 # th of waveguide
# gds layers
WG = (1, 0) # waveguide layer
BBOX = (10, 0) # bounding box layer
# we create geometry in gdsfactory. alternatively, you can import .gds layout into gdsfactory or .stl bodies directly into ours
# margins
height_port_margin = lateral_port_margin = 0.8
margin = 1.25 * lateral_port_margin
zmargin = 1.25 * height_port_margin
source_port_margin = 1.0 * (w_wg + 2 * lateral_port_margin)
# draw layout in gdsfactory. `gf.components` defaults to layer WG = (1, 0)
c = gf.Component()
dut = c << gf.components.ring(radius=r, width=w_wg, layer=WG)
l_branch = 2 * r + w_wg + source_port_margin + 2*margin
xoffset = -source_port_margin - margin - r - w_wg / 2
yoffset = w_wg + gap + r
branch_top = c << gf.components.straight(length=l_branch, width=w_wg)
branch_top.move((xoffset, w_wg + gap + r))
branch_bottom_left = c << gf.components.straight(length=source_port_margin, width=w_wg)
branch_bottom_left.move((xoffset, -yoffset))
branch_bottom_right = c << gf.components.straight(length=l_branch - source_port_margin, width=w_wg)
branch_bottom_right.connect("o1", branch_bottom_left.ports["o2"])
# add ports
c.add_port("o1", port=branch_bottom_right.ports["o1"])
c.add_port("o2", port=branch_bottom_right.ports["o2"]) # thru channel
c.add_port("o3", port=branch_top.ports["o1"]) # drop channel
c << gf.components.bbox(component=c, layer=BBOX, top=margin, bottom=margin)
c.plot()
Solve#
# for gdsfactory, we need vertical layer stack. "core" layer is special as `height_port_margin` are demarcated from it for defining modal sources and monitors. during 3d meshing lower mesh order layers supplant higher mesh order layers
layer_stack = LayerStack(
layers={
"core": LayerLevel(
layer=LogicalLayer(layer=WG),
thickness=th,
zmin=0.0,
material="Si",
mesh_order=10,
),
}
)
# our default lumi.MATERIALS_LIBRARY lib covers limited range of photonic and PCB materials_library. here we create our own materials_library. `background` is special and tags regions outside any bodies during meshing
materials_library = {
"Si": lumi.Material(epsilon=12.25),
"SiO2": lumi.Material(epsilon=2.25),
}
materials_library["background"] = materials_library[
"SiO2"
]
# source launches `source_port_margin` in front of port. it's bidirectional: the other direction goes into PML. pulse spectrum rolls off gradually past bandwidth limits.
sources = [
lumi.Source(
"o1",
source_port_margin=source_port_margin,
wavelength=wavelength,
bandwidth=wavelength/5,
)
]
lx=w_wg + 2 * lateral_port_margin
ly=th + 2 * height_port_margin
modes = [lumi.Mode(
ports=[f'o{i}' for i in [1, 2, 3]], # ports that this mode applies to
wavelengths=lumi.chebyshev_nodes(wavelengths[0], wavelengths[-1]),
start=[-lx/2, -height_port_margin], # local xy frame, local y=0 at global zmin of port layer
stop=[lx/2, th + height_port_margin],
)]
lumi.make(
path=path,
component=c,
wavelengths=wavelengths,
sources=sources,
modes=modes,
# limits
zmin=-zmargin,
zmax=th + zmargin,
# materials and layers
materials_library=materials_library,
layer_stack=layer_stack,
# accuracy and speed settings
gpu="CUDA", # use GPU acceleration
nres=8, # number of grid points per wavelength in material (not vacuum)
relative_courant=0.95, # relative to maximum theoretical Courant number at
relative_pml_depths=[1, 1, 0.3], # relative PML depth
Tsim=500, # max time [periods]
field_decay_threshold=0.01, # field energy decay threshold for stopping simulation
# visualization
saveat=50, # save and plot field every n periods
views=[lumi.View("Hz", z="halfway", y=0, x=0)],
)
lumi.solve(path)
Visualize#
lumi.peek(path) # halfway and final snapshots
lumi.movie(path) # full movie - this may take a while to generate
Analysis#
sol = lumi.load(path)
x = wavelengths
y = lumi.query(sol, "T3,1") # wavelength or frequency ordered depending on problem setup
# y=np.abs2(sol['waves']['o3@0']/sol['waves']['o1@0'])
plt.plot(x, y)
plt.xlabel("wavelength [um]")
plt.ylabel("drop channel transmitted power")
plt.show()