Generation of Kerr sideband#
Note: the cost of running the entire notebook is larger than 1 FlexCredits.
Nonlinear materials are of great interest in many industries due to their unique capability to exhibit nontrivial phenomena, such as wave mixing and frequency generation. An interesting phenomenon that occurs in media with third-order nonlinearities is the Kerr sidebands, a form of four-wave mixing. Due to phase-matching conditions, these sidebands appear at frequencies offset by integer multiples of the frequency difference between the pump and signal.
Kerr sidebands can significantly enhance sensor resolution through all-optical signal processing. By combining a tunable laser source and a pump laser with a Fiber Bragg Grating (FBG) in a nonlinear fiber, frequency sidebands are generated, with each sideband’s power dependent on input intensities. Filtering specific sidebands narrows the FBG-reflected signal, improving wavelength shift detection. This technique enhances FBG-based temperature sensors and can be applied to other optical systems for increased resolution.
In this notebook, we use Tidy3D to perform a simulation of the generation of Kerr sideband in a waveguide. This example is based on the paper from Ole Krarup, Chams Baker, Liang Chen, and Xiaoyi Bao " Nonlinear resolution enhancement of an FBG based temperature sensor using the Kerr effect." Optics Express Vol. 28, Issue 26, pp. 39181-39188 (2020)
Doi: https://doi.org/10.1364/OE.411179
This example was kindly created by Dr. Chenchen Wang, Postdoctoral Researcher at the University of Wisconsin–Madison.
For more examples, please refer to our learning center, where you can find tutorials such as Bistability in photonic crystal microcavities.
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.
Theoretical base#
In this section, we will introduce the theoretical framework of generating sidebands in a Kerr medium, as developed in the reference paper.
To generate Kerr sidebands, we inject laser light with two distinct angular frequencies into a Kerr medium. The two frequencies are a signal frequency
where
Neglecting the effects of dispersion, loss, and polarization, the evolution of this field is governed by the Non-linear Schrödinger Equation (NLSE):
where
Solving this differential equation, the field at the output of the waveguide is given by:
Here,
The term involving
where
The output field becomes a sum of frequency sidebands, spaced by
This result indicates that the output field is a superposition of several frequency components, with the sidebands separated by
The power of the
Introducing the normalization
Here we agree that
Under the condition
This equation shows that the normalized output power is proportional to the normalized input power, raised to an integer exponent.
As an example, filtering out the
Similarly for
This implies an symmetry between
Initial setup#
First we start defining the parameters for the simulation:
[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
# define geometry
wg_width = 0.25
wg_length = 2.5
wg_spacing = 0.5
buffer = 1.0
# compute quantities based on geometry parameters
x_span = 2 * wg_spacing + 2 * wg_length + 2 * buffer
y_span = wg_width + 2 * buffer
wg_insert_x = wg_length + wg_spacing
Define frequency:
[2]:
# 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
freqd = 1e13
freqp = freq0 + 0.5 * freqd
freqs = freq0 - 0.5 * freqd
# frequency for the first sideband
freq1 = freqs + (freqp - freqs)
min_steps_per_wvl = 30
run_time = 5e-12
Define Materials:
To define the NonlinearSpec
object with a NonlinearSusceptibility
model. Since the underlying mechanism for Kerr sidebands is four-wave mixing, NonlinearSusceptibility
is a suitable choice as it utilizes real electric fields.
The num_iters
parameter can be used if the convergence is poor, although it can’t prevent a simulation with high nonlinearities from diverging, as we will discuss below.
[3]:
n_bg = 1.0
n_solid = 1.5
background = td.Medium(permittivity=n_bg**2)
solid = td.Medium(permittivity=n_solid**2)
# define the nonlinear parameters
n_kerr_2 = 2e-8
kerr_chi3 = (
4 * (n_solid**2) * td.constants.EPSILON_0 * td.constants.C_0 * n_kerr_2 / 3
)
amp = 400
chi3_model = td.NonlinearSpec(
models=[td.NonlinearSusceptibility(chi3=kerr_chi3)], num_iters=10
)
kerr_solid = td.Medium(permittivity=n_solid**2, nonlinear_spec=chi3_model)
Define structures:
[4]:
waveguide = td.Structure(
geometry=td.Box(
center=[0, 0, 0],
size=[td.inf, wg_width, 0.22],
),
medium=kerr_solid,
name="waveguide",
)
Compute and visualize the waveguide modes.
[5]:
from tidy3d.plugins.mode import ModeSolver
from tidy3d.plugins.mode.web import run as run_ms
mode_plane = td.Box(
center=[-wg_insert_x, 0, 0],
size=[0, 1, td.inf],
)
sim_modesolver = td.Simulation(
center=[0, 0, 0],
size=[x_span, y_span, 3],
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)
16:02:25 -03 Mode solver created with task_id='fdve-6a1ba312-297a-490c-93da-148f8b850822', solver_id='mo-6322c53c-27cc-48a9-a873-e47b718dbf21'.
16:02:29 -03 Mode solver status: success
[6]:
f, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(
2, 3, tight_layout=True, figsize=(10, 6)
)
mode_solver.plot_field("Ex", "abs", mode_index=0, ax=ax1)
mode_solver.plot_field("Ey", "abs", mode_index=0, ax=ax2)
mode_solver.plot_field("Ez", "abs", mode_index=0, ax=ax3)
mode_solver.plot_field("Ex", "abs", mode_index=1, ax=ax4)
mode_solver.plot_field("Ey", "abs", mode_index=1, ax=ax5)
mode_solver.plot_field("Ez", "abs", mode_index=1, 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()
[6]:
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.108526 | 0.0 | 0.984406 | 0.858946 | 0.895359 | 0.201495 |
1 | 0.545455 | 1.094335 | 0.0 | 0.017490 | 0.846849 | 0.898209 | 0.205158 |

Here we choose TM mode, then we create two mode sources
To ensure spectral overlap between the signal and pump sources, we will define the time profile of the sources as ContinuousSource
, which simulates a continuous wave (CW) source.
Note that a GaussianPulse
time profile is preferred in most cases, as the fields decay at the end of the simulation, making normalization in frequency domain meaningful. In contrast, for a ContinuousSource
, the fields do not decay, so frequency domain normalization is not meaningful. Nevertheless, the temporal mean power of the source is equal to the square of the amplitude
parameter.
Also, note that the fwidth
argument controls the ramping of the CW amplitude rather than the spectral width, since a CW source is nearly monochromatic.
[7]:
mode_source_p = td.ModeSource(
size=mode_plane.size,
center=mode_plane.center,
source_time=td.ContinuousWave(freq0=freqp, fwidth=freq0 / 10, amplitude=amp),
mode_spec=td.ModeSpec(num_modes=2),
mode_index=1,
direction="+",
num_freqs=1,
)
mode_source_s = td.ModeSource(
size=mode_plane.size,
center=mode_plane.center,
source_time=td.ContinuousWave(freq0=freqs, fwidth=freq0 / 10, amplitude=amp),
mode_spec=td.ModeSpec(num_modes=2),
mode_index=1,
direction="+",
num_freqs=1,
)
Define monitors:
[8]:
# field monitor for the first sideband
field_monitor = td.FieldMonitor(
center=[0, 0, 0], size=[td.inf, td.inf, 0], freqs=[freq1], 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",
)
mode_monitor = mode_monitor.copy(update=dict(center=[wg_insert_x, 0, 0]))
# flux monitor
flux_monitor = td.FluxMonitor(
center=(3.5, 0, 0), size=mode_plane.size, name="fluxMon", freqs=freqs_measure
)
Define simulation.
Note that we will set the shutoff
argument as 0, as we are using CW sources so the fields will not decay. For the same reason, we can also disregard the warning messages in the log.
[9]:
# create simulation
sim = td.Simulation(
normalize_index=None,
center=[0, 0, 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],
sources=[mode_source_p, mode_source_s],
monitors=[field_monitor, mode_monitor, flux_monitor],
run_time=run_time,
boundary_spec=td.BoundarySpec(
x=td.Boundary.pml(), y=td.Boundary.pml(), z=td.Boundary.periodic()
),
medium=background,
shutoff=0,
)
Visualize structure and sources.
[10]:
# plot the two simulations
fig, ax = plt.subplots(1, 1, figsize=(6, 6))
sim.plot_eps(z=0.01, ax=ax)
plt.show()

Run simulation#
[11]:
sim_data = web.run(
sim, task_name="kerr_sideband", path="data/simulation_data.hdf5", verbose=True
)
16:02:36 -03 Created task 'kerr_sideband' with task_id 'fdve-31d13b1e-dfc8-4937-8018-12bc44deffb0' and task_type 'FDTD'.
View task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-31d13b1e-dfc 8-4937-8018-12bc44deffb0'.
16:02:38 -03 status = success
16:02:43 -03 loading simulation from data/simulation_data.hdf5
Next, we can plot the fields at the frequency of the first sideband. As there is no source injecting power in this frequency, the fields are generated due to the nonlinear process.
[12]:
# visualize the fields
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()

Now, we can visualize the transmittance spectrum, where the two sidebands are visible at frequencies equally spaced from the pump and signal pulse.
[13]:
transmission_amps = sim_data["mode"].amps.sel(mode_index=1, direction="+") ** 2
f, ax = plt.subplots(figsize=(10, 5))
transmission_amps.abs.plot.line(x="f", ax=ax, label="absolute value")
ax.legend()
ax.set_title("Flux of the fundamental TM mode (forward)")
ax.set_ylim(0, None)
ax.set_xlim(freqs_measure[0], freqs_measure[-1])
ax.set_ylabel("Power (a.u.)")
plt.show()

Amplitude analysis#
We can now vary the amplitude parameter and observe the power at the n = 1 sideband (at 565 Thz). Since the amplitudes of the pump and signal are identical, we expect the power of this band to vary approximately with
[14]:
Simulations = {}
Amplitudes = [50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 700]
for amplitude in Amplitudes:
# creating the sources with the new amplitude value
s1, s2 = sim.sources
st1 = s1.source_time.updated_copy(amplitude=amplitude)
st2 = s2.source_time.updated_copy(amplitude=amplitude)
sim2 = sim.updated_copy(
sources=[s1.updated_copy(source_time=st1), s2.updated_copy(source_time=st2)]
)
Simulations[amplitude] = sim2
[15]:
batch = web.Batch(simulations=Simulations, verbose=True)
results = batch.run(path_dir="batch_simulations")
16:02:49 -03 Started working on Batch containing 13 tasks.
16:03:06 -03 Maximum FlexCredit cost: 1.327 for the whole batch.
Use 'Batch.real_cost()' to get the billed FlexCredit cost after the Batch has completed.
16:03:17 -03 Batch complete.
[16]:
Amps = []
Power1 = []
for amplitude in Amplitudes:
sim_data = results["%i" % amplitude]
# recording the power for the band n = 1
band1 = sim.sources[0].source_time.freq0 + (
sim.sources[0].source_time.freq0 - sim.sources[1].source_time.freq0
)
bm = (sim_data["fluxMon"].flux.f > band1 * 0.99) & (
sim_data["fluxMon"].flux.f < band1 * 1.01
)
max = sim_data["fluxMon"].flux[bm].max()
Amps.append(amplitude)
Power1.append(max)
16:03:46 -03 WARNING: The simulation has diverged! For more information, check 'SimulationData.log' or use 'web.download_log(task_id)'.
[17]:
# fitting the data with a polynomial function
from scipy.optimize import curve_fit
func = lambda X, a: a * X**5
Y = np.array(Power1) * 10**19
res, err = curve_fit(func, Amps[:9], Y[:9], p0=[1e-9], bounds=([0], [1]))
fig, ax = plt.subplots()
ax.plot(Amps, Y, "o")
ax.plot(Amps, func(np.array(Amps), *(res)))
ax.set_xlabel("Amplitude value")
ax.set_ylabel("First sideband power (a.u.)")
ax.set_ylim(-0.1, 1.1 * Y.max())
plt.show()

It can be observed that the power in the n = 1 band follows a power law until amplitude values around 500. Beyond this point, the results deviate from the exponential trend until the simulation diverges for amplitudes greater than 700.
This occurs because the simulations remain stable only for small nonlinearities. The nonlinear permittivity should be smaller than the linear value. Therefore:
$ \epsilon_0 \chi`^{(3)} E^3 :nbsphinx-math:ll :nbsphinx-math:epsilon`_0 :nbsphinx-math:`chi`^{(1)} E$
$ \chi`^{(3)} E^2 :nbsphinx-math:ll `n_0^2-1$
Using the Poynting theorem, and the relation
Since the Intensity (
Additionally, we should mention that the
Further verification#
In the previous derivation, we obtained the following conclusions about
To verify this symmetry, We set the amplitude of the two input signals to half of the original amplitude and observe the change of the sideband signal.
[18]:
mode_source_p = mode_source_p.copy(
update=dict(
source_time=td.ContinuousWave(freq0=freqp, fwidth=freq0 / 10, amplitude=amp)
)
)
mode_source_s = mode_source_s.copy(
update=dict(
source_time=td.ContinuousWave(freq0=freqs, fwidth=freq0 / 10, amplitude=amp / 2)
)
)
sim_1 = td.Simulation(
normalize_index=None,
center=[0, 0, 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],
sources=[mode_source_p, mode_source_s],
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=0,
)
mode_source_p = mode_source_p.copy(
update=dict(
source_time=td.ContinuousWave(freq0=freqp, fwidth=freq0 / 10, amplitude=amp / 2)
)
)
mode_source_s = mode_source_s.copy(
update=dict(
source_time=td.ContinuousWave(freq0=freqs, fwidth=freq0 / 10, amplitude=amp)
)
)
sim_2 = td.Simulation(
normalize_index=None,
center=[0, 0, 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],
sources=[mode_source_p, mode_source_s],
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=0,
)
Simulations2 = {1: sim_1, 2: sim_2}
[19]:
batch2 = web.Batch(simulations=Simulations2, verbose=True)
results2 = batch2.run(path_dir="batch_simulations")
sim_data_1 = results2["1"]
sim_data_2 = results2["2"]
16:03:48 -03 Started working on Batch containing 2 tasks.
16:03:52 -03 Maximum FlexCredit cost: 0.204 for the whole batch.
Use 'Batch.real_cost()' to get the billed FlexCredit cost after the Batch has completed.
16:06:25 -03 Batch complete.
Visualize the output spectrum of two further simualtions.
[20]:
transmission_amps_1 = sim_data_1["mode"].amps.sel(mode_index=1, direction="+") ** 2
transmission_amps_2 = sim_data_2["mode"].amps.sel(mode_index=1, direction="+") ** 2
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(10, 10))
transmission_amps_1.abs.plot.line(x="f", ax=ax1, label="absolute value (Top)")
ax1.legend()
ax1.set_title("Fundamental TM mode flux with half signal")
ax1.set_ylim(0, None)
ax1.set_xlim(freqs_measure[0], freqs_measure[-1])
ax1.set_ylabel("Power (a.u.)")
# Bottom plot1
transmission_amps_2.abs.plot.line(x="f", ax=ax2, label="absolute value (Bottom)")
ax2.legend()
ax2.set_title("Fundamental TM mode flux with half pump")
ax2.set_ylim(0, None)
ax2.set_xlim(freqs_measure[0], freqs_measure[-1])
ax2.set_ylabel("Power (a.u.)")
plt.tight_layout()
plt.show()

From the figure above, we can observe the symmetry for