3.2. Design variations and hierarchical layout

The next step is to use the MZI PCell defined in the previous section to create a sweep of MZIs with different length of the right arm. Also for this case we will use CircuitCell, so that we can easily simulate each MZI that is part of our design to check their behaviour.

Mach-Zehnder interferometer

When submitting designs to a foundry, more often than not you are allowed limited space to fit your designs. To make sure that the design is not bigger than the allowed area, it is useful to use a frame. The IPKISS PDK for CORNERSTONE contains a PCell called DesignFrame, which can be used exactly for this purpose. The height and width of this PCell are fixed to a width of 11.47 mm and a height of 15.45 mm. This is quite generous so we will simply show how to fit on one small part of the frame.

Let’s now have a look at how we can define design variations using IPKISS. Open the file luceda-academy/training/topical_training/cornerstone_mzi_sweep/design_variations.py in your PyCharm project, so that you can follow along with the explanation.

3.2.1. Parameters for the MZI sweep

First of all, we define the parameters that we will use for the MZI sweep. We want to design three different MZIs. The right arm of each MZI will pass through a different coordinate in order to control its length. We provide this coordinate to the MZI PCell using the through_point property.

The following parameters are defined at the beginning of the file:

  • through_points: A list of coordinates [(x1, y1), (x2, y2), (x3, y3)] that will be used to instantiate the MZI PCells of the three MZIs we want to design.
  • bend_radius: The waveguide bend radius we will use for our designs.
  • pos_x and pos_y: The x- and y-coordinates where we will place the bottom left fiber coupler of the first MZI. They are defined to be close to the bottom left corner of the design frame as the center of the design frame will be placed at the origin. This is why we use the large negative quantities of -5150 and -7700 micrometers.
  • spacing_y: The vertical spacing between the fiber grating couplers of the three MZIs.

After defining these parameters, we create an empty child_cells dictionary and an empty place_specs lists. We will add to them all the child cells and the placement specifications that we need to provide to CircuitCell to create our final design.

Listing 3.10 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/design_variations.py
# Parameters for the MZI sweep
through_points = [(100.0, 200.0), (200.0, 200.0), (300.0, 200.0)]
bend_radius = 80.0
pos_x = -5150
pos_y = -7700
spacing_y = 600

child_cells = dict()
place_specs = []

3.2.2. Design frame

Before designing the MZIs, it is good to have clear what are the boundaries for our design. We instantiate the design frame from the IPKISS PDK for CORNERSTONE SiN.

Listing 3.11 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/design_variations.py
# Create the design frame
design_frame = pdk.DesignFrame()

# Add the design frame to the child cells and place it at (0.0, 0.0)
child_cells["design_frame"] = design_frame
place_specs.append(i3.Place("design_frame", (0.0, 0.0)))

3.2.3. MZI sweep

To create the MZI sweep, we use a practical for-loop over the list of through points. Each time, we instantiate an MZI with the correct through point, we add this instance to the dictionary of child cells and we place it at coordinates (x0, y0). For the first MZI, these are the coordinates we defined in the initial parameters. For each following MZI, the value of y0 is increased by the value in spacing_y.

For our own reference, after each MZI is instantiated, we calculate the delay length between the two arms of the MZI and print it.

Very important! Each time we instantiate an MZI, we have to assign a unique name. Otherwise, the tree instantiated MZIs will be an exact copy of each other and changing the design of one of them will result in a change in all of them. To make sure the names are all different, we use the for-loop counter ind to call them MZI0, MZI1, MZI2.

Listing 3.12 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/design_variations.py
# Create the MZI sweep
for ind, tp in enumerate(through_points):
    mzi = MZI(

    # Calculate the actual delay length and print the results
    right_arm_length = mzi.get_connector_instances()[1].reference.trace_length()
    left_arm_length = mzi.get_connector_instances()[0].reference.trace_length()
    delay_length = right_arm_length - left_arm_length

        "Delay length = {} um".format(delay_length),
        "Through points = {}".format(tp),

    mzi_cell_name = "mzi{}".format(ind)
    child_cells[mzi_cell_name] = mzi
    place_specs.append(i3.Place(mzi_cell_name, (pos_x, pos_y)))

    pos_y += spacing_y

3.2.4. Circuit design

We now create our circuit to be simulated using CircuitCell. Most of the heavy work is done, we only need to instantiate a circuit cell which contains the child cells and the placement specifications that we carefully defined before.

Listing 3.13 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/design_variations.py
# Create the design of the circuit with circuit cell
circuit = CircuitCell(

3.2.5. Layout and circuit simulation

We can now instantiate the layout of the circuit to view it and save the layout of the final design to a GDS file:

Listing 3.14 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/design_variations.py
# Layout
circuit_lv = circuit.Layout()

Mach-Zehnder interferometer

We show here the corner of the design frame we use for our design sweep:

Mach-Zehnder interferometer

Next, we can extract the S-matrix and plot the results of the circuit simulation to visualize the behaviour of our MZIs:

Listing 3.15 luceda-academy/training/topical_training/siepic_mzi_dc_sweep/design_variations.py
# Circuit model
circuit_cm = circuit.CircuitModel()
wavelengths = np.linspace(1.52, 1.58, 5001)
S_total = circuit_cm.get_smatrix(wavelengths=wavelengths)

# Plotting
fig, axs = plt.subplots(3, sharex='all')

for ind, wav_sep in enumerate(through_points):
    tr_out = 20 * np.log10(np.abs(S_total['mzi{}_out:0'.format(ind), 'mzi{}_in:0'.format(ind)]))

    axs[ind].plot(wavelengths, tr_out, '-', linewidth=2.2, label="TE - MZI{}:out".format(ind))

    axs[ind].set_ylabel('Transmission [dB]', fontsize=16)
    axs[ind].set_title('MZI{}'.format(ind), fontsize=16)
    axs[ind].legend(fontsize=14, loc=4)
    axs[ind].set_ylim([-100, 0])

axs[2].set_xlabel('Wavelength [um]', fontsize=16)

Mach-Zehnder interferometer

3.2.6. Test your knowledge

Do these results satisfy your expectations? If not, change the waveguide spacing to change the length of the right arm and check again the simulation.

Only a small fraction of the design frame is covered by this design sweep, how could you have a design sweep with 100 or even a 100 MZIs on the frame?