在光子集成线路设计中,马赫曾德干涉仪是一个非常基础和非常重要的器件,其是可编程光子芯片、集成光计算网络以及集成量子密钥分配系统的重要组成部分。接下来介绍利用ipkiss来设计一个可调的MZI,并通过ipkiss来对设计的MZI进行频域和时域的仿真。

老规矩,所有代码如下:

from si_fab import all as pdk
from ipkiss3 import all as i3
import pylab as plt
import numpy as np
import os

class MZI(i3.Circuit):
    """This creates a ppc basic unit, made of tunable MZI.
    Users can specify the length of heater in one of MZI arm,
    They can also specify the length differences between the arms.
    """

    _name_prefix = "MZI"
    dc = i3.ChildCellProperty(doc="Directional coupler in circuit")
    heater = i3.ChildCellProperty(doc="Heater in circuit ")
    waveguide = i3.ChildCellProperty(doc="Waveguide in circuit ")
    wg_buffer_dx = i3.PositiveNumberProperty(default=10.0, doc="Extra non-heated waveguide length around heater")
    heater_length = i3.LockedProperty(doc="Length of heater in arm")
    two_arm_length_difference = i3.NumberProperty(default=0, doc="The length difference between two arm")
    bend_radius = i3.PositiveNumberProperty(default=50, doc="The bend radius of the routing waveguide")
    trace_template = i3.TraceTemplateProperty(
        default=pdk.NWG900(), doc="The trace template for waveguide and heater", locked=True
    )
    v_bias_dc = i3.NumberProperty(default=0.0, doc="DC voltage bias on the MZI (for S-matrix only)")

    def _default_dc(self):
        return pdk.SiNDirectionalCouplerSPower(power_fraction=0.5, target_wavelength=1.55)

    # Set the default value for the heater
    def _default_heater(self):
        ht = pdk.HeatedWaveguide(trace_template=self.trace_template)
        ht.Layout(shape=[(0.0, 0.0), (100.0, 0.0)])
        ht.CircuitModel(v_bias_dc=self.v_bias_dc)
        return ht

    # Set the default value for the parallel waveguide
    def _default_waveguide(self):
        wg = i3.Waveguide(trace_template=self.trace_template)
        wg.Layout(shape=[(0.0, 0.0), (self.heater_length, 0.0)])
        return wg

    def _default_heater_length(self):
        if hasattr(self.heater, "heater_length"):
            return self.heater.heater_length
        else:
            heater_size = self.heater.get_default_view(i3.LayoutView).size_info()
            return heater_size.width

    def _default_insts(self):
        insts = {"dc_1": self.dc, "dc_2": self.dc, "ht": self.heater, "rg": self.waveguide}
        return insts

    def _default_specs(self):
        r = self.bend_radius
        dx = self.wg_buffer_dx
        specs = [
            i3.Place("dc_1:in1", (0.0, 0.0)),
            i3.PlaceRelative("ht:in", "dc_1:out2", (2.0 * r + dx, 2.0 * r)),
            i3.PlaceRelative("rg:in", "dc_1:out1", (2.0 * r + dx, -2.0 * r - self.two_arm_length_difference / 2)),
            i3.PlaceRelative("dc_2:in2", "ht:out", (2.0 * r + dx, -2.0 * r)),
            i3.ConnectManhattan("ht:in", "dc_1:out2", bend_radius=r, min_straight=0),
            i3.ConnectManhattan("rg:in", "dc_1:out1", bend_radius=r, min_straight=0),
            i3.ConnectManhattan("ht:out", "dc_2:in2", bend_radius=r, min_straight=0),
            i3.ConnectManhattan("rg:out", "dc_2:in1", bend_radius=r, min_straight=0),
        ]
        return specs

    def _default_exposed_ports(self):
        exposed_ports = {
            "dc_1:in1": "in1",
            "dc_1:in2": "in2",
            "dc_2:out1": "out1",
            "dc_2:out2": "out2",
            "ht:elec1": "ht1",
            "ht:elec2": "ht2",
        }
        return exposed_ports

