Disk Resonator with Wraparound waveguide

In this example, we construct a PCell for a parametric disk resonator with a bus waveguide which wraps partially around the disk.

Disk Resonator

Disk resonator with wraparound bus waveguide

Defining the Parametric Cell

We define a parametric cell (or PCell) as a python class. Each view of the PCell (a Layout, a Netlist, a Model …) is defined as a new class within the PCell class:

class DiskResonator(i3.PCell):

    # code of the PCell

    class Layout(i3.LayoutView):

        # code of the Layout view

PCells and views are defined by parameters, also called properties. These need to be listed in the class definition. It is good practice to identify which parameters are global to the complete PCell, and which only apply to one particular view.

For our disk resonator, as shown in the figure above, we can identify the following parameters

  • The disk radius

  • The spacing between the disk and the waveguide

  • The angle over which the bus waveguide is coupled to the disk

These are very clearly layout-specific parameters, so we can add them to the Layout view:

class DiskResonator(i3.PCell):

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6,
            doc="spacing between centerline of bus waveguide and edge of the disk",
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0,
            doc="angular span (in degrees) of the bus waveguide around the disk",
        )

Because waveguides in ipkiss are stand-alone components (also PCells), we should define the bus waveguide as a child cell of our class. Child cells should be defined at cell level, so we can add the bus waveguide there.

class DiskResonator(i3.PCell):

    bus_wg = i3.ChildCellProperty(doc="the bus waveguide")

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6,
            doc="spacing between centerline of bus waveguide and edge of the disk",
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0,
            doc="angular span (in degrees) of the bus waveguide around the disk",
        )

This is already a good start for our PCell.

Generating the Disk Layout using geometric primitives

We can now start generating the disk by drawing a circle on the layout. The dimensions of the circle are generated using the properties we defined in our Layout View:

from si_fab.technology import TECH  # noqa
import ipkiss3.all as i3


class DiskResonator(i3.PCell):
    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6,
            doc="spacing between centerline of bus waveguide and edge of the disk",
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0,
            doc="angular span (in degrees) of the bus waveguide around the disk",
        )

        def _generate_elements(self, elems):
            # define the disk as pure geometric elements
            elems += i3.Circle(layer=i3.TECH.PPLAYER.SI, radius=self.radius)
            return elems

Geometric primitives are added using the _generate_elements methods, which we have to add to our Layout view. We add two Circle elements:

  • the first circle is the disk itself, which is drawn on the waveguide layer

  • the second circle defines a trench around the disk, as some technologies require that the trench is explicitly provided.

disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
plot wrapped disk
<Figure size 640x480 with 1 Axes>

Creating a Waveguide child cell

Drawing a circle is easy. It is more difficult to calculate the waveguide, as this consists of circular segments that need to be correctly positioned. IPKISS provides a number of constructs for this. First, let’s define a waveguide for the bus. This we can do by adding a _default_bus_wg method on the PCell which creates a waveguide cell, and another method (with the same name) in the Layout view, that sets all the correct layout parameters.

class DiskResonator(i3.PCell):
    bus_wg = i3.ChildCellProperty(doc="the bus waveguide")

    def _default_bus_wg(self):
        return i3.Waveguide(name=self.name + "bus_wg")

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6,
            doc="spacing between centerline of bus waveguide and edge of the disk",
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0,
            doc="angular span (in degrees) of the bus waveguide around the disk",
        )

        def _default_bus_wg(self):
            # set the layout parameters of the waveguide

            r = self.radius + self.spacing

            bus_layout = self.cell.bus_wg.get_default_view(i3.LayoutView)
            bus_layout.set(shape=[(-r, -r), (+r, -r)])
            return bus_layout

        def _generate_instances(self, insts):
            # Adding an instance of the bus as a hierarchical component
            insts += i3.SRef(name="bus", reference=self.bus_wg)
            return insts

In the second _default_bus_wg method, we assign a shape to the bus waveguide layout. But for now we have just used a straight shape. Now we could go and calculate a shape with circular segments, for which IPKISS provides the i3.ShapeArc. But it is not that easy to do all those calculations by hand.

The _generate_instances method places a reference to the waveguide child cell in the layout.

disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
plot wrapped disk
<Figure size 640x480 with 1 Axes>

Creating a Rounded Waveguide

IPKISS provides rounded waveguides, which automatically add bends. This means you don;t have to specify the complete shape of the waveguide path, but you can provide a list of waypoints, or routes. The i3.RoundedWaveguide will automatically add bends with the specified radius at every waypoint. Using a RoundedWaveguide instead of a waveguide is fairly simple:

