Addendum: Fully customized connection bundle

Introduction

In this addendum we will show you how you can take advantage of full customizability when you are designing a connection bundle. To illustrate what this means, let’s first have a look at the following routed splitter tree.

../../../../_images/routed_tree_4.png

A careful look at the waveguides in this design shows that although they are all part of the same bundle, their paths strongly vary. Some waveguides:

  • Just have one bend (input waveguide)

  • Route around the corner and go up (gratings to the left)

  • Route up and have an S-bend at the end (gratings in the middle)

  • Route up and to the right (gratings to the right)

To create such a bundle in IPKISS, a fine level of control is needed and therefore every connector should be controlled individually. By creating a set of rules for our routing procedures, it would have to be possible to customize every connector dependent on their relative position in the bundle. Furthermore, in order to design in a flexible manner, we should be able to make a few small changes to some of our ports of our device-under-test (in our case our splitter tree) without having to create a completely new customized bundle. This can all be realized if we take the time to code a specific routing heuristic for our bundle.

Creating a fully customized bundle has the following advantages:

  • The heuristics of the code translate well from one design to the next.

  • The routing of the connectors can be improved step-by-step.

  • This approach saves you a lot of work when making small changes to your device-under-test.

Creating a fully customized connection bundle

We want to create a set of rules that every connector of our bundle should follow. The best way to handle this is to define conditional waypoints for every connector. These waypoints are shifted left or right (and up or down) depending on the relative position of the connection in the bundle. In addition, we create conditional connectors, where the routing methodology is adapted depending on the relative position of the connection in the bundle.

To illustrate this, we will show an example of a customized bundle that connects a set of input grating couplers with a set of output grating couplers. In the example below we have:

  • Two conditional waypoints for every connector: wp1 and wp2.

  • Two options for the conditional connector:

    • When the y-coordinate of the waveguide (wp_y) is almost at the same height as the grating coupler, an S-bend is used in the first and the last part of the connector;

    • When the y_coordinate of the waveguide (wp_y) is above or below the grating coupler, then a manhattan connector is used from start to finish.

from si_fab import all as pdk
from ipkiss3 import all as i3
import numpy as np

gr = pdk.FC_TE_1550()
insts = {}
specs = []
sep = 28.0
wav_sep = 5.0
bend_radius = 5.0
wp1x = 50.0
wp2x = 350.0
wp_y = 55.0

for cnt in range(10):
    gc_in = f"gc_in{cnt}"
    gc_out = f"gc_out{cnt}"
    gr_y = cnt * sep
    insts[gc_in] = gr
    insts[gc_out] = gr

    specs.extend(
        [
            i3.Place(gc_in + ":out", (0, gr_y)),
            i3.Place(gc_out + ":out", (400, gr_y)),
            i3.FlipH(gc_out),
        ]
    )

    # Controlling the conditional waypoints
    wp_y += wav_sep
    if wp_y > gr_y:
        wp1x -= wav_sep
        wp2x += wav_sep
    else:
        wp1x += wav_sep
        wp2x -= wav_sep

    # Controlling the conditional routing specifications
    if np.abs(wp_y - gr_y) > 2 * bend_radius:
        specs.append(
            i3.ConnectManhattan(
                gc_in + ":out",
                gc_out + ":out",
                control_points=[i3.V(wp1x), i3.H(wp_y), i3.V(wp2x)],
                bend_radius=bend_radius,
                min_straight=0.0,
            )
        )
    else:
        intermediate_port1 = i3.OpticalPort(name="intermediate_port1", position=(wp1x, wp_y), angle=180)
        intermediate_port2 = i3.OpticalPort(name="intermediate_port2", position=(wp2x, wp_y), angle=180)
        specs.extend(
            [
                i3.ConnectBend(
                    gc_in + ":out",
                    intermediate_port1,
                    f"connect_gc_in_{cnt}",
                    bend_radius=bend_radius,
                    start_straight=0.0,
                    end_straight=0.0,
                ),
                i3.ConnectManhattan(
                    intermediate_port1.flip_copy(),
                    intermediate_port2,
                    f"connect_gc_mid_{cnt}",
                    bend_radius=bend_radius,
                    min_straight=0.0,
                    start_straight=0.0,
                    end_straight=0.0,
                ),
                i3.ConnectBend(
                    intermediate_port2.flip_copy(),
                    gc_out + ":out",
                    f"connect_gc_out_{cnt}",
                    bend_radius=bend_radius,
                    start_straight=0.0,
                    end_straight=0.0,
                ),
            ]
        )


