2D ring resonator

2D ring resonator#

View this project in Tidy3D Web App.

This is a simple example of using Tidy3D to perform a 2D simulation of a ring resonator side coupled to a dielectric waveguide.

diagram

With a center wavelength of 500 nm and 10 nm resolution, this is a challenging FDTD problem because of the large simulation size and the large number of time steps needed to capture the resonance of the ring. However, it can be easily handled with Tidy3D.

For more integrated photonic examples such as the 8-Channel mode and polarization de-multiplexer, the broadband bi-level taper polarization rotator-splitter, and the broadband directional coupler, please visit our examples page. If you are interested in ring resonator simulation in 3D, please check out the THz filter case study.

If you are new to the finite-difference time-domain (FDTD) method, we highly recommend going through our FDTD101 tutorials. FDTD simulations can diverge due to various reasons. If you run into any simulation divergence issues, please follow the steps outlined in our troubleshooting guide to resolve it.

[1]:
# standard python imports
import numpy as np
from numpy import random
import matplotlib.pyplot as plt

# tidy3D import
import tidy3d.web as web
import tidy3d as td

Initial setup#

Our ring resonator will include a ring centered at (0,0) with a waveguide just above the ring spanning the x direction.

schematic

[2]:
# define geometry
wg_width = 0.25
couple_width = 0.13
ring_radius = 2.5
ring_wg_width = 0.25
wg_spacing = 0.5
buffer = 1.0

# compute quantities based on geometry parameters
x_span = 2 * wg_spacing + 2 * ring_radius + 2 * buffer
y_span = 2 * ring_radius + ring_wg_width + wg_width + couple_width + 2 * buffer
sim_center_y = (couple_width + wg_width) / 2
wg_insert_x = ring_radius + wg_spacing
wg_center_y = ring_radius + ring_wg_width / 2.0 + couple_width + wg_width / 2.0

Define frequency parameters for sources and monitors based on the wavelength range of interest. Since in this case study we are investigating the system’s response over a wide range of frequencies we will use a broadband mode source that takes into account frequency-dependent character of waveguide modes. The Tidy3D’s broadband feature is designed in such a way that it produces the most accurate results in the range (freq0 - 1.5 * fwidth, freq0 + 1.5 * fwidth), thus we define parameters freq0 and fwidth such that the frequency range of interest is exactly covered by (freq0 - 1.5 * fwidth, freq0 + 1.5 * fwidth).

[3]:
# wavelength range of interest
lambda_beg = 0.5
lambda_end = 0.6

# define pulse parameters
freq_beg = td.C_0 / lambda_end
freq_end = td.C_0 / lambda_beg
freq0 = (freq_beg + freq_end) / 2
fwidth = (freq_end - freq0) / 1.5

min_steps_per_wvl = 30
run_time = 15e-12

Define materials.

[4]:
n_bg = 1.0
n_solid = 1.5
background = td.Medium(permittivity=n_bg**2)
solid = td.Medium(permittivity=n_solid**2)

Define structures.

[5]:
# waveguide
waveguide = td.Structure(
    geometry=td.Box(
        center=[0, wg_center_y, 0],
        size=[td.inf, wg_width, td.inf],
    ),
    medium=solid,
    name="waveguide",
)

# outer ring boundary
outer = td.Cylinder(
    center=[0, 0, 0],
    axis=2,
    radius=ring_radius + ring_wg_width / 2.0,
    length=td.inf,
)

# inner ring boundary
inner = td.Cylinder(
    center=[0, 0, 0],
    axis=2,
    radius=ring_radius - ring_wg_width / 2.0,
    length=td.inf,
)

ring = td.Structure(
    geometry=outer - inner,
    medium=solid,
    name="outer_ring",
)

Compute and visualize the waveguide modes.

[6]:
from tidy3d.plugins.mode import ModeSolver
from tidy3d.plugins.mode.web import run as run_ms

mode_plane = td.Box(
    center=[-wg_insert_x, wg_center_y, 0],
    size=[0, 2, td.inf],
)

sim_modesolver = td.Simulation(
    center=[0, sim_center_y, 0],
    size=[x_span, y_span, 0],
    grid_spec=td.GridSpec.auto(
        min_steps_per_wvl=min_steps_per_wvl, wavelength=td.C_0 / freq0
    ),
    structures=[waveguide],
    run_time=1e-12,
    boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()),
    medium=background,
)

mode_spec = td.ModeSpec(num_modes=2)
mode_solver = ModeSolver(
    simulation=sim_modesolver, plane=mode_plane, mode_spec=mode_spec, freqs=[freq0]
)
mode_data = run_ms(mode_solver)