def simulate_ppc_unit_linear_voltages(
        cell,
        v_start=0.0,
        v_end=5.0,
        v_num=1000,
        center_wavelength=1.55,
        debug=False,
    ):
    dt = 1e-12  # time step
    t0 = 0e-12  # start time
    t1 = v_num * 1e-12  # end time

    # define optical source
    optical_in = i3.FunctionExcitation(
        port_domain=i3.OpticalDomain,
        excitation_function=lambda t: 1,
    )

    # define electrical source
    def linear_ramp(t):
        return v_end * t / t1 + v_start * (t1 - t) / t1

    electrical_in = i3.FunctionExcitation(
        port_domain=i3.ElectricalDomain,
        excitation_function=linear_ramp,
    )
    gnd = i3.FunctionExcitation(
        port_domain=i3.ElectricalDomain,
        excitation_function=lambda t: 0,
    )
    # Define the child cells and links (direct connections)
    child_cells = {
        "DUT": cell,
        "optical_in": optical_in,
        "ht": electrical_in,
        "gnd": gnd,
        "out1": i3.Probe(port_domain=i3.OpticalDomain),
        "out2": i3.Probe(port_domain=i3.OpticalDomain),
    }
    links = [
        ("optical_in:out", "DUT:in1"),
        ("ht:out", "DUT:ht1"),
        ("gnd:out", "DUT:ht2"),
        ("out1:in", "DUT:out1"),
        ("out2:in", "DUT:out2"),
    ]
    testbench = i3.ConnectComponents(
        child_cells=child_cells,
        links=links,
    )
    # get time simulation results
    cm = testbench.CircuitModel()
    results = cm.get_time_response(
        t0=t0,
        t1=t1,
        dt=dt,
        center_wavelength=center_wavelength,
        debug=debug,
    )
    results.timesteps = results.timesteps[8:]
    results.data = results.data[0:, 8:]
    return results

if __name__ == '__main__':
    # 频域仿真
    mzi = MZI(
        wg_buffer_dx=10.0,
        two_arm_length_difference=0.0,
        bend_radius=50,
        v_bias_dc=0,
    )
    mzi.Layout().visualize(annotate=True)

    # Frequency simulation visualization
    wavelengths = np.linspace(1.5, 1.6, 101)
    cm = mzi.CircuitModel()
    S = cm.get_smatrix(wavelengths=wavelengths)
    plt.plot(wavelengths, i3.signal_power(S["out1", "in1"]), label="out1")
    plt.plot(wavelengths, i3.signal_power(S["out2", "in1"]), label="out2")
    plt.legend()
    plt.xlabel("Wavelengths [um]")
    plt.ylabel("Output optical transmission")
    plt.show()

    # 时域仿真
    # mzi = MZI(
    #     wg_buffer_dx=10.0,
    #     two_arm_length_difference=0.0,
    #     bend_radius=50,
    # )
    # # Time domain simulation
    # # Sweep the voltage for single wavelength
    # results = simulate_ppc_unit_linear_voltages(cell=mzi, v_start=0.0, v_end=5.0, center_wavelength=1.55)
    # times = results.timesteps
    # signals = ["gnd", "ht", "optical_in", "out1", "out2"]
    # ylabels = ["voltage [V]", "voltage [V]", "power [W]", "power [W]", "power [W]"]
    # processes = [np.real, np.real, i3.signal_power, i3.signal_power, i3.signal_power]
    # # Simulation visualization
    # fig, axs = plt.subplots(nrows=len(signals), ncols=1, figsize=(6, 8))
    # for axes, signal, ylabel, process in zip(axs, signals, ylabels, processes):
    #     data = process(results[signal])
    #     axes.set_title(signal)
    #     axes.plot(times, data)
    #     axes.set_ylabel(ylabel)
    #     axes.set_xlabel("time [s]")
    # fig.savefig(os.path.join("{}.png".format("simulation_results_linear_volts")), bbox_inches="tight")
    # plt.show()