cell = i3.Circuit(
    insts=insts,
    specs=specs,
)

cell_lo = cell.Layout()
cell_lo.write_gdsii("example_bundle.gds")
cell_lo.visualize()
../../../../_images/example_bundle.png

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_bundle.py

Below, we will show two examples where a design for a fully customized bundle is created to connect the outputs of a splitter tree to a set of grating couplers at the edge of the chip.

Example 1: Splitter Tree North

This example illustrates the application of a routing heuristic on a splitter tree oriented in the horizontal direction with grating couplers placed north of the chip. In both bundles chained connectors are being used. Here, the position of the waypoints is calculated from the position and size of instances that have already been placed in the circuit. This is done using the layout views of specific instance variables, as provided to i3.Circuit.

Note

The following example uses the splitter tree designed in the P-Team Library built on top of SiFab (pteam_library_si_fab). For the import statement to work, add the folder libraries/pteam_libraries_si_fab/ipkiss to the PYTHONPATH (right-click > Mark Directory as > Sources Root).

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_splitter_tree_routed_north.py
import si_fab.all as pdk
from ipkiss3 import all as i3
from pteam_library_si_fab.all import SplitterTree
import numpy as np
import os


class RouteSplitterTreeNorth(i3.Circuit):
    """Routed Splitter tree with grating couplers placed north."""

    dut = i3.ChildCellProperty(doc="splitter used")
    grating = i3.ChildCellProperty(doc="Splitter")
    grating_sep = i3.PositiveNumberProperty(default=50.0, doc="Separation between the gratings")
    bend_radius = i3.PositiveNumberProperty(default=20.0, doc="Bend Radius")
    wav_sep = i3.PositiveNumberProperty(default=8.0, doc="Separation between waveguides")

    def _default_grating(self):
        return pdk.FC_TE_1550()

    def _default_bend_radius(self):
        return 20.0

    def _default_dut(self):
        return SplitterTree(name=self.name + "_DUT", levels=6)

    def _default_insts(self):
        instances = dict()
        instances["DUT"] = self.dut
        for cnt in range(2**self.dut.levels):
            instances[f"gr_out_{cnt}"] = self.grating
        instances["gr_in"] = self.grating

        return instances

    def _default_specs(self):
        si_dut = self.dut.get_default_view(i3.LayoutView).size_info()
        nw = si_dut.north_west
        gr_y = nw[1] + (1 + np.ceil(si_dut.width / self.grating_sep)) * self.wav_sep + 5 * self.bend_radius
        specs = [
            i3.Place("DUT", (0, 0)),
            i3.Place("gr_in:out", (nw[0] - 1 * self.grating_sep, gr_y), angle=-90),
            i3.ConnectManhattan("gr_in:out", "DUT:in", bend_radius=self.bend_radius, min_straight=0.0),
        ]

        wp_x = si_dut.east + self.bend_radius
        wp_y = si_dut.north + 2 * self.bend_radius
        n_gratings = 2**self.dut.levels
        for cnt in range(n_gratings):
            pos_grating_port = (nw[0] + cnt * self.grating_sep, gr_y)
            specs.append(i3.Place(f"gr_out_{cnt}:out", pos_grating_port, angle=-90))

            tt_grating_port = self.grating.get_default_view(i3.LayoutView).ports["out"].trace_template

            wp_x += self.wav_sep
            if pos_grating_port[0] > wp_x:
                wp_y -= self.wav_sep
            else:
                wp_y += self.wav_sep

            if np.abs(pos_grating_port[0] - wp_x) < 2 * self.bend_radius:
                intermediate_port = i3.OpticalPort(
                    name=f"intermediate_port_{cnt}",
                    position=(wp_x, wp_y),
                    angle=90,
                    trace_template=tt_grating_port,
                )

                specs.extend(
                    [
                        i3.ConnectBend(f"gr_out_{cnt}:out", intermediate_port, bend_radius=self.bend_radius),
                        i3.ConnectManhattan(
                            intermediate_port.flip_copy(),
                            f"DUT:out{n_gratings - cnt}",
                            bend_radius=self.bend_radius,
                            min_straight=0.0,
                            start_straight=0.0,
                        ),
                    ]
                )

            else:
                specs.append(
                    i3.ConnectManhattan(
                        f"gr_out_{cnt}:out",
                        f"DUT:out{n_gratings - cnt}",
                        control_points=[i3.H(wp_y), i3.V(wp_x)],
                        bend_radius=self.bend_radius,
                        min_straight=0.0,
                        start_straight=0.0,
                    )
                )

        return specs