09:13:57 -03 Mode solver created with
             task_id='fdve-e4a8ccd0-24d0-4681-9082-d413de49b0a9',
             solver_id='mo-469975f4-a90d-4cf9-9865-01308cbe7da1'.
09:14:08 -03 Mode solver status: queued
09:14:10 -03 Mode solver status: running
09:14:16 -03 Mode solver status: success

Tidy3D is warning us that the simulation does not contain a source. However, since this simulation is used to construct the mode solver and will not be run directly, we can ignore this warning.

[7]:
f, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(
    2, 3, tight_layout=True, figsize=(10, 6)
)
mode_data.Ex.sel(mode_index=0, f=freq0).abs.plot(ax=ax1)
mode_data.Ey.sel(mode_index=0, f=freq0).abs.plot(ax=ax2)
mode_data.Ez.sel(mode_index=0, f=freq0).abs.plot(ax=ax3)
mode_data.Ex.sel(mode_index=1, f=freq0).abs.plot(ax=ax4)
mode_data.Ey.sel(mode_index=1, f=freq0).abs.plot(ax=ax5)
mode_data.Ez.sel(mode_index=1, f=freq0).abs.plot(ax=ax6)
ax1.set_title("|Ex|: mode_index=0")
ax2.set_title("|Ey|: mode_index=0")
ax3.set_title("|Ez|: mode_index=0")
ax4.set_title("|Ex|: mode_index=1")
ax5.set_title("|Ey|: mode_index=1")
ax6.set_title("|Ez|: mode_index=1")

mode_data.to_dataframe()

[7]:
wavelength n eff k eff TE (Ey) fraction wg TE fraction wg TM fraction mode area
f mode_index
5.496195e+14 0 0.545455 1.348767 0.0 2.217226e-14 1.000000 0.882226 0.309919
1 0.545455 1.271590 0.0 1.000000e+00 0.789246 1.000000 0.454016
../_images/notebooks_RingResonator_13_1.png

From the above plots, we see that

mode_index=0 corresponds to exciting 0-th order waveguide TE mode (\(E = E_z\)) and

mode_index=1 corresponds to exciting 0-th order waveguide TM mode (\(H = H_z\)).

We can therefore switch the mode index accordingly based on our polarization.

Let’s select Ey and create the source for it. As mentioned before, we will use a broadband mode source that will approximate the chosen mode’s frequency dependence in the range (freq0 - 1.5 * fwidth, freq0 + 1.5 * fwidth) using 11 Chebyshev polynomials.

[8]:
mode_source = td.ModeSource(
    size=mode_plane.size,
    center=mode_plane.center,
    source_time=td.GaussianPulse(freq0=freq0, fwidth=fwidth),
    mode_spec=td.ModeSpec(num_modes=2),
    mode_index=1,
    direction="+",
    num_freqs=11,  # using 11 (Chebyshev) points to approximate frequency dependence
)

In addition, let’s monitor both the fields in plane as well as the output mode amplitudes into the fundamental TE mode.

[9]:
# monitor steady state fields at central frequency over whole domain
field_monitor = td.FieldMonitor(
    center=[0, 0, 0], size=[td.inf, td.inf, 0], freqs=[freq0], name="field"
)


# monitor the mode amps on the output waveguide
lambdas_measure = np.linspace(lambda_beg, lambda_end, 1001)
freqs_measure = td.C_0 / lambdas_measure[::-1]

mode_monitor = td.ModeMonitor(
    size=mode_plane.size,
    center=mode_plane.center,
    freqs=freqs_measure,
    mode_spec=td.ModeSpec(num_modes=2),
    name="mode",
)

# lets reset the center to the on the right hand side of the simulation though
mode_monitor = mode_monitor.copy(update=dict(center=[wg_insert_x, wg_center_y, 0]))

Define simulation. To make the simulation 2D, we can just set the simulation size in one of the dimensions to be 0. However, note that we still have to define a grid size in that direction. Additionally, we lower the shutoff factor to make sure the high-frequency response of this resonant system is accurately resolved.

[10]:
# create simulation
sim = td.Simulation(
    center=[0, sim_center_y, 0],
    size=[x_span, y_span, 0],
    grid_spec=td.GridSpec.auto(
        min_steps_per_wvl=min_steps_per_wvl, wavelength=td.C_0 / freq0
    ),
    structures=[waveguide, ring],
    sources=[mode_source],
    monitors=[field_monitor, mode_monitor],
    run_time=run_time,
    boundary_spec=td.BoundarySpec(
        x=td.Boundary.pml(), y=td.Boundary.pml(), z=td.Boundary.periodic()
    ),
    medium=background,
    shutoff=1e-9,
)

