2.1. Mach-Zehnder interferometer

The Mach-Zehnder interferometer (MZI) we are going to design is composed of:

  1. One input fiber grating coupler
  2. One Y-branch splitter
  3. One broadband directional coupler
  4. Two output fiber grating couplers
  5. Two waveguide arms (left and right) connecting the two outputs of the splitter to the two inputs of the directional coupler
  6. Two waveguides connecting the two outputs of the directional coupler to the two output fiber grating couplers
Mach-Zehnder interferometer

IPKISS integrates the different aspects of photonic design into one framework, where you can define your component once as a parametric cell (PCell) and then use it throughout the whole design process, allowing to tightly link layout and simulations. Therefore, in order to easily create and manipulate the MZI that we need, we are going to create a PCell. This PCell inherits from CircuitCell, a custom class defined in additional_utils, that makes it is easy to place and connect components together to achieve the final circuit.

2.1.1. PCell Properties

The first step is to define the PCell properties, which will be used to design the MZI. One aspect to pay attention to is the right arm of the MZI. Depending on the length of this arm, the result of the MZI simulation will change. Therefore, we need to be able to control the shape of this arm when we instantiate the MZI PCell, without touching the code inside the PCell itself.

The PCell has the following accessible properties:

  • through_point: This is a coordinate (through point) through which the right (longest) arm of the MZI has to pass. This allows to control the shape of the right arm when we instantiate the MZI.
  • bend_radius: Here we assing the value of the bend radius to be used for the waveguide routes.
  • fgc: The PCell of the fiber grating coupler to be used. The default value is EBeamGCTE1550 from the IPKISS PDK for SiEPIC.
  • splitter: The PCell of the splitter to be used. The default value is EBeamY1550 from the IPKISS PDK for SiEPIC.
  • dir_coupler: The PCell of the directional coupler to be used. The default value is EBeamBDCTE1550 from the IPKISS PDK for SiEPIC.

Below is the code that defines the PCell properties.

Listing 2.15 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
class MZI(CircuitCell):

    through_point = i3.Coord2Property(doc="Point the longer arm of the MZI has to go through")
    bend_radius = i3.PositiveNumberProperty(default=5.0, doc="Bend radius of the waveguides")

    fgc = i3.ChildCellProperty(doc="PCell for the fiber grating coupler")
    splitter = i3.ChildCellProperty(doc="PCell for the Y-Branch")
    dir_coupler = i3.ChildCellProperty(doc="PCell for the directional coupler")

    def _default_through_point(self):
        return [(100.0, 220.0)]

    def _default_fgc(self):
        return pdk.EbeamGCTE1550()

    def _default_splitter(self):
        return pdk.EbeamY1550()

    def _default_dir_coupler(self):
        return pdk.EbeamBDCTE1550()

2.1.2. Child cells and connections

The next step is to define a list of child cells. We need three fiber grating couplers, one splitter and one directional coupler.

Listing 2.16 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
    def _default_child_cells(self):
        child_cells = {
            "fgc_1": self.fgc,
            "fgc_2": self.fgc,
            "fgc_3": self.fgc,
            "yb": self.splitter,
            "dc": self.dir_coupler,
        return child_cells

The connections between the child cells are defined differently depending on whether we connect them butt-to-butt or we need a waveguide connector:

  • For butt-to-butt connections we use joins. This is the case for the connection between the input fiber grating coupler and the splitter:

    Listing 2.17 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
        def _default_joins(self):
            joins = [("fgc_3:opt1", "yb:opt1")]
            return joins
  • To connect components using waveguide connectors, we use connectors. Here we specify the type of connector used between the desired ports, together with other useful information, such as the bend radius of the waveguides.

    Listing 2.18 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
        def _default_connectors(self):
            br = self.bend_radius
            connectors = [
                ("yb:opt3", "dc:opt2", manhattan, {"bend_radius": br}),
                ("yb:opt2", "dc:opt1", partial(manhattan, through_points=[self.through_point]), {"bend_radius": br}),
                ("dc:opt4", "fgc_2:opt1", manhattan, {"bend_radius": br}),
                ("dc:opt3", "fgc_1:opt1", manhattan, {"bend_radius": br}),
            return connectors

    The connection between the splitter (“yb:opt2”) and the directional coupler (“dc:opt1”) is performed using a Manhattan-type waveguide connector, which is forced to pass through the coordinate that we provide in the property through_point.

2.1.3. Placement of the components

Components that are placed butt-to-butt using joins are automatically moved by IPKISS to ensure that the connection requirement is satisfied. In the case of connections defined using connectors, this is not the case. IPKISS only knows that these two components have to be connected and the waveguide connector will be generated depending on their position, which we need to specify ourselves.

We place the input port of the bottom fiber grating coupler at the origin (0.0, 0.0). We then create an array of fiber grating coupler by placing the second grating coupler 127 um above the first, and the third 127 um above the second. Finally, we place the directional coupler relative to the output ports of the Y-branch splitter and we rotate it of 90 degrees.

Listing 2.19 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
    def _default_place_specs(self):
        fgc_spacing_y = 127.0
        place_specs = [
            i3.Place("fgc_1:opt1", (0, 0)),
            i3.PlaceRelative("fgc_2:opt1", "fgc_1:opt1", (0.0, fgc_spacing_y)),
            i3.PlaceRelative("fgc_3:opt1", "fgc_2:opt1", (0.0, fgc_spacing_y)),
            i3.PlaceRelative("dc:opt1", "yb:opt2", (20.0, -40.0), angle=90),
        return place_specs

2.1.4. External ports

Our circuit is almost finished. All we need to do now is to define the names of the external ports we want to access. We will use these port names to plot the transmission when we perform the circuit simulation.

Listing 2.20 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
    def _default_external_port_names(self):
        epn = {
            "fgc_3:fib1": "in",
            "fgc_2:fib1": "out1",
            "fgc_1:fib1": "out2",
        return epn

2.1.5. Run the code

Open luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py. At the bottom of the file, the MZI is instantiated and simulated:

Listing 2.21 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/mzi_pcell.py
    # Layout
    mzi = MZI(
        through_point=(100.0, 240.0),
    mzi_layout = mzi.Layout()

    # Circuit model
    my_circuit_cm = mzi.CircuitModel()
    wavelengths = np.linspace(1.52, 1.58, 4001)
    S_total = my_circuit_cm.get_smatrix(wavelengths=wavelengths)

    # Plotting
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['out1:0', 'in:0'])), '-', linewidth=2.2, label="TE-out1")
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['out2:0', 'in:0'])), '-', linewidth=2.2, label="TE-out2")
    plt.xlabel('Wavelength [um]', fontsize=16)
    plt.ylabel('Transmission [dB]', fontsize=16)
    plt.legend(fontsize=14, loc=4)

If you run the code, you will visualize the layout and the simulation of an MZI circuit for through_point=[(100.0, 240.0)] and bend_radius=5.0.

Mach-Zehnder interferometer
Mach-Zehnder interferometer

2.1.6. Test your knowledge

Try to change the through point and the bend radius to see how the simulation changes. What happens when the delay arm becomes longer? Why?