if __name__ == "__main__":
    project_folder = "./splitter_tree_north"  # Name of the project
    if not os.path.exists(project_folder):
        os.mkdir(project_folder)
    for levels in range(1, 7):
        print(f"Number levels:{levels}")
        dut = SplitterTree(name=f"SP{levels}", levels=levels)
        cell = RouteSplitterTreeNorth(name=f"Routed_tree{levels}", dut=dut)
        cell_lv = cell.Layout()
        fig = cell_lv.visualize(show=False)
        fig.savefig(
            os.path.join(project_folder, f"routed_tree_{levels}.png"),
            transparent=True,
            bbox_inches="tight",
        )
        cell_lv.write_gdsii(os.path.join(project_folder, f"splitter_tree_routed_north_{levels}.gds"))
        print("done")

Let’s analyze the code in a bit more detail. The overall cell inherits from i3.Circuit and defines cell “instances” that constitute our circuit and the placement and routing specs. This is done by implementing the methods _default_insts and _default_specs.

Step 1: Definition of the instances

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_splitter_tree_routed_north.py
    def _default_insts(self):
        instances = dict()
        instances["DUT"] = self.dut
        for cnt in range(2**self.dut.levels):
            instances[f"gr_out_{cnt}"] = self.grating
        instances["gr_in"] = self.grating

The first instance we place is the DUT, or device-under-test, which is passed on as a i3.ChildCellProperty. The reason we do that is to allow the DUT to be defined externally to the class and passed on as a property. Next we have a for loop for all the output grating couplers as well as the input grating coupler.

Step 2: Definition of the placement and routing specifications

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_splitter_tree_routed_north.py
    def _default_specs(self):
        si_dut = self.dut.get_default_view(i3.LayoutView).size_info()
        nw = si_dut.north_west
        gr_y = nw[1] + (1 + np.ceil(si_dut.width / self.grating_sep)) * self.wav_sep + 5 * self.bend_radius
        specs = [
            i3.Place("DUT", (0, 0)),
            i3.Place("gr_in:out", (nw[0] - 1 * self.grating_sep, gr_y), angle=-90),
            i3.ConnectManhattan("gr_in:out", "DUT:in", bend_radius=self.bend_radius, min_straight=0.0),
        ]

        wp_x = si_dut.east + self.bend_radius
        wp_y = si_dut.north + 2 * self.bend_radius
        n_gratings = 2**self.dut.levels
        for cnt in range(n_gratings):
            pos_grating_port = (nw[0] + cnt * self.grating_sep, gr_y)
            specs.append(i3.Place(f"gr_out_{cnt}:out", pos_grating_port, angle=-90))

            tt_grating_port = self.grating.get_default_view(i3.LayoutView).ports["out"].trace_template

            wp_x += self.wav_sep
            if pos_grating_port[0] > wp_x:
                wp_y -= self.wav_sep
            else:
                wp_y += self.wav_sep

            if np.abs(pos_grating_port[0] - wp_x) < 2 * self.bend_radius:
                intermediate_port = i3.OpticalPort(
                    name=f"intermediate_port_{cnt}",
                    position=(wp_x, wp_y),
                    angle=90,
                    trace_template=tt_grating_port,
                )

                specs.extend(
                    [
                        i3.ConnectBend(f"gr_out_{cnt}:out", intermediate_port, bend_radius=self.bend_radius),
                        i3.ConnectManhattan(
                            intermediate_port.flip_copy(),
                            f"DUT:out{n_gratings - cnt}",
                            bend_radius=self.bend_radius,
                            min_straight=0.0,
                            start_straight=0.0,
                        ),
                    ]
                )

            else:
                specs.append(
                    i3.ConnectManhattan(
                        f"gr_out_{cnt}:out",
                        f"DUT:out{n_gratings - cnt}",
                        control_points=[i3.H(wp_y), i3.V(wp_x)],
                        bend_radius=self.bend_radius,
                        min_straight=0.0,
                        start_straight=0.0,
                    )
                )

        return specs

