1. Splitter tree with two levels

In this section, we are going to design and simulate a simple circuit: a splitter tree with two levels made of three connected splitters. The result is the following filter with one input and four outputs.

../../../_images/splitter_tree_two_levels_layout.png

The main building block of this circuit is an MMI with one input and two outputs. For this circuit, we use Optimized1x2MMI, a 1x2 MMI optimized from transmission, contained in the SiFab PDK. This MMI was designed following the procedure explained in the previous section and the code is almost identical. The only differences are that its layout parameters have been optimized to maximize the transmission and that the circuit model uses the simulation data. If you would like to learn how to design an optimized MMI, you may want to check out the following advanced tutorial: Multi-mode interferometer (MMI).

Let’s visualize the MMI.

Listing 1.1 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    mmi = pdk.MMI1x2Optimized()
    mmi_layout = mmi.Layout()
    mmi_layout.visualize(annotate=True)

../../../_images/mmi_optimized_annotated.png

We already know that the layout of this MMI is optimized and the correct data is used for the circuit simulation. Therefore, we can immediately go ahead and see how to use it to design and simulate a circuit.

1.1. Ports

The three MMIs that are part of the final circuit are connected to each other using waveguides. Each MMI contains information about ports in the LayoutView. This information is very important because ports are used to indicate how the waveguides connect the different components together. Each port contains the following properties:

  • name: name of the port.
  • position: position of the port.
  • angle: direction in which a waveguide leaving the port has to go. 0 degrees is parallel to the x-axis, going eastwards.
  • trace_template: trace template of the waveguide used at the port.

The component MMI1x2Optimized has three ports: in1, out1 and out2. The names of the ports can be visualized on the LayoutView with the command splitter_lv.visualize(annotate=True) or they can be extracted by executing print(splitter_lv.ports).

1.2. Building a circuit with CircuitCell

A simple way to build a circuit in IPKISS is to use ‘CircuitCell’, which allows you to easily define connectivity between a number of child cells. ‘CircuitCell’ is an additional feature built on IPKISS and is provided with this training material as part of ‘additional_utils’. When placing these child cells, CircuitCell will generate all the waveguides (as separate PCells) needed to connect the child cells together.

First, we are going to define the splitter tree PCell in the file splitter_tree_2levels.py using the functionalities of CircuitCell. Then, we are going to instantiate this PCell at the bottom of the same file.

Let’s start with the CircuitCell definition. To define a CircuitCell, we define a Python class that inherits from CircuitCell in splitter_tree_2levels.py and call it SplitterTree2Levels. Furthermore, we need the following ingredients:

  • Properties Just as we did for the MMI PCell in the previous section, we first define properties that will be used to design the splitter tree circuit. In this case we have one i3.ChildCellProperty for the splitter, and two i3.PositiveNumberProperty to adjust the distance between the splitters in the circuit.

    Listing 1.2 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    class SplitterTree2Levels(CircuitCell):
    
        # 1. We define the properties of the PCell.
        splitter = i3.ChildCellProperty()
        spacing_x = i3.PositiveNumberProperty(default=100.0)
        spacing_y = i3.PositiveNumberProperty(default=50.0)
    
        def _default_splitter(self):
            return pdk.MMI1x2Optimized()
    
    

Note

For experienced Python users: we can add properties like in a PCell because CircuitCell is also a PCell, as it inherits from it.

  • Child cells. This is a Python dictionary containing the names of the child cell instances as keys. In this case, we have three child cells, one for each splitter.
Listing 1.3 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    # 2. We define the child cells of our circuit. We have 3 splitters, 1 for the first level and 2 for the
    # second level.
    def _default_child_cells(self):
        child_cells = {
            "sp_0_0": self.splitter,
            "sp_1_0": self.splitter,
            "sp_1_1": self.splitter,
        }
        return child_cells

