Step 4: Using rounded waveguides

Result

In this step, we add the rounded bends to the waveguides in our Ring-Loaded Mach-Zehnder Interferometer (RLMZI). We also improve our previous example by using some Python for-loops, which automates some of the work and reduces the amount of code.

Routes and ports

Routes and ports to create a RLMZI.

Illustrates

  1. how to use a waveguide template with a RoundedWaveguide.

Files (see: tutorials/layout_advanced/04-rlmzi-rounding)

There are four files contained in this step.

  • ring.py : this is a self-contained python file that contains a description of our ring resonator.

  • dircoup.py : this is a self-contained python file that contains a description of a directional coupler PCell.

  • rlmzi.py : this is a self-contained python file that contains a description of a RLMZI PCell.

  • execute.py : this is the file that is executed by python. It instantiates an rlmzi and performs operations on it.

How to run this example

To run the example, run ‘execute.py’.

Defining rounded waveguides

All that is needed to create a rounded waveguide from an existing waveguide template is to use the template together with the i3.RoundedWaveguide <ipkiss3.pcell.photonics.rounded_waveguide.RoundedWaveguide> PCell. This will allow you to define the path of the waveguide using waypoints and the bends will be added automatically.

Tip

Use waveguides whenever it’s possible. Since you can change waveguide templates rather easily it will make your component much more generic. In particular, it will be a lot easier to port your component from one technology to another, because a lot of technology related properties are encoded in the waveguide templates.

Let’s now see how we modify our method _default_wgs(self) to include rounding:

class RingLoadedMZI(i3.PCell):

    # ...

    # 2. We define a waveguide template and the waveguide cells
    wg_template = i3.WaveguideTemplateProperty(default=i3.TECH.PCELLS.WG.DEFAULT)
    wgs = i3.ChildCellListProperty(doc="list of waveguides")

    #3. We first define the 4 rounded waveguides that we need at the PCELL level.
    def _default_wgs(self):
        w1 = i3.RoundedWaveguide(name=self.name+"_w1",
                                 trace_template=self.wg_template)
        w2 = i3.RoundedWaveguide(name=self.name+"_w2",
                                 trace_template=self.wg_template)
        # ...
        return w1, w2, w3, w4

So instead of directly calling the waveguide template (which creates a waveguide pcell), we use the waveguide template as a parameter in i3.RoundedWaveguide <ipkiss3.pcell.photonics.rounded_waveguide.RoundedWaveguide>. then we return the 4 waveguide pcells.

The bend radius of the rounded waveguide is defined in the layout view of RoundedWaveguide. We can set that parameter in de _default_wgs method in the layout view of our RLMZI class

class RingLoadedMZI(i3.PCell):

    # ...

    # 2. We define a waveguide template and the waveguide cells
    wg_template = i3.WaveguideTemplateProperty(default=i3.TECH.PCELLS.WG.DEFAULT)
    wgs = i3.ChildCellListProperty(doc="list of waveguides")

    #3. We first define the 4 rounded waveguides that we need at the PCELL level.
    def _default_wgs(self):
        w1 = i3.RoundedWaveguide(name=self.name+"_w1",
                                 trace_template=self.wg_template)
        w2 = i3.RoundedWaveguide(name=self.name+"_w2",
                                 trace_template=self.wg_template)
        # ...
        return w1, w2, w3, w4

   # ...

    class Layout(i3.LayoutView):
        bend_radius = i3.PositiveNumberProperty(default=i3.TECH.WG.BEND_RADIUS)

        def _default_wgs(self):

            splitter, ring1, ring2, combiner = self._get_components()
            w1_cell, w2_cell, w3_cell, w4_cell  = self.cell.wgs
            wg_template = self.wg_template

            # Defining the north arm
            w1_layout = w1_cell.get_default_view(i3.LayoutView)
            w1_layout.set(bend_radius=self.bend_radius,
                          # We set the shape using RouteManhattan and the ports.
                          shape=i3.RouteManhattan(start_port=splitter.ports["out_1"],
                                                  end_port=ring1.ports["in"],
                                                  bend_radius=self.bend_radius),
                          trace_template=wg_template)

            #...

            return w1_layout, w2_layout, w3_layout, w4_layout