The second step is the definition of the specifications. The first thing we do is to calculate the size_info of the DUT. This object contains information on the size of the DUT and allows for the relative positioning of the grating couplers. The y-position of the grating couplers gr_y is calculated using the y-value of the north-west corner of the DUT plus some room for the waveguides.

The formula

        gr_y = nw[1] + (1 + np.ceil(si_dut.width / self.grating_sep)) * self.wav_sep + 5 * self.bend_radius

was obtained through thinking and some iterations.

The definition of the routing specifications is based on the routing heuristic using conditional waypoints and connectors. In the following picture we annotated the layout of RouteSplitterTreeNorth with the points where the connectors initially bend.

../../../../_images/annotated_routed_tree_4.png

The red dots are the bending points that are specified in the manhattan route by where the horizontal and the vertical parts of the route have to pass through i3.H(wp_y) and i3.V(wp_x) (see i3.H and i3.V). This ensures that the route will bend at (wp_x, wp_y). The brown dot is the waypoint used in the case of an S-bend route (wp_x, wp_y).

The value of wp_x and wp_y is increased or decreased depending on the relative position of the current waypoint and the grating coupler.

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_splitter_tree_routed_north.py
            wp_x += self.wav_sep
            if pos_grating_port[0] > wp_x:
                wp_y -= self.wav_sep
            else:
                wp_y += self.wav_sep

Similarly, the connector itself depends on the relative position as well. An S-bend is needed in case there is no space for a lateral bend when the grating coupler sits more or less on top of (wp_x, wp_y).

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_splitter_tree_routed_north.py
            if np.abs(pos_grating_port[0] - wp_x) < 2 * self.bend_radius:
                intermediate_port = i3.OpticalPort(
                    name=f"intermediate_port_{cnt}",
                    position=(wp_x, wp_y),
                    angle=90,
                    trace_template=tt_grating_port,
                )

                specs.extend(
                    [
                        i3.ConnectBend(f"gr_out_{cnt}:out", intermediate_port, bend_radius=self.bend_radius),
                        i3.ConnectManhattan(
                            intermediate_port.flip_copy(),
                            f"DUT:out{n_gratings - cnt}",
                            bend_radius=self.bend_radius,
                            min_straight=0.0,
                            start_straight=0.0,
                        ),
                    ]
                )

            else:
                specs.append(
                    i3.ConnectManhattan(
                        f"gr_out_{cnt}:out",
                        f"DUT:out{n_gratings - cnt}",
                        control_points=[i3.H(wp_y), i3.V(wp_x)],
                        bend_radius=self.bend_radius,
                        min_straight=0.0,
                        start_straight=0.0,
                    )
                )

In what follows, we plot the layout of a routed splitter tree with varying number of levels.

The instances (including the connector instances) are automatically placed in the Layout view based on the specs using i3.place_and_route.

1 level

../../../../_images/routed_tree_1.png

3 levels

../../../../_images/routed_tree_3.png

6 levels

../../../../_images/routed_tree_6.png

Example 2: Splitter Tree West

This example is similar to the first one, but we route the splitter tree to grating couplers placed west of the circuit.

luceda-academy/training/topical_training/connectors_trace_templates_wg/4_addendum_customized_bundle/example_splitter_tree_routed_west.py
import si_fab.all as pdk
from ipkiss3 import all as i3
from pteam_library_si_fab.all import SplitterTree
import numpy as np
import os


