Micro-ring resonator on silicon-on-insulator (SOI). Compare with Lumerical example: our results appear more accurate (higher Q and no splitting artifact) and run faster
Parameters¶
import os
import numpy as np
import matplotlib.pyplot as plt
import gdsfactory as gf
from gdsfactory.technology import LogicalLayer, LayerLevel, LayerStack
import luminescent as lumi
# simulation folder
path = os.path.join("runs", "ring")
# 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, 201)
# horizontal dims
r = 3.1 # radius of ring
w_wg = 0.4 # width of waveguide
gap = 0.1 # gap between waveguide and ring
# vertical dims
h_wg = 0.18 # height of waveguide
def setup(path,r,gap,w_wg,h_wg):
# simulation parameters
nres = 6 # number of grid points per wavelength in material (not vacuum)
stop_decay = 0.003
courant = 0.95 # Courant factor for time step size, relative to max allowed by grid
subpixel_smoothing = 1
PML_decay = [.001,.1,.1]
tmax = 1000 # max time [periods]
shot_every = 10 # save and plot field every _ periods, involves slow disk write
views = [lumi.view("Hz", x=0, z=0)]
boundaries = ['PML', ('PEC', 'PML'), ('PMC', 'PML')] # z midplane PMC symmetry
# margins
source_offset = 2
xmargin = 1.5
ymargin = lateral_port_margin = 1
# vertical midplane symmetry as PMC boundary on z-
zmargin = height_port_margin = 1
zmin = 0
zmax = h_wg / 2 + zmargin
# materials
core = 'Si'
clad = 'SiO2'
background = clad
nSi = 3.5
nSiO2 = 1.5
materials = {
'Si': lumi.material(nSi),
'SiO2': lumi.material(nSiO2),
}
# layer stack
WG = (1, 0) # waveguide layer
BBOX = (10, 0) # bounding box layer
layer_stack = LayerStack(
layers={
'core': LayerLevel(
layer=LogicalLayer(layer=WG),
thickness=h_wg / 2,
zmin=0.0,
material=core,
mesh_order=1,
) })
## Geometry
"""
2 symmetries:
- mid waveguide height z PMC boundary
- ring frame y PEC boundary (implies fictious mirror source at port o2 on other side of ring. original S21 turns into S11 in our mirror setup assuming no back coupling)
"""
# make component in gdsfactory. alternatively, you can import .gds layout into gdsfactory or .stl bodies directly into ours
c = gf.Component()
b = c << gf.components.bend_circular180(radius=r, width=w_wg, layer=WG, allow_min_radius_violation=True)
b.rotate(90)
b.movex(r)
l = 2 * r + 2 * xmargin + w_wg
s = c << gf.components.straight(length=l - source_offset, width=w_wg)
s.move((-l / 2 + source_offset, w_wg + gap + r))
s1 = c << gf.components.straight(length=source_offset, width=w_wg)
s1.connect('o2', s.ports['o1'])
c.add_port('o1', port=s.ports['o1'])
c.add_port('o3', port=s.ports['o2'])
c << gf.components.bbox(component=c, layer=BBOX, top=ymargin)
c.plot()
bbox = c.bbox_np().tolist()
bbox[0].append(zmin)
bbox[1].append(zmax)
# source and modes setup
w_port = w_wg + 2 * lateral_port_margin
frames = [
lumi.plane_frame('mode', wls_mode_solver=[wavelength], start=[-w_port / 2, zmin], stop=[w_port / 2, zmax], boundaries=['PML', ['PMC', 'PML']], nmodes=1), # local xy basis
lumi.points_frame('trace', points=[[0,0]], time_domain=True),
]
sources = [lumi.gf_source('o1', c, layer_stack, source_offset, frame='mode', wl=wavelength, bandwidth=max(max(wavelengths) - min(wavelengths), wavelength / 4))]
monitors = [
lumi.gf_monitor('o1', c, layer_stack, frame='mode'),
lumi.gf_monitor('o3', c, layer_stack, frame='mode', mirrored=True),
lumi.gf_monitor('o1', c, layer_stack, frame='trace', name='u1')
]
lumi.mesh(path, c, layer_stack, materials, zmin, zmax)
lumi.setup(
path=path,
wl=wavelength,
wls=wavelengths,
materials=materials,
background=background,
boundaries=boundaries,
bbox=bbox,
#
frames=frames,
sources=sources,
monitors=monitors,
#
nres=nres,
courant=courant,
subpixel_smoothing=subpixel_smoothing,
tmax=tmax,
PML_decay=PML_decay,
#
views=views,
shot_every=shot_every,
stop_decay=stop_decay,
device='gpu',
)
setup(path,r,gap,w_wg,h_wg)lumi backend: ~/Luminescent/bin/lumi
showing modes for wavelength 1.55
mode Boundary conditions: 0A00