代码有点长,先来介绍一下代码的结构:

optisystem马赫曾德调制器 马赫-曾德_时域


代码主要分为四个部分:

第一部分:导入ipkiss演示用的pdk包,ipkiss基础包以及python三方包

第二部分:i3.Circuit线路设计函数设计MZI线路

第三部分:定义时域的线路仿真函数

第四部分:MZI实例化以及频域和时域仿真

先来看第二部分,利用i3.Circuit线路设计函数设计MZI线路,这部分代码的结构在教程2中介绍过就不展开了,MZI线路实例化之后如下图:

optisystem马赫曾德调制器 马赫-曾德_时域_02


为了实现MZI的可调,MZI的一个臂加入了热光调制器来调节相位。

在定义MZI属性时,特意加了两个重要的参数:two_arm_length_difference 以及 v_bias_dc

optisystem马赫曾德调制器 马赫-曾德_时域_03

two_arm_length_difference可以方便调节两个臂的长度差,比如:

two_arm_length_difference=200 时

if __name__ == '__main__':
    mzi = MZI(
        wg_buffer_dx=10.0,
        two_arm_length_difference=200.0,
        bend_radius=50,
        v_bias_dc=0,
    )
    mzi.Layout().visualize(annotate=True)

optisystem马赫曾德调制器 马赫-曾德_python_04


v_bias_dc是调制器两端加的电压,这个参数是为了方便MZI加电压时的频域仿真

第三部分是定义的时域仿真函数

def simulate_ppc_unit_linear_voltages(
        cell,
        v_start=0.0,
        v_end=5.0,
        v_num=1000,
        center_wavelength=1.55,
        debug=False,
    ):

仿真函数的参数主要有:cell是要仿真的线路,电加热器两端的电压v_start,v_end, 仿真电压点数v_num,以及中心波长center_wavelength

仿真函数首先定义仿真时间轴:

dt = 1e-12  # time step
t0 = 0e-12  # start time
t1 = v_num * 1e-12  # end time

定义输入的光源

# define optical source
    optical_in = i3.FunctionExcitation(
        port_domain=i3.OpticalDomain,
        excitation_function=lambda t: 1,
    )

定义加热器两端的电源:

# define electrical source
    def linear_ramp(t):
        return v_end * t / t1 + v_start * (t1 - t) / t1

    electrical_in = i3.FunctionExcitation(
        port_domain=i3.ElectricalDomain,
        excitation_function=linear_ramp,
    )
    gnd = i3.FunctionExcitation(
        port_domain=i3.ElectricalDomain,
        excitation_function=lambda t: 0,
    )

这里定义的电源正极是随时间线性变化,负极接地(为0)

接下来是定义仿真的单元,这是什么意思呢,相当于模拟一个实际的系统,这个系统需要有哪些器件,比如线路(DUT),光源(optical_in),电源正极(ht),电源负极(gnd),以及两个探测器(out1,out2),在ipkiss用i3.Probe(port_domain=i3.OpticalDomain)表示虚拟的光探测器

child_cells = {
        "DUT": cell,
        "optical_in": optical_in,
        "ht": electrical_in,
        "gnd": gnd,
        "out1": i3.Probe(port_domain=i3.OpticalDomain),
        "out2": i3.Probe(port_domain=i3.OpticalDomain),
    }

仿真单元定义好,接下来要把它们连起来:

links = [
        ("optical_in:out", "DUT:in1"),
        ("ht:out", "DUT:ht1"),
        ("gnd:out", "DUT:ht2"),
        ("out1:in", "DUT:out1"),
        ("out2:in", "DUT:out2"),
    ]

