Advanced examples

The options for placement and routing are more extensive than what are covered in the tutorials. In these examples we will see several powerful methods that provide additional tools for circuit layout design.

Generalized splitter tree

../../../_images/splitter_tree_parametric.png

Three-level splitter tree made in the tutorial

In the getting started tutorials we designed a splitter tree circuit. That design is already parametric, with the spacing between various components and the type of splitter used all being parameters. We can extend this approach even further, creating a circuit that is parametric to the number of levels in the splitter tree. To do this, all the instance creation, placement and routing must be parametric so that we simply pass in the size of splitter tree we want and the circuit is built for us. Whilst this may sound complicated, there are actually very few methods needed for this that we have not already seen. We will use a combination of f-strings and for loops with some basic logic to achieve the desired circuit.

By nesting a for loop within another for loop, we can iterate though each splitter in each level and add them to our instances dictionary.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_generalized_splitter_tree.py

        for level in range(self.n_levels):
            for splitter_no in range(2**level):
                insts[f"sp_{level}_{splitter_no}"] = self.splitter
        return insts

The placement is done in a similar way, where using nested for loops we can place all the MMIs at once.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_generalized_splitter_tree.py

        for level in range(self.n_levels):
            for splitter in range(2**level):
                x_coord = level * self.spacing_x
                y_coord = self.spacing_y * (
                    -0.5 * 2 ** (self.n_levels - 1) + ((splitter + 0.5) * 2 ** (self.n_levels - level - 1))
                )
                specs.append(i3.Place(f"sp_{level}_{splitter}", (x_coord, y_coord)))

We can also make use of Python logic to add another layer of control to our design. Since each splitter has two outputs, we can iterate through the splitters at each level and determine their connections based on whether the splitter number is even or odd. This allows us to connect all the splitters in each level with the splitters in the following level (except the last one) in a single for loop.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_generalized_splitter_tree.py
        for level in range(1, self.n_levels):
            for splitter in range(2**level):
                if splitter % 2 == 0:
                    in_port = f"sp_{level-1}_{int(splitter/2)}:out1"
                else:
                    in_port = f"sp_{level-1}_{int(splitter/2)}:out2"
                out_port = f"sp_{level}_{splitter}:in1"
                specs.append(i3.ConnectBend(in_port, out_port))

In this way, we can build a very large circuit with hundreds of outputs from the same code just by changing the number of splitter levels. You could extend this example to take the number of outputs as a parameter instead of the number of levels as an additional exercise.

../../../_images/four_level_splitter_tree.png

Four-level splitter tree made using the GeneralizedSplitterTree class

Advanced layout options

Using the parametric splitter tree, we can now create a more complicated circuit, which connects the splitter tree outputs to grating couplers. Waveguide connections from the splitter tree to the gratings is achieved using bundle routing and control points. To reuse the splitter tree circuit, we first import it from the other script and then rename it to Tree.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_advanced_layout_options.py
from explore_generalized_splitter_tree import GeneralizedSplitterTree as Tree


Then we can pass parameters from the new circuit into the previous splitter tree to reuse it with a different number of levels.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_advanced_layout_options.py
    splitter_tree = i3.ChildCellProperty(doc="Splitter used.")

Since the generalized splitter tree can have an arbitrary footprint, it is tedious to calculate the absolute coordinates to place waveguides and components around it. This problem is worse in larger circuits with several different designs sharing a layout area. IPKISS permits the designer to place waveguides, instances and circuits relative to each other, while calculating the absolute coordinates in the background.

Relative placement of ports is shown in this example using a waveguide crossing. We can use AlignV and AlignH to position this crossing in line with existing ports, to reduce the number of bends in the circuit. By providing these specifications to the specs property of i3.Circuit, we can constrain the location of the crossing allowing the placement engine to resolve its coordinates.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_advanced_layout_options.py
            i3.AlignV("crossing:out2", "test_input_gc:out"),  # used to align an instance in one axis to another port
            i3.AlignH("crossing:in1", "tree:in"),

The grating couplers can be placed north of the generalized splitter tree. The positions of the output grating couplers with respect to the input port of the tree can be manually calculated and specified using the following lines.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_advanced_layout_options.py
        for grating_number in range(2**self.n_levels):
            specs.append(
                i3.Place(
                    f"tree_output_gc_{grating_number}",
                    (grating_number * self.grating_spacing, (2**self.n_levels + 1) * 0.5 * self.spacing_y),
                    angle=270,
                    relative_to="tree:in",
                )
            )

The waveguide that connects the southern port of the crossing with the grating coupler array needs to be routed below the splitter tree. Otherwise, the waveguides will cross. Objects such as waveguides or circuits can be placed relative to specific edges or corners of other instances. For example, the bottom edge of the tree instance can be referenced using the following syntax, tree@S. More information about reference symbols and their uses can be found in the API reference and the Placement and Routing tutorial.

In this example, we use i3.H to specify a horizontal coordinate position (a control line) that the waveguide must pass through. This axis is defined relative to the bounding box of the splitter tree, ensuring it will work regardless of the splitter tree size. It is also possible to pass a list of control lines/points for more advanced routing if necessary (see the examples in Placement and Routing tutorial and Bundle Routing tutorial).

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_advanced_layout_options.py
            i3.ConnectManhattan(
                "crossing:in2",
                "test_output_gc:out",
                control_points=[i3.H(-self.spacing_y / 2, relative_to="tree@S")],
            ),

For connecting the output of the splitter tree, with the array of grating couplers, we make use of the bundle connector. This connector, called ConnectManhattanBundle, lets us connect multiple input and output ports with the waveguides running parallel to each other. We use list comprehension to pass a list of the connections to it and set some parameters to control the shape of the fanouts and spacings.

luceda-academy/training/getting_started/2_circuit_layout/3_advanced_examples/explore_advanced_layout_options.py

        specs.append(
            i3.ConnectManhattanBundle(
                connections=[
                    (f"tree:out{n+1}", f"tree_output_gc_{2**self.n_levels-1-n}:out") for n in range(2**self.n_levels)
                ],
                start_fanout=i3.SBendFanout(max_sbend_angle=90.0, reference=f"tree:out{2**self.n_levels}"),
                end_fanout=i3.SBendFanout(max_sbend_angle=90, reference=f"tree_output_gc_{2**self.n_levels-1}:out"),
                pitch=10,
                bend_radius=5,
            )
        )

This example circuit could be designed in a way to avoid some of these challenges, but it is a good demonstration of how even complex circuits can be defined easily and parametrically.

../../../_images/routed_splitter_tree.png

A routed splitter tree, connected to grating couplers using ConnectManhattan(Bundle)