Splitter tree with two levels

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


The main building block of this circuit is an MMI with 1 input and 2 outputs. Therefore, we can use the optimized 1x2 MMI that we designed in the previous sections and is included in the SiFab PDK. The first step is to instantiate this MMI.

Listing 42 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
splitter = pdk.MMI1x2Optimized()
splitter_lv = splitter.Layout()

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.


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).

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.

In order to define a CircuitCell, we need the following ingredients:

  • 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 43 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
child_cells = {"sp_0_0": splitter,
               "sp_1_0": splitter,
               "sp_1_1": splitter}

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 44 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    # 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).
    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)]
  • 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 45 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
spacing_x = 100.0
spacing_y = 50.0
place_specs = [i3.Place("sp_0_0:in1", (0, 0)),
               i3.PlaceRelative("sp_1_0:in1", "sp_0_0:out1", (spacing_x, -spacing_y / 2)),
               i3.PlaceRelative("sp_1_1:in1", "sp_0_0:out2", (spacing_x, +spacing_y / 2))]
  • 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 46 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
    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"}

Now, we can create the circuit by instantiating CircuitCell and passing the child cells, connectors, specs and external port names we have just defined.

Listing 47 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
my_circuit = CircuitCell(name="splittertree_2levels",
#    * CircuitCell Layout
my_circuit_layout = my_circuit.Layout()

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. CAPHE, the circuit simulator included in IPKISS, takes care of putting together the models and simulating the full circuit. All we have to do is to instantiate the circuit model.

Listing 48 luceda-academy/training/getting_started/circuit_splitter_tree/splitter_tree_2levels.py
my_circuit_cm = my_circuit.CircuitModel()
wavelengths = np.linspace(1.5, 1.6, 501)
S_total = my_circuit_cm.get_smatrix(wavelengths=wavelengths)
#    * Plotting
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)