比如,如果仿真时要在MZI的in1端口输入光,就可以将光源的输出(optical_in:out)连接线路的in1端口(DUT:in1),
而想看线路哪个端口的输出,就可以将探测器的输入(out1:in,out2:in )接到线路要探测的端口上(DUT:out1,DUT:out2)
电源的连接也是一样(“ht:out”, “DUT:ht1”), (“gnd:out”, “DUT:ht2”)

最后就是是获取时域仿真结果并返回

testbench = i3.ConnectComponents(
        child_cells=child_cells,
        links=links,
    )
    cm = testbench.CircuitModel()
    results = cm.get_time_response(
        t0=t0,
        t1=t1,
        dt=dt,
        center_wavelength=center_wavelength,
        debug=debug,
    )
    results.timesteps = results.timesteps[8:]
    results.data = results.data[0:, 8:]
    return results

第四部分先来看频域仿真

仿真之前,先实例化MZI

mzi = MZI(
        wg_buffer_dx=10.0,
        two_arm_length_difference=0.0,
        bend_radius=50,
        v_bias_dc=0,
    )
    mzi.Layout().visualize(annotate=True)

通过get_smatrix就可以获取整个线路的散射矩阵来进行频域仿真

wavelengths = np.linspace(1.5, 1.6, 101)
cm = mzi.CircuitModel()
S = cm.get_smatrix(wavelengths=wavelengths)
plt.plot(wavelengths, i3.signal_power(S["out1", "in1"]), label="out1")
plt.plot(wavelengths, i3.signal_power(S["out2", "in1"]), label="out2")
plt.legend()
plt.xlabel("Wavelengths [um]")
plt.ylabel("Output optical transmission")
plt.show()

两个端口的输出结果如下:

optisystem马赫曾德调制器 马赫-曾德_频域_05


改变MZI的臂长差(two_arm_length_difference=100.0),以及加电压(v_bias_dc=2):

mzi = MZI(
        wg_buffer_dx=10.0,
        two_arm_length_difference=100.0,
        bend_radius=50,
        v_bias_dc=2,
    )
mzi.Layout().visualize(annotate=True)
wavelengths = np.linspace(1.5, 1.6, 1001)
cm = mzi.CircuitModel()
S = cm.get_smatrix(wavelengths=wavelengths)
plt.plot(wavelengths, i3.signal_power(S["out1", "in1"]), label="out1")
plt.plot(wavelengths, i3.signal_power(S["out2", "in1"]), label="out2")
plt.legend()
plt.xlabel("Wavelengths [um]")
plt.ylabel("Output optical transmission")
plt.show()

仿真结果如下:

optisystem马赫曾德调制器 马赫-曾德_开发语言_06


接下来是时域仿真:

仿真之前同样需要先实例化MZI

mzi = MZI(
        wg_buffer_dx=10.0,
        two_arm_length_difference=0.0,
        bend_radius=50,
    )

调用第三部分定义好的时域仿真函数,就能得到MZI时域的响应:

results = simulate_ppc_unit_linear_voltages(cell=mzi, v_start=0.0, v_end=5.0, center_wavelength=1.55)
times = results.timesteps
signals = ["gnd", "ht", "optical_in", "out1", "out2"]
ylabels = ["voltage [V]", "voltage [V]", "power [W]", "power [W]", "power [W]"]
processes = [np.real, np.real, i3.signal_power, i3.signal_power, i3.signal_power]
# Simulation visualization
fig, axs = plt.subplots(nrows=len(signals), ncols=1, figsize=(6, 8))
for axes, signal, ylabel, process in zip(axs, signals, ylabels, processes):
    data = process(results[signal])
    axes.set_title(signal)
    axes.plot(times, data)
    axes.set_ylabel(ylabel)
    axes.set_xlabel("time [s]")
fig.savefig(os.path.join("{}.png".format("simulation_results_linear_volts")), bbox_inches="tight")
plt.show()

仿真结果如下:

optisystem马赫曾德调制器 马赫-曾德_python_07


可以看到当电源正极的电压随时间线性变化,给线路in1口一个固定的输入,探测器探测到的两个输出端口的光强随时间发生变化。