Note
Go to the end to download the full example code
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.
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()
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()
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()
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:
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()
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 PCellwe have to pass the
wg_template
as a parameter to RoundedWaveguidewe have to set the
wg_template
in the Layout view as well.
disk = DiskResonator()
disk_lo = disk.Layout()
disk_lo.visualize()
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()
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.