class RouteSplitterTreeWest(i3.Circuit):
    """Routed Splitter tree with grating couplers placed west."""

    dut = i3.ChildCellProperty(doc="splitter used")
    grating = i3.ChildCellProperty(doc="Splitter")
    grating_sep = i3.PositiveNumberProperty(default=50.0, doc="Separation between the gratings")
    bend_radius = i3.PositiveNumberProperty(default=20.0, doc="Bend Radius")
    wav_sep = i3.PositiveNumberProperty(default=8.0, doc="Separation between waveguides")

    def _default_grating(self):
        return pdk.FC_TE_1550()

    def _default_bend_radius(self):
        return 20.0

    def _default_dut(self):
        return SplitterTree(name=self.name + "_DUT", levels=6)

    def _default_insts(self):
        instances = dict()
        instances["DUT"] = self.dut
        for inst in range(2**self.dut.levels):
            instances[f"gr_out_{inst}"] = self.grating
        instances["gr_in"] = self.grating
        return instances

    def _default_specs(self):
        dut_lo = self.dut.get_default_view(i3.LayoutView)
        si_dut = dut_lo.size_info()
        # dut_port
        sw = si_dut.south_west
        nw = si_dut.north_west
        n_gratings = 2**self.dut.levels
        gr_x = sw[0] - n_gratings * self.wav_sep - 3 * self.bend_radius
        specs = [
            i3.Place("DUT", (0, 0)),
            i3.Place("gr_in:out", (gr_x, sw[1] - self.grating_sep)),
            i3.ConnectManhattan("gr_in:out", "DUT:in", bend_radius=self.bend_radius, min_straight=0),
        ]

        wp_x = nw[0] - sw[0]
        for cnt in range(n_gratings):
            pos_grating_port = (gr_x, sw[1] + cnt * self.grating_sep)
            specs.append(i3.Place(f"gr_out_{cnt}:out", pos_grating_port))
            tt_grating_port = self.grating.get_default_view(i3.LayoutView).ports["out"].trace_template

            wp_y = nw[1] + (cnt + 1) * self.wav_sep + 2 * self.bend_radius
            if pos_grating_port[1] > wp_y:
                wp_x += self.wav_sep
            else:
                wp_x -= self.wav_sep

            intermediate_port = i3.OpticalPort(
                name=f"intermediate_port_{cnt}",
                position=(wp_x, wp_y),
                angle=180,
                trace_template=tt_grating_port,
            )
            if np.abs(pos_grating_port[1] - wp_y) < 2 * self.bend_radius:
                specs.append(
                    i3.ConnectBend(
                        f"gr_out_{cnt}:out",
                        intermediate_port,
                        f"connect_gc_{cnt}",
                        bend_radius=self.bend_radius,
                    )
                )
            else:
                specs.append(
                    i3.ConnectManhattan(
                        f"gr_out_{cnt}:out",
                        intermediate_port,
                        bend_radius=self.bend_radius,
                    )
                )

            specs.append(
                i3.ConnectManhattan(
                    f"DUT:out{n_gratings - cnt}",
                    intermediate_port.flip_copy(),
                    control_points=[
                        i3.V(self.bend_radius + cnt * self.wav_sep, relative_to=f"DUT:out{n_gratings - cnt}")
                    ],
                    bend_radius=self.bend_radius,
                    min_straight=0.0,
                )
            )

        return specs


if __name__ == "__main__":
    project_folder = "./splitter_tree_west"  # Name of the project
    if not os.path.exists(project_folder):
        os.mkdir(project_folder)

    for levels in range(1, 7):
        print(f"Number levels:{levels}")
        dut = SplitterTree(name=f"SP{levels}", levels=levels)
        cell = RouteSplitterTreeWest(name=f"Routed_tree{levels}", dut=dut)
        cell_lv = cell.Layout()
        fig = cell_lv.visualize(show=False)
        fig.savefig(
            os.path.join(project_folder, f"routed_tree_{levels}.png"),
            transparent=True,
            bbox_inches="tight",
        )
        cell_lv.write_gdsii(os.path.join(project_folder, f"splitter_tree_routed_west_{levels}.gds"))
        print("done")

The routing is parametric and can be applied to any number of levels.

1 level

../../../../_images/routed_tree_11.png

3 levels

../../../../_images/routed_tree_31.png

6 levels

../../../../_images/routed_tree_61.png