We explicitly set the bend radius as a parameter of our rounded waveguide.

The remainder of our RLMZI is unchanged compared to our previous example.

Automating and writing less code

Now you might have noticed that in our code fragments above we wrote quite some #... statements. That is quite often because we just repeat the same code fragments several times. This kind of copy-and-paste code is usually not recommended. There is a chance that somewhere you might have forgotten to rename a variable, and if you want to change something to your code in the future, you need to make the change several times.

Python is a powerful programming language, and it provides many constructs to eliminate such repetitive code. In our RLMZI example, the most obvious source of duplicate code is in the definition of our 4 waveguides. We can easily simplify the creation using a for-loop:

class RingLoadedMZI(i3.PCell):

    # ...

    # 2. We define a waveguide template and the waveguide cells
    wg_template = i3.WaveguideTemplateProperty(default=i3.TECH.PCELLS.WG.DEFAULT)
    wgs = i3.ChildCellListProperty(doc="list of waveguides")

    #3. We first define the 4 rounded waveguides that we need at the PCELL level.
    def _default_wgs(self):
        wgs=[]
        for i in range(4):
            wg = i3.RoundedWaveguide(name=self.name+"_w"+str(i+1),
                                     trace_template=self.wg_template)
            wgs.append(wg)
        return wgs

This does not really reduce the number of lines in our methods, but we have written the definition of the RoundedWaveguide cell only once. If we want to update it in the future, it will take less effort. (Python expert programmers will immediately see ways to reduce this method to only one or two lines of code using :term:list comprehension:, but this would make the example less readable). Also note that we used a string operation to calculate the unique name of each child cell.

We can apply the same principle for settgni the parameters of the rounded waveguides in the layout view:

class RingLoadedMZI(i3.PCell):

    # ...

    class Layout(i3.LayoutView):
        bend_radius = i3.PositiveNumberProperty(default=i3.TECH.WG.BEND_RADIUS)

        def _default_wgs(self):
            splitter, ring1, ring2, combiner = self._get_components()
            wg_cells = self.cell.wgs
            wg_template = self.wg_template

            # Defining the connections between ports using a tuple shorthand
            connections = [ (splitter.ports["out_1"] , ring1.ports["in"]), # splitter to ring1
                            (combiner.ports["in_1"] , ring1.ports["out"]), # combiner to ring1
                            (splitter.ports["out_2"] , ring2.ports["in"]), # splitter to ring2
                            (combiner.ports["in_2"] , ring2.ports["out"]), # combiner to ring2
                            ]

            # Updating the 4 layout views of the waveguides using a loop
            wg_layouts = []
            for wg_cell, connection in zip(wg_cells, connections):
                wg_layout = wg_cell.get_default_view(i3.LayoutView)
                wg_layout.set(bend_radius=self.bend_radius,
                              shape=i3.RouteManhattan(start_port=connection[0],
                                                      end_port=connection[1],
                                                      bend_radius=self.bend_radius),
                              trace_template=wg_template)

                wg_layouts.append(wg_layout)

            return wg_layouts

This is slightly more involved, as we now have to specify the input and output port of the RouteManhattan shape. This we do by making a simple list of _tuples_ with the input port and output port. This is quite readable and it provides all the connectivity information in one simple list. In the for loop we use the _zip_ command to map the 4 waveguide cells on the connection tuples.

Finally, we can also automate the placement of the waveguide instances:

class RingLoadedMZI(i3.PCell):

    # ...

    class Layout(i3.LayoutView):

    # ...

        def _generate_instances(self, insts):
            insts += self._get_components()
            for i, wg_layout in enumerate(self.wgs):
                insts += i3.SRef(reference=wg_layout, name="w"+str(i))
            return insts

Here, we use the Python _enumerate_ command and string operations to give a unique name to each instance

Recap

In this step you have learned how to use add rounded waveguides and how to use python for-loops to make your code easier to maintain. You should now be able to define hierarchical components and use waveguides to connect the different sub-components: congratulations.