mode/1.55 mode 0 (neff=2.191+0.000j)
saving mode to runs/ring/modes/mode/1.55/0.npz
using simulation folder runs/ring


Solve¶
lumi.solve(path)running Luminescent FDTD 4.2.3
========================================
backend using simulation folder runs/ring
========================================
setting up simulation...
processing geometry bodies...
generating voxels...
subpixel smoothing: 0.309962 seconds (15.67 M allocations: 679.104 MiB, 9.23% gc time)
making sources o1 ...
making monitors o1 o3 u1 ...
========================================
run: runs/ring
processor: CPU
cpu threads: 16
nres: 6.0
courant: 0.95
subpixel_smoothing: 1
boundaries: [:PML :PML; :PEC :PML; :PMC :PML]
sources
o1: -4.8, 3.6, 0
monitors
o1: -2.8, 3.6, 0
o3: 4.8, 3.6, 0
u1: -2.8, 3.6, 0
original size: (133, 63, 10)
padded size: (171, 66, 13)
cell count: 146,718
step size: 43.0 steps/period
source duration: 3.93 periods
max time: 1,000 periods
max steps: 43,000
max load: 6,308,874,000 cell-steps
energy decay threshold: 0.003
========================================
running simulation...
accumulating dft field...
period 10, decay: 0.96, 0.17 seconds/period
period 20, decay: 0.93, 0.047 seconds/period
period 30, decay: 0.262, 0.046 seconds/period
period 40, decay: 0.18, 0.046 seconds/period
period 50, decay: 0.158, 0.045 seconds/period
period 60, decay: 0.104, 0.046 seconds/period
period 70, decay: 0.099, 0.045 seconds/period
period 80, decay: 0.085, 0.045 seconds/period
period 90, decay: 0.066, 0.045 seconds/period
period 100, decay: 0.064, 0.045 seconds/period
period 110, decay: 0.053, 0.045 seconds/period
period 120, decay: 0.048, 0.046 seconds/period
period 130, decay: 0.046, 0.045 seconds/period
period 140, decay: 0.038, 0.045 seconds/period
period 150, decay: 0.037, 0.045 seconds/period
period 160, decay: 0.035, 0.045 seconds/period
period 170, decay: 0.03, 0.046 seconds/period
period 180, decay: 0.029, 0.046 seconds/period
period 190, decay: 0.026, 0.045 seconds/period
period 200, decay: 0.024, 0.046 seconds/period
period 210, decay: 0.023, 0.045 seconds/period
period 220, decay: 0.021, 0.045 seconds/period
period 230, decay: 0.019, 0.046 seconds/period
period 240, decay: 0.019, 0.046 seconds/period
period 250, decay: 0.017, 0.045 seconds/period
period 260, decay: 0.017, 0.047 seconds/period
period 270, decay: 0.016, 0.046 seconds/period
period 280, decay: 0.014, 0.045 seconds/period
period 290, decay: 0.014, 0.046 seconds/period
period 300, decay: 0.013, 0.045 seconds/period
period 310, decay: 0.012, 0.046 seconds/period
period 320, decay: 0.012, 0.046 seconds/period
period 330, decay: 0.011, 0.046 seconds/period
period 340, decay: 0.011, 0.046 seconds/period
period 350, decay: 0.01, 0.045 seconds/period
period 360, decay: 0.01, 0.046 seconds/period
period 370, decay: 0.009, 0.045 seconds/period
period 380, decay: 0.009, 0.045 seconds/period
period 390, decay: 0.008, 0.045 seconds/period
period 400, decay: 0.008, 0.045 seconds/period
period 410, decay: 0.008, 0.046 seconds/period
period 420, decay: 0.007, 0.046 seconds/period
period 430, decay: 0.007, 0.046 seconds/period
period 440, decay: 0.007, 0.045 seconds/period
period 450, decay: 0.007, 0.046 seconds/period
period 460, decay: 0.006, 0.045 seconds/period
period 470, decay: 0.006, 0.046 seconds/period
period 480, decay: 0.006, 0.046 seconds/period
period 490, decay: 0.006, 0.046 seconds/period
period 500, decay: 0.006, 0.046 seconds/period
period 510, decay: 0.005, 0.046 seconds/period
period 520, decay: 0.005, 0.046 seconds/period
period 530, decay: 0.005, 0.046 seconds/period
period 540, decay: 0.005, 0.046 seconds/period
period 550, decay: 0.004, 0.046 seconds/period
period 560, decay: 0.004, 0.046 seconds/period
period 570, decay: 0.004, 0.046 seconds/period
period 580, decay: 0.004, 0.046 seconds/period
period 590, decay: 0.004, 0.045 seconds/period
period 600, decay: 0.004, 0.048 seconds/period
period 610, decay: 0.004, 0.048 seconds/period
period 620, decay: 0.004, 0.046 seconds/period
period 630, decay: 0.003, 0.046 seconds/period
period 640, decay: 0.003, 0.046 seconds/period
period 650, decay: 0.003, 0.045 seconds/period
period 660, decay: 0.003, 0.045 seconds/period
period 666.79 stop on energy decay
simulation completed.
Movie¶
lumi.movie(path)Video generated successfully at runs/ring/Hz_0_None_0.webp!
Loading...
Results¶
S parameters¶
sol = lumi.load(path)
o1=sol['monitors']['o1']
o3=sol['monitors']['o3']
S21 = o1['modes'][0]['+'] / o1['modes'][0]['-'] # drop port power, S21 aliased as S11 with mirrored source
S31 = o3['modes'][0]['+'] / o1['modes'][0]['-'] # through port power
plt.plot(wavelengths, np.abs(S21)**2, label="S21")
# plt.plot(wavelengths, S31, label="S31")
plt.xlabel("wavelength [um]")
plt.ylabel("power")
plt.legend()
plt.show()loading solution from runs/ring
may take a minute if simulation folder is remotely mounted