The first number indicates the level at which we are going to place the splitter while the second number indicates the number of the splitter in a specific level.

  • Connectors. This is a list of tuples, each containing the names of the ports to be connected and the routing function used to draw the waveguide between them. The ports are identified by the name of the child cell and the name of the port, as child_cell:port. Various functions can be used to connect the ports and you can even write your own. Optionally, one can tune the default parameters of the connector function by passing a dictionary as an optional fourth argument to the connector. All the connectors that were introduced in the previous section Waveguides and waveguide connectors can be used here.

    Listing 1.4 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
        # 3. We define connectors (list of tuples): ports to be connected + algorithm to connect them (here a Bezier s-bend)
        # Optionally we can tune the default parameters of the connector function as a fourth argument. In this
        # case we change the adiabatic angle for one of the bezier_sbends (default is 15).
        def _default_connectors(self):
            connectors = [
                ("sp_0_0:out1", "sp_1_0:in1", bezier_sbend, {"adiabatic_angle": 1.0}),
                ("sp_0_0:out2", "sp_1_1:in1", bezier_sbend),
            ]
            return connectors
    
    
  • Place specs. This is a list containing all the layout specifications that apply to each component. Specifications can control the distance between components, transformations (e.g. mirroring) and alignment. Click here for a full list of available specs.

Listing 1.5 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    # 4. We define specs, containing all the transformations that apply to each component.
    def _default_place_specs(self):
        place_specs = [
            i3.Place("sp_0_0:in1", (0, 0)),
            i3.PlaceRelative("sp_1_0:in1", "sp_0_0:out1", (self.spacing_x, -self.spacing_y / 2)),
            i3.PlaceRelative("sp_1_1:in1", "sp_0_0:out2", (self.spacing_x, self.spacing_y / 2)),
        ]
        return place_specs

  • External port names. In this dictionary, we expose the ports that we want to access once the circuit is completed. This is useful in order to expose only the ports that need to be routed on the upper hierarchical level as external ports, and to rename them.

    Listing 1.6 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
        # 5. We define the names of the external ports.
        def _default_external_port_names(self):
            external_port_names = {
                "sp_0_0:in1": "in",
                "sp_1_0:out1": "out1",
                "sp_1_0:out2": "out2",
                "sp_1_1:out1": "out3",
                "sp_1_1:out2": "out4",
            }
            return external_port_names
    

Now, we can instantiate the SplitterTree2Levels PCell in splitter_tree_2levels.py and we pass the desired MMI PCell as a property.

Listing 1.7 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    # 2. We instantiate the 2-level splitter tree and visualize it
    splitter_tree = SplitterTree2Levels(splitter=mmi)
    splitter_tree_layout = splitter_tree.Layout()
    splitter_tree_layout.visualize(annotate=True)

../../../_images/splitter_tree_two_levels_layout.png

1.3. Performing a circuit simulation

The circuit we have just built can be simulated as a whole because all its components, MMIs and waveguides, have a circuit model of their own. Caphe, the circuit simulator included in IPKISS, takes care of putting together the models and simulating the full circuit. All we have to do now is to instantiate the circuit model and plot the transmission.

Listing 1.8 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    # 3. We instantiate the Circuit Model
    splitter_tree_cm = splitter_tree.CircuitModel()
    wavelengths = np.linspace(1.5, 1.6, 501)
    S_total = splitter_tree_cm.get_smatrix(wavelengths=wavelengths)

    # 4. We plot the transmission
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['out1', 'in'])), '-', linewidth=2.2, label="out1")
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['out2', 'in'])), '-', linewidth=2.2, label="out2")
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['out3', 'in'])), '-', linewidth=2.2, label="out3")
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['out4', 'in'])), '-', linewidth=2.2, label="out4")
    plt.plot(wavelengths, 20 * np.log10(np.abs(S_total['in', 'in'])), '-', linewidth=2.2, label="in")
    plt.ylim(-70, 0)
    plt.xlabel('Wavelength [um]', fontsize=16)
    plt.ylabel('Transmission [dB]', fontsize=16)
    plt.legend(fontsize=14, loc=4)
    plt.show()
../../../_images/splitter_tree_two_levels_circuitsim.png

1.4. Test your knowledge

Start from exercise.py and modify it to obtain a SplitterTree3Levels with the following layout and simulation:

../../../_images/splitter_tree_three_levels_layout.png ../../../_images/splitter_tree_three_levels_circuitsim.png

Solution