2D ring resonator#

Run this notebook in your browser using Binder.

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. The simulation contains 2 million grid points to model the entire domain and 290,000 time steps to capture the resonance of the ring.

With Tidy3D, we perform the simulation in just a few minutes.

[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.

      (waveguide)
in -> ========== -> out
           0
      (resonator)
[2]:
# define geometry
wg_width = 0.25
couple_width = 0.05
ring_radius = 3.5
ring_wg_width = 0.25
wg_spacing = 2.0
buffer = 2.0

# compute quantities based on geometry parameters
x_span = 2*wg_spacing + 2*ring_radius + 2*buffer
y_span = 2*ring_radius + 2*ring_wg_width + wg_width + couple_width + 2*buffer
wg_insert_x = ring_radius + wg_spacing
wg_center_y = ring_radius + ring_wg_width/2. + couple_width + wg_width/2.
[3]:
# define pulse parameters
lambda0 = 0.5
freq0 = td.C_0 /lambda0
fwidth =  freq0 / 6
min_steps_per_wvl = 30
run_time_norm = 1e-13  # run time for normalization run without ring
run_time = 5e-12       # run time for simulation with ring

Define materials. (docs)

[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. (docs)

[5]:
# background of entire domain (set explicitly as a box)
background_box = td.Structure(
    geometry=td.Box(
        center=[0, 0, 0],
        size=[td.inf, td.inf, td.inf],
    ),
    medium=background,
    name='background')

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

# outside ring
outer_ring = td.Structure(
    geometry=td.Cylinder(
        center=[0,0,0],
        axis=2,
        radius=ring_radius+ring_wg_width/2.0,
        length=td.inf,
    ),
    medium=solid,
    name='outer_ring')

# inside ring fill
inner_ring = td.Structure(
    geometry=td.Cylinder(
        center=[0,0,0],
        axis=2,
        radius=ring_radius-ring_wg_width/2.0,
        length=td.inf,
    ),
    medium=background,
    name='inner_ring')

Compute and visualize the waveguide modes.

[6]:
from tidy3d.plugins import ModeSolver

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

sim_modesolver = td.Simulation(
    size=[x_span, y_span, 0],
    grid_spec=td.GridSpec.auto(min_steps_per_wvl=min_steps_per_wvl, wavelength=lambda0),
    structures=[background_box, waveguide],
    run_time=1e-12,
)

mode_spec = td.ModeSpec(num_modes=2)
mode_solver = ModeSolver(simulation=sim_modesolver, plane=mode_plane, mode_spec=mode_spec, freqs=[freq0])
mode_data = mode_solver.solve()
[17:01:42] WARNING  No sources in simulation.                               simulation.py:406
[7]:
f, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(2, 3, tight_layout=True, figsize=(10, 6))
abs(mode_data.fields.Ex.sel(mode_index=0).abs).plot(ax=ax1)
abs(mode_data.fields.Ey.sel(mode_index=0).abs).plot(ax=ax2)
abs(mode_data.fields.Ez.sel(mode_index=0).abs).plot(ax=ax3)
abs(mode_data.fields.Ex.sel(mode_index=1).abs).plot(ax=ax4)
abs(mode_data.fields.Ey.sel(mode_index=1).abs).plot(ax=ax5)
abs(mode_data.fields.Ez.sel(mode_index=1).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')
plt.show()
<Figure size 720x432 with 6 Axes>
../_images/notebooks_RingResonator_11_1.png

From the above plots, we see that

mode_index=0 corresponds to exciting 0-th order TM mode (E=Ez) and

mode_index=1 corresponds to exciting 0-th order TE mode (E=Ey).

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

Let’s select Ey and create the source for it.

[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='+'
)

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(0.4, 0.6, 1001)
freqs_measure = td.C_0 / lambdas_measure

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. (docs) 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.

[10]:
# create normalization simulation (no ring)
sim0 = td.Simulation(
    size=[x_span, y_span, 0],
    grid_spec=td.GridSpec.auto(min_steps_per_wvl=min_steps_per_wvl, wavelength=lambda0),
    structures=[background_box, waveguide],
    sources=[mode_source],
    monitors=[field_monitor, mode_monitor],
    run_time = run_time_norm,
    boundary_spec=td.BoundarySpec.pml(x=True, y=True)
)

# create simulation (with ring)
sim = td.Simulation(
    size=[x_span, y_span, 0],
    grid_spec=td.GridSpec.auto(min_steps_per_wvl=min_steps_per_wvl, wavelength=lambda0),
    structures=[background_box, waveguide, outer_ring, inner_ring],
    sources=[mode_source],
    monitors=[field_monitor, mode_monitor],
    run_time = run_time,
    boundary_spec=td.BoundarySpec.pml(x=True, y=True)
)

Visualize structure, source, and modes. (docs)

[11]:
# plot the two simulations
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 6))
sim0.plot_eps(z=0.01, ax=ax1)
sim.plot_eps(z=0.01, ax=ax2)
plt.show()
<Figure size 1296x432 with 4 Axes>
../_images/notebooks_RingResonator_19_1.png

Run Simulation#

Run simulations on our server. (docs)

[12]:
# use function above to run simulation with and without ring
sim_data0 = web.run(sim0, task_name='normalization', path='data/simulation_data0.hdf5')
sim_data = web.run(sim, task_name='with_ring', path='data/simulation_data.hdf5')
           INFO     Using Tidy3D credentials from stored file                      auth.py:74
[17:01:44] INFO     Uploaded task 'normalization' with task_id                  webapi.py:120
                    'd9db1851-d5da-4fae-91da-ee379ef64dd8'.                                  
[17:01:47] INFO     status = queued                                             webapi.py:253
[17:01:52] INFO     Maximum flex unit cost: 0.20                                webapi.py:244
[17:01:55] INFO     status = preprocess                                         webapi.py:265
[17:02:04] INFO     starting up solver                                          webapi.py:269
[17:02:14] INFO     running solver                                              webapi.py:275
[17:02:18] INFO     early shutoff detected, exiting.                            webapi.py:286
           INFO     status = postprocess                                        webapi.py:292
[17:02:32] INFO     status = success                                            webapi.py:298
[17:02:33] INFO     downloading file "output/monitor_data.hdf5" to              webapi.py:574
                    "data/simulation_data0.hdf5"                                             
[17:03:02] INFO     loading SimulationData from data/simulation_data0.hdf5      webapi.py:398
[17:03:03] INFO     Uploaded task 'with_ring' with task_id                      webapi.py:120
                    '8e7f4f6d-6f34-4dd3-bcb7-a11eb7eec78c'.                                  
[17:03:06] INFO     status = queued                                             webapi.py:253
[17:03:11] INFO     Maximum flex unit cost: 0.20                                webapi.py:244
[17:03:14] INFO     status = preprocess                                         webapi.py:265
[17:03:23] INFO     starting up solver                                          webapi.py:269
[17:03:34] INFO     running solver                                              webapi.py:275
[17:04:11] INFO     early shutoff detected, exiting.                            webapi.py:286
           INFO     status = postprocess                                        webapi.py:292
[17:04:29] INFO     status = success                                            webapi.py:298
[17:04:30] INFO     downloading file "output/monitor_data.hdf5" to              webapi.py:574
                    "data/simulation_data.hdf5"                                              
[17:05:06] INFO     loading SimulationData from data/simulation_data.hdf5       webapi.py:398
[13]:
print(sim_data0.log)
Simulation domain Nx, Ny, Nz: [1375, 741, 1]
Applied symmetries: (0, 0, 0)
Number of computational grid points: 1.0231e+06.
Using subpixel averaging: True
Number of time steps: 4.7330e+03
Automatic shutoff factor: 1.00e-05
Time step (s): 2.1135e-17


Compute source modes time (s):     0.2220
Compute monitor modes time (s):    2.6835
Rest of setup time (s):            4.2628

Running solver for 4733 time steps...
- Time step    189 / time 3.99e-15s (  4 % done), field decay: 1.00e+00
- Time step    377 / time 7.97e-15s (  7 % done), field decay: 1.00e+00
- Time step    378 / time 7.99e-15s (  8 % done), field decay: 1.00e+00
- Time step    567 / time 1.20e-14s ( 12 % done), field decay: 1.00e+00
- Time step    757 / time 1.60e-14s ( 16 % done), field decay: 9.96e-01
- Time step    946 / time 2.00e-14s ( 20 % done), field decay: 9.96e-01
- Time step   1135 / time 2.40e-14s ( 24 % done), field decay: 9.94e-01
- Time step   1325 / time 2.80e-14s ( 28 % done), field decay: 9.94e-01
- Time step   1514 / time 3.20e-14s ( 32 % done), field decay: 9.93e-01
- Time step   1703 / time 3.60e-14s ( 36 % done), field decay: 9.93e-01
- Time step   1893 / time 4.00e-14s ( 40 % done), field decay: 9.93e-01
- Time step   2082 / time 4.40e-14s ( 44 % done), field decay: 9.92e-01
- Time step   2271 / time 4.80e-14s ( 48 % done), field decay: 9.92e-01
- Time step   2461 / time 5.20e-14s ( 52 % done), field decay: 9.91e-01
- Time step   2650 / time 5.60e-14s ( 56 % done), field decay: 9.90e-01
- Time step   2839 / time 6.00e-14s ( 60 % done), field decay: 9.89e-01
- Time step   3029 / time 6.40e-14s ( 64 % done), field decay: 9.88e-01
- Time step   3218 / time 6.80e-14s ( 68 % done), field decay: 9.88e-01
- Time step   3407 / time 7.20e-14s ( 72 % done), field decay: 9.87e-01
- Time step   3597 / time 7.60e-14s ( 76 % done), field decay: 7.57e-01
- Time step   3786 / time 8.00e-14s ( 80 % done), field decay: 1.07e-02
- Time step   3975 / time 8.40e-14s ( 84 % done), field decay: 2.06e-07
Field decay smaller than shutoff factor, exiting solver.

Solver time (s):                   0.9330
Post-processing time (s):          2.3577


[14]:
# visualize normalization run
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, tight_layout=True, figsize=(15, 10))