Visualize structure, source, and modes.

[11]:
# plot the two simulations
fig, ax = plt.subplots(1, 1, figsize=(6, 6))
sim.plot_eps(z=0.01, ax=ax)
plt.show()

../_images/notebooks_RingResonator_21_0.png

Run Simulation#

Run simulations on our server.

[12]:
# use function above to run simulation
sim_data = web.run(
    sim, task_name="ring_resonator", path="data/simulation_data.hdf5", verbose=True
)

09:14:20 -03 Created task 'ring_resonator' with task_id
             'fdve-afc4a4ad-100a-45f8-9f1d-c96b5e3d1eb8' and task_type 'FDTD'.
09:14:23 -03 status = queued
             To cancel the simulation, use 'web.abort(task_id)' or
             'web.delete(task_id)' or abort/delete the task in the web UI.
             Terminating the Python script will not stop the job running on the
             cloud.
09:14:29 -03 status = preprocess
09:14:31 -03 Maximum FlexCredit cost: 0.164. Use 'web.real_cost(task_id)' to get
             the billed FlexCredit cost after a simulation run.
             starting up solver
             running solver
09:16:29 -03 early shutoff detected at 76%, exiting.
             status = postprocess
09:16:34 -03 status = success
09:16:42 -03 loading simulation from data/simulation_data.hdf5
[13]:
# visualize normalization run
fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(15, 6))

ax1 = sim_data.plot_field("field", "Hz", val="real", z=0, ax=ax1)
ax2 = sim_data.plot_field("field", "S", val="abs", z=0, ax=ax2)

plt.show()

../_images/notebooks_RingResonator_24_0.png

Analyze Spectrum#

Now let’s analyze the mode amplitudes in the output waveguide.

First, let’s grab the data to inspect it.

[14]:
sim_data["mode"].amps

[14]:
<xarray.ModeAmpsDataArray (direction: 2, f: 1001, mode_index: 2)> Size: 64kB
array([[[-1.99379025e-07+8.39880174e-07j,
         -9.71468481e-01+5.94761407e-02j],
        [-6.56671859e-07+3.99442064e-07j,
         -9.71645801e-01+3.53229274e-02j],
        [-1.06827037e-06-2.07560656e-07j,
         -9.71115508e-01+1.08562657e-02j],
        ...,
        [ 9.42267725e-08+2.73165042e-07j,
         -7.29538006e-01-6.78746551e-01j],
        [ 3.26978399e-07+5.78399242e-07j,
         -6.90452316e-01-7.17011347e-01j],
        [ 4.52311885e-08+3.38274760e-07j,
         -6.42265066e-01-7.58513104e-01j]],

       [[-2.14289973e-09+2.05699659e-09j,
          5.35168909e-05+1.59106830e-05j],
        [-5.01320853e-09+2.35181885e-09j,
          5.78542496e-05+2.02228810e-05j],
        [ 6.46781560e-09+1.93345722e-09j,
          6.34580275e-05+4.51471121e-06j],
        ...,
        [-2.61250376e-09-3.88945487e-10j,
          2.29753636e-05+7.32964040e-05j],
        [-9.44435806e-09-7.35493441e-09j,
          1.39186205e-05+6.53310046e-05j],
        [-2.72464311e-09-9.40418030e-10j,
          1.90789591e-05+6.53667922e-05j]]])
Coordinates:
  * direction   (direction) <U1 8B '+' '-'
  * f           (f) float64 8kB 4.997e+14 4.997e+14 ... 5.995e+14 5.996e+14
  * mode_index  (mode_index) int64 16B 0 1
Attributes:
    units:      sqrt(W)
    long_name:  mode amplitudes

As we see, the mode amplitude data is complex-valued with three 3 dimensions:

  • index into the mode order returned by solver (remember, we wanted mode_index=1 for fundamental TE).

  • direction of the propagation (for decomposition).

  • frequency.

Let’s select into the first two dimensions to get mode amplitudes as a function of frequency.

[15]:
transmission_amps = sim_data["mode"].amps.sel(mode_index=1, direction="+")

Now let’s plot the data.

[16]:
f, ax = plt.subplots(figsize=(10, 5))
transmission_amps.abs.plot.line(x="f", ax=ax, label="absolute value")
# transmission_amps.real.abs.plot.line(x='f', ax=ax, label='|real|')
ax.legend()
ax.set_title("transmission amplitudes into fundamental TM (forward)")
ax.set_ylim(0, None)
ax.set_xlim(freqs_measure[0], freqs_measure[-1])
plt.show()

../_images/notebooks_RingResonator_30_0.png