Defining a compact model for thermal MZI

In this example, we first define a temperature-dependent compact model of a waveguide. Then, we employ this custom model for the waveguides of a Mach-Zehnder interferometer to perform a temperature-dependent simulation.

Getting started

We start by importing the technology and other necessary modules:

import matplotlib.pyplot as plt
from si_fab import all as pdk
import ipkiss3.all as i3
import numpy as np

The temperature-dependent waveguide model

This takes into account the effect of temperature, which is taken from env, just like the wavelength. The behavior in frequency-domain is fully described by the S-matrix, which we compute in calculate_smatrix.

class ThermalWGCompactModel(i3.CompactModel):
    """Single mode waveguide model with thermal effects.

    Parameters
    ----------
    n_eff: float
       Effective index at center_wavelength.
    n_g: float
       Group index at center_wavelength.
    center_wavelength: float (m)
        Center wavelength (in meter) of the model around which n_eff and loss are defined.
    loss: float (dB/m)
        Waveguide loss in dB/m.
    length: float (m)
       Total length of the waveguide in meter.
    reference_temperature (K)
        At temperature at which n_eff, n_g and dn_dT are defined at.
    dn_dT: float (1/K)
        Effective index variation with temperature
    """

    parameters = ["n_eff", "n_g", "center_wavelength", "loss", "length", "reference_temperature", "dn_dT"]
    terms = [i3.OpticalTerm(name="in"), i3.OpticalTerm(name="out")]

    def calculate_smatrix(parameters, env, S):
        center_wavelength = parameters.center_wavelength * 1e6
        dneff = -(parameters.n_g - parameters.n_eff) / center_wavelength
        T_diff = env.temperature - parameters.reference_temperature  # temperature difference with respect to reference
        neff_total = parameters.n_eff + (env.wavelength - center_wavelength) * dneff + T_diff * parameters.dn_dT
        length = parameters.length * 1e6
        phase = 2 * np.pi / env.wavelength * neff_total * length
        loss = 10 ** (-parameters.loss * length * 1e-6 / 20.0)
        S["in", "out"] = S["out", "in"] = np.exp(1j * phase) * loss

Using the custom model for the waveguide

Now we need to swap the default waveguide model with the one we just defined. Our waveguides will use it provided that we specify tt as the trace template.

class GenericWaveguideWithTemperatureModel(pdk.GenericWaveguide):
    class CircuitModel(i3.CircuitModelView):
        def _generate_model(self):
            trace_length = self.cell.get_default_view(i3.LayoutView).trace_length()
            trace_length *= 1e-6  # trace_length() gives the length in um, model expects m
            return ThermalWGCompactModel(
                n_eff=1.0,
                n_g=2.0,
                center_wavelength=1.55e-6,
                loss=140.0,
                length=trace_length,
                reference_temperature=293.15,  # we will vary the external temperature
                dn_dT=3e-4,
            )


class TTWithTempModel(pdk.SiWireWaveguideTemplate):
    _templated_class = GenericWaveguideWithTemperatureModel


tt = TTWithTempModel()  # waveguides will obey ThermalWGCompactModel if this trace template is specified for connections

Designing the MZI

class ThermalMZI(i3.Circuit):
    path_difference = i3.PositiveNumberProperty(default=100.0, doc="Difference in optical paths in um")
    arm_separation = i3.PositiveNumberProperty(default=100.0, doc="Separation between the two arms in um")
    length = i3.PositiveNumberProperty(default=200.0, doc="Length of the MZI in um")

    def _default_insts(self):
        mmi = pdk.MMI1x2Optimized1550()
        return {"mmi1": mmi, "mmi2": mmi}

    def _default_specs(self):
        return [
            i3.ConnectManhattan(
                "mmi1:out2",
                "mmi2:out1",
                trace_template=tt,  # for the temperature-dependent model
                control_points=[i3.H(self.arm_separation / 2 + self.path_difference / 4)],
            ),
            i3.ConnectManhattan(
                "mmi1:out1",
                "mmi2:out2",
                trace_template=tt,  # for the temperature-dependent model
                control_points=[i3.H(-self.arm_separation / 2 + self.path_difference / 4)],
            ),
            i3.Place("mmi1", (0, 0), angle=0),
            i3.Place("mmi2", (self.length, 0), angle=180),
        ]

    def _default_exposed_ports(self):
        return {"mmi1:in1": "in", "mmi2:in1": "out"}

Instantiating and visualizing the MZI

mzi = ThermalMZI(name="thermalMZI")
mzi_lo = mzi.Layout()
mzi_lo.visualize()
plot thermal mzi

Running the Caphe simulation

We see that increasing temperature results in a redshift (longer wavelengths) of the transmission spectrum.

mzi_model = mzi.CircuitModel()
wavelength_range = np.linspace(1.52, 1.58, 1000)
temperature_range = np.linspace(280, 300, 3)

ax = plt.gca()
for T in temperature_range:
    s_matrix = mzi_model.get_smatrix(wavelength_range, temperature=T)
    ax.plot(wavelength_range, i3.signal_power_dB(s_matrix["out", "in"]), label=f"ambient temperature {T} K")

plt.legend()
plt.xlabel(r"wavelength [$\mu$m]")
plt.ylabel("transmission [dB]")
ax.set_ylim(-35, 0)
plt.show()
plot thermal mzi