Resonances¶
from scipy.signal import find_peaks
dw=bandwidth/len(wavelengths)
I, props = find_peaks(np.abs(S21), height=0.4, distance=.01//dw)
peaks = wavelengths[I]
print(f'Peaks: {", ".join(f"{1000*x:.2f}nm" for x in peaks)}')Peaks: 1520.00nm, 1548.50nm, 1577.50nm
FSR¶
FSR = np.mean(np.diff(peaks))
print(f'FSR: {1000*FSR:.2f}nm')FSR: 28.75nm
Q factor (FWHM method)¶
i=len(I)//2
mid = I[i]
y=np.abs(S21)**2 # power: FWHM is the half-power (-3 dB) width
half = y[mid]/2
l = mid
while y[l] > half: l -= 1
wl_lo = np.interp(half, [y[l], y[l+1]], [wavelengths[l], wavelengths[l+1]])
r = mid
while y[r] > half: r += 1
wl_hi = np.interp(half, [y[r], y[r-1]], [wavelengths[r], wavelengths[r-1]]) # np.interp needs increasing y
FWHM = wl_hi - wl_lo
wl=wavelengths[mid]
Q=wl/FWHM
print(f'FWHM: {1000*FWHM:.2f}nm\nQ factor (FWHM method): {Q:.0f}')FWHM: 3.06nm
Q factor (FWHM method): 506
Early stopping (optional)¶
Can still get FSR and Q factor by stopping early. Saves time because high Q cavity takes long time to decay completely. Instead simulate for only long enough for resonances to settle into decay trend. Here, can set stop_decay=0.1
Stopping early in time domain convolves sinc blur in frequency domain. Can still extract resonances and FSR from blurred spectrum, but Q factor must be computed from time domain decay signal.
Q factor (time domain decay method)¶
t = np.array(sol['t'])
y = np.array(sol['monitors']['u1']['fields']['Ex'][0,:])
t = t[:len(y)]
# zero transients before t0, leaving only ring decay:
# the source pulse peaks at the monitor, then takes ~its duration (~4 periods here) to pass
t0 = t[np.argmax(np.abs(y))] + 4
i = np.searchsorted(t, t0)
y[:i] = 0
plt.plot(t, y)
plt.xlabel("t [period]")
plt.ylabel("u1 Ex (monitor basis of reference)")
plt.show()
import subprocess
# harmonic inversion of the ringdown (needs harminv: sudo apt install harminv)
# time is in periods of `wavelength`, so frequency f = wavelength / λ
fmin, fmax = wavelength / wavelengths[-1], wavelength / wavelengths[0]
dt = t[1] - t[0]
out = subprocess.run(["harminv", "-t", str(dt), f"{fmin}-{fmax}"],
input="\n".join(map(str, y[i:])), capture_output=True, text=True).stdout
# columns: frequency, decay constant, Q, |amplitude|, phase, error
for line in out.splitlines()[1:]:
f, decay, Q, amp, phase, err = map(float, line.split(","))
if fmin <= f <= fmax:
print(f"{1000 * wavelength / f:.2f}nm Q = {Q:.0f} |amp| = {amp:.2g} error = {err:.1e}")
1577.17nm Q = 457 |amp| = 0.00096 error = 1.0e-06
1548.37nm Q = 576 |amp| = 0.00047 error = 2.3e-06
1519.70nm Q = 619 |amp| = 0.00057 error = 2.0e-06