class DiskResonator(i3.PCell):
    bus_wg = i3.ChildCellProperty(doc="the bus waveguide")

    def _default_bus_wg(self):
        return i3.RoundedWaveguide(name=self.name + "_bus")

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6,
            doc="spacing between centerline of bus waveguide and edge of the disk",
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0,
            doc="angular span (in degrees) of the bus waveguide around the disk",
        )

        def _default_bus_wg(self):
            # set the layout parameters of the waveguide
            r = self.radius + self.spacing

            # Bus waveguide layout
            bus_layout = self.cell.bus_wg.get_default_view(i3.LayoutView)  # default bus layout view
            bus_layout.set(bend_radius=r, shape=[(-r, r), (-r, -r), (+r, -r), (r, r)], draw_control_shape=True)

            return bus_layout

        def _generate_instances(self, insts):
            # Adding an instance of the bus as a hierarchical component
            insts += i3.SRef(name="bus", reference=self.bus_wg)
            return insts

To incorporate the RoundedWaveguide cell, we just changed the _default_bus_wg in the PCell to use RoundedWaveguide instead of Waveguide, and added the additional parameters in the _default_bus_wg method in the Layout view:

  • the bend radius, which is slightly larger than that of the disk itself (adding the spacing between the disk edge and the centerline of the waveguide)

  • the control shape: with 4 control points we created a U-shaped waveguide that wraps around the disk

  • we also switch on the displaying of the control shape itself

disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
plot wrapped disk
<Figure size 640x480 with 1 Axes>

Calculating the correct control shape

One of the nice things of using rounded waveguides, is that these components are aware of their own bend size. This means you can use them to calculate the points of the control shape. In our case, we need a control shape to generate the wraparond bend:

Control shape of the bus waveguide

Control shape of the bus waveguide

The control shape consists of only 6 waypoints, and RoundedWaveguide will create the bends for these. The distance between the control points can be calculated using the get_bend_size method of RoundedWaveguide: this gives the distances between the control shape and the two end points of a bend.

class DiskResonator(i3.PCell):
    bus_wg = i3.ChildCellProperty(doc="the bus waveguide")

    def _default_bus_wg(self):
        return i3.RoundedWaveguide(name=self.name + "_bus")

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6,
            doc="spacing between centerline of bus waveguide and edge of the disk",
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0,
            doc="angular span (in degrees) of the bus waveguide around the disk",
        )

        def _default_bus_wg(self):
            # set the layout parameters of the waveguide
            r = self.radius + self.spacing
            a = 0.5 * self.bus_angle
            s = i3.TECH.WG.SHORT_STRAIGHT

            # Bus waveguide layout
            bus_layout = self.cell.bus_wg.get_default_view(i3.LayoutView)  # default bus layout view
            bus_layout.set(bend_radius=r)

            b1, b2 = bus_layout.get_bend_size(a)  # calculates the size of the waveguide bend

            # control shape for the bus waveguide (one half)
            # the RoundedWaveguide will automatically generate smooth bends
            s1 = i3.Shape([(-b1, -r)])  # point 1
            s1.add_polar(2 * b2, 180.0 - a)  # point 2
            s1.add_polar(b1 + s, 180.0)  # point 3

            # stitching 2 halves together
            bus_shape = s1.reversed() + s1.h_mirror_copy()

            # assigning the shape to the bus
            bus_layout.set(shape=bus_shape, draw_control_shape=True)  # will also draw the control shape on a doc layer
            return bus_layout

        def _generate_instances(self, insts):
            insts += i3.SRef(
                name="bus", reference=self.bus_wg
            )  # Adding an instance of the bus as a hierarchical component
            return insts

We first set the bend radius of the RoundedWaveguide. Based on that value, we can then query the size of the bends. We then use IPKISS’s shape construction methods to build a shape of 3 points (as indicated in the figure above). The add_polar method adds a point to the shape that is a given distance away at a given angle (in degrees).

The full shape is built from a reversed copy and a mirrored copy of the shape. That full shape is then assigned as control shape for the waveguide.

disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
plot wrapped disk
<Figure size 640x480 with 1 Axes>

Using Waveguide Templates

We now use a standard waveguide. However, IPKISS supports the use of waveguide templates. These are predefined PCells that describe how a waveguide is constructed (layers, widths, …). We can add a waveguide template to our PCell, and that way we can use different waveguides as bus waveguides:

class DiskResonator(i3.PCell):
    wg_template = i3.WaveguideTemplateProperty(default=i3.TECH.PCELLS.WG.DEFAULT, doc="trace template used for the bus")
    bus_wg = i3.ChildCellProperty(doc="the bus waveguide")

    def _default_bus_wg(self):
        # waveguide cell
        bus_wg = i3.RoundedWaveguide(name=self.name + "_bus", trace_template=self.wg_template)

        return bus_wg

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6, doc="spacing between centerline of bus waveguide and edge of the disk"
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0, doc="angular span (in degrees) of the bus waveguide around the disk"
        )

        def _default_bus_wg(self):
            # set the layout parameters of the waveguide
            r = self.radius + self.spacing
            a = 0.5 * self.bus_angle
            s = i3.TECH.WG.SHORT_STRAIGHT

            # Bus waveguide layout
            bus_layout = self.cell.bus_wg.get_default_view(i3.LayoutView)  # default bus layout view
            bus_layout.set(bend_radius=r, trace_template=self.wg_template)

            b1, b2 = bus_layout.get_bend_size(a)  # calculates the size of the waveguide bend

            # control shape for the bus waveguide (one half)
            # the RoundedWaveguide will automatically generate smooth bends
            s1 = i3.Shape([(-b1, -r)])
            s1.add_polar(2 * b2, 180.0 - a)
            s1.add_polar(b1 + s, 180.0)

            # stitching 2 halves together
            bus_shape = s1.reversed() + s1.h_mirror_copy()

            # assigning the shape to the bus
            bus_layout.set(shape=bus_shape, draw_control_shape=True)
            return bus_layout

        def _generate_instances(self, insts):
            insts += i3.SRef(
                name="bus", reference=self.bus_wg
            )  # Adding an instance of the bus as a hierarchical component
            return insts

We see three changes when adding the support for waveguide templates:

  • we add a property wg_template to the PCell

  • we have to pass the wg_template as a parameter to RoundedWaveguide

  • we have to set the wg_template in the Layout view as well.

disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
plot wrapped disk
<Figure size 640x480 with 1 Axes>

Adding Ports

When we want to incorporate the disk in a circuit, it is important that the input and output are correctly defined. In this case, these correspond to the inputs and outputs of the bus waveguide. Still we have to explicitly define them in the Layout view:

class DiskResonator(i3.PCell):
    wg_template = i3.WaveguideTemplateProperty(default=i3.TECH.PCELLS.WG.DEFAULT, doc="trace template used for the bus")
    bus_wg = i3.ChildCellProperty(doc="the bus waveguide")

    def _default_bus_wg(self):
        # waveguide cell
        bus_wg = i3.RoundedWaveguide(name=self.name + "_bus", trace_template=self.wg_template)

        return bus_wg

    class Layout(i3.LayoutView):
        radius = i3.PositiveNumberProperty(default=10.0, doc="radius of the disk")
        spacing = i3.PositiveNumberProperty(
            default=0.6, doc="spacing between centerline of bus waveguide and edge of the disk"
        )
        bus_angle = i3.PositiveNumberProperty(
            default=60.0, doc="angular span (in degrees) of the bus waveguide around the disk"
        )

        def _default_bus_wg(self):
            # set the layout parameters of the waveguide
            r = self.radius + self.spacing
            a = 0.5 * self.bus_angle
            s = i3.TECH.WG.SHORT_STRAIGHT

            # Bus waveguide layout
            bus_layout = self.cell.bus_wg.get_default_view(i3.LayoutView)  # default bus layout view
            bus_layout.set(bend_radius=r, trace_template=self.wg_template)

            b1, b2 = bus_layout.get_bend_size(a)  # calculates the size of the waveguide bend

            # control shape for the bus waveguide (one half)
            # the RoundedWaveguide will automatically generate smooth bends
            s1 = i3.Shape([(-b1, -r)])
            s1.add_polar(2 * b2, 180.0 - a)
            s1.add_polar(b1 + s, 180.0)

            # stitching 2 halves together
            bus_shape = s1.reversed() + s1.h_mirror_copy()

            # assigning the shape to the bus
            bus_layout.set(shape=bus_shape, draw_control_shape=True)
            return bus_layout

        def _generate_instances(self, insts):
            # Adding an instance of the bus as a hierarchical component
            insts += i3.SRef(name="bus", reference=self.bus_wg)
            return insts

        def _generate_elements(self, elems):
            # define the disk as pure geometric elements
            elems += i3.Circle(layer=i3.TECH.PPLAYER.SI, radius=self.radius)
            return elems

        def _generate_ports(self, ports):
            # the connection to the outside world
            ports += self.instances["bus"].ports
            return ports

We just pass on the ports from the “bus” instance.

disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
plot wrapped disk
<Figure size 640x480 with 1 Axes>

Recap

In this examples we built up a Disk resonator from the ground up, using a combination of geometric primitives and the built-in rounded waveguides.