ax1 = sim_data0.plot_field('field', 'Ex', val='real', z=0, freq=freq0, ax=ax1)
ax2 = sim_data0.plot_field('field', 'Ey', val='real', z=0, freq=freq0, ax=ax2)
ax1.set_title('Ex')
ax2.set_title('Ey')

ax3 = sim_data.plot_field('field', 'Ex', val='real', z=0, freq=freq0, ax=ax3)
ax4 = sim_data.plot_field('field', 'Ey', val='real', z=0, freq=freq0, ax=ax4)
ax3.set_title('Ex')
ax4.set_title('Ey')

plt.show()
<Figure size 1080x720 with 8 Axes>
../_images/notebooks_RingResonator_23_1.png

Analyze Spectrum#

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

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

[15]:
sim_data['mode']
ModeData(type='ModeData', data_dict={'amps': ModeAmpsData(type='ModeAmpsData', values=array([[[-1.36748595e-08-3.54299805e-07j,
          6.67187296e-01-7.32922290e-01j],
        [-3.91183705e-07+8.80675735e-08j,
          5.30794930e-01-8.35484975e-01j],
        [-7.61546932e-08-5.03426656e-07j,
          3.88862407e-01-9.08825832e-01j],
        ...,
        [ 1.31547302e-08-4.17567149e-09j,
         -8.79975414e-01-4.26730958e-01j],
        [-2.83325274e-07-1.08636654e-07j,
         -9.25109939e-01-3.18559222e-01j],
        [-6.25343250e-08-2.55287750e-08j,
         -9.56228774e-01-2.08281329e-01j]],

       [[-5.21015333e-10+6.64221637e-10j,
         -6.40159271e-06+8.28102695e-06j],
        [-3.41120027e-09+5.05742228e-09j,
         -9.31455451e-06-1.61433922e-05j],
        [ 2.37012830e-09-6.24865849e-09j,
          2.39816467e-06-1.55517018e-05j],
        ...,
        [ 2.19387902e-09+7.91885295e-10j,
          1.57870538e-05-4.19278858e-05j],
        [-7.99745318e-09-2.91163112e-09j,
          1.09425509e-05-4.27140064e-05j],
        [-3.55634820e-10-1.56853308e-10j,
          1.17555646e-06-4.32657027e-05j]]]), data_attrs={'units': 'sqrt(W)', 'long_name': 'mode amplitudes'}, f=array([7.49481146e+14, 7.49106593e+14, 7.48732414e+14, ...,
       4.99987423e+14, 4.99820705e+14, 4.99654098e+14]), mode_index=array([0, 1]), direction=['+', '-']), 'n_complex': ModeIndexData(type='ModeIndexData', values=array([[1.3983288+0.j, 1.3546187+0.j],
       [1.3982598+0.j, 1.354504 +0.j],
       [1.3981907+0.j, 1.3543893+0.j],
       ...,
       [1.331268 +0.j, 1.2425503+0.j],
       [1.331204 +0.j, 1.2424469+0.j],
       [1.33114  +0.j, 1.2423441+0.j]], dtype=complex64), data_attrs={'units': '', 'long_name': 'effective index'}, f=array([7.49481146e+14, 7.49106593e+14, 7.48732414e+14, ...,
       4.99987423e+14, 4.99820705e+14, 4.99654098e+14]), mode_index=array([0, 1]))})

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 and divide the results with a ring by the normalization.

[16]:
mode_data = sim_data['mode'].amps.sel(mode_index=1, direction='+')
mode_data0 = sim_data0['mode'].amps.sel(mode_index=1, direction='+')
transmission_amps = mode_data / mode_data0

Now let’s plot the data.

[17]:
f, ax = plt.subplots(figsize=(10, 5))
abs(transmission_amps.real).plot.line(x='f', ax=ax, label='|real|')
# abs(transmission_amps.imag).plot.line(x='f', ax=ax, label='|imag|')
abs(transmission_amps).plot.line(x='f', ax=ax, label='absolute value')
ax.legend()
ax.set_title('transmission amplitudes into fundamental TE (forward)')
ax.set_ylim(0, 1)
ax.set_xlim(freqs_measure[0], freqs_measure[-1])
plt.show()
<Figure size 720x360 with 1 Axes>
../_images/notebooks_RingResonator_29_1.png
[ ]: