Example of a customized Place and Route cell

Result

In this example, we construct our own Place and Route Cell based on one of the standard PICAZZO Place and Route cells: PlaceAndAutoRoute. We will start with a static place and route cell, and then build our own specialized PCell to link together 4 ring resonators back-to-back:

Four Rings generated by a custom place and route cell.

Illustrates

  1. how to use the PICAZZO place and route cell
  2. how to subclass this cell to specialize it
  3. how to use _default methods to automate much of the placement

Files (see: samples/ipkiss3/samples/place_and_route)

There are 4 files contained in this step.

How to run this example

To run the different parts of the exanple, run ‘execute.py’, ‘execute2.py’, ‘execute3.py’ and ‘execute4.py’

Using PlaceAndAutoRoute

PlaceAndAutoRoute is a generic PICAZZO component that allows you to easily define connectivity between a number of child cells. When placing these child cells, PlaceAndAutoRoute will then generate all the waveguides (as separate PCells) needed to connect the child cells together. This example is available in execute.py

We will start with a simple example, defining a Ring-Loaded Mach-Zehnder Interferometer (RLMZI) with only a few lines of code. The simplified schematic of our RLMZI looks like this:

Simplified schematic of a ring-loaded MZI.

The lines in red indicate not just logical connections, but optical waveguides. First, we create a splitter and a ring resonator. We will reuse the splitter as a combiner, and use the same ring resonator in both arms, but we can just as well use a separate combiner and two different rings (or any other type of device to put in the MZI’s arms)

from picazzo3.filters.ring import RingRectNotchFilter
from picazzo3.wg.splitters import WgY90Splitter


my_ring = RingRectNotchFilter(name="my_ring")
my_splitter = WgY90Splitter(name="my_splitter")

Then, we create a PlaceAndAutoRoute cell. We specify that we will use my_splitter twice, and my_ring as well. We do this in the ‘child_cells’ property, which is a dictionary with the names of the child cell instances (you can have multiple instances of each child cell) as keys.

We then define the connectivity. This is done through a list of ‘tuples’ with the end terms of each link. We identify the terms through the name of the instance and the name of the term, like ‘inst:term’. The terms of the PICAZZO ring resonators are named ‘in1’, ‘out1’, … with the number as the index of the bus waveguide.

from picazzo3.routing.place_route import PlaceAndAutoRoute
pr = PlaceAndAutoRoute(name="my_place_and_route",
                       child_cells={"splitter": my_splitter,
                                    "combiner": my_splitter,
                                    "arm1" : my_ring,
                                    "arm2" : my_ring},
                       links=[("splitter:arm1", "arm1:in"   ),
                              ("arm1:out"   , "combiner:arm1"),
                              ("splitter:arm2", "arm2:in"   ),
                              ("arm2:out"   , "combiner:arm2")],
                       external_port_names={"splitter:center": "in",
                                            "combiner:center": "out"}
                        )

In the PCell we specify the connectivity we want. From this connectivity, the PlaceAndAutoRoute will now create additional waveguide child cells, and generate the real netlist connecting all the child cells and waveguide together. This expanded circuit will look like this:

Expanded schematic of the ring-loaded MZI.

The red lines have been replaced with Waveguide PCells, which are stored in the (locked) property waveguides:

>>> print pr.waveguides

[<RoundedWaveguide 'PCELL_1_link1'>,
 <RoundedWaveguide 'PCELL_1_link2'>,
 <RoundedWaveguide 'PCELL_1_link3'>,
 <RoundedWaveguide 'PCELL_1_link4'>]

The netlist view of PlaceAndAutoroute will generate all the connections, including those between the new waveguide PCells, automatically

>>> netlist = pr.Netlist()
>>> print netlist.instances

InstanceDict([('splitter', <Single instance in netlist of my_splitter>),
              ('combiner', <Single instance in netlist of my_splitter>),
              ('arm2', <Single instance in netlist of my_ring>),
              ('arm1', <Single instance in netlist of my_ring>),
              ('link1', <Single instance in netlist of PCELL_1_link1>),
              ('link2', <Single instance in netlist of PCELL_1_link2>),
              ('link3', <Single instance in netlist of PCELL_1_link3>),
              ('link4', <Single instance in netlist of PCELL_1_link4>)])

>>> print netlist.terms

TermDict([('splitter_center', <OpticalTerm 'splitter_center'>),
          ('combiner_center', <OpticalTerm 'combiner_center'>)])

We see indeed 8 instances in the netlist (4 child cells and 4 waveguides), and only two external Terms, corresponding to the input and output of the splitter and combiner, respectively.

We can now generate the Layout. Here we still have to specify the transformations or positions of the child cells. The ring resonators we position to the South-East and North-East of the splitter. The combiner we place further to the East, and we apply a horizontal flip.

layout = pr.Layout(child_transformations={"arm1": (30, -30),
                                          "arm2": (30, 30),
                                          "combiner": i3.HMirror(0.0)+i3.Translation((60, 0))},
                   bend_radius=10.0,
                   manhattan=True
                   )

This will result in the following layout:

A simple Ring-Loaded MZI defined using PlaceAndAutoRoute

Note

Of course, this smart place and route cell will only work with PCells which are well constructed: an important requirement is that the Terms in the netlist match the Ports in the layout: the same number, as well as the same names. The same for the instances. The generation of links and waveguides is done based on the instance and port names.

Also, all connecting waveguides will use the same trace template (which can be specified in the PCell property trace_template). Therefore, all ports of the child cells will be automatically tapered to the same template. If no automatic transition can be constructed (because you have used trace templates for which IPKISS has no registered transitions) an error will be thrown.

Defining Four Rings with PlaceAndAutoRoute

Now we can use the same PlaceAndAutoRoute component to make a PCell with 4 identical rings which are connected back-to-back. This example is available in execute2.py. We will put the rings themselves in a ring configuration, as shown in the schematic diagram below:

Schematic of the four rings that we want to connect. The connections in red should be replaced by Waveguide PCells.

Again, the connections in red should be replaced with optical waveguide PCells. First, we define our ring resonator. This time, we need a ‘drop filter’, which has 4 ports.

from picazzo3.filters.ring import RingRect180DropFilter

ring = RingRect180DropFilter(name="my_ring")

Next, we create a PlaceAndAutoRoute cell with 4 instances of the same ring and 7 connections.

from picazzo3.routing.place_route import PlaceAndAutoRoute

pr = PlaceAndAutoRoute(name="my_place_and_route2",
                       child_cells={"ring1" : ring,
                                    "ring2" : ring,
                                    "ring3" : ring,
                                    "ring4" : ring},
                       links=[("ring1:out1", "ring2:in1" ),
                              ("ring1:in2" , "ring2:out2"),
                              ("ring2:out1", "ring3:in1" ),
                              ("ring2:in2" , "ring3:out2"),
                              ("ring3:out1", "ring4:in1" ),
                              ("ring3:in2" , "ring4:out2"),
                              ("ring4:in2" , "ring1:out2"),
                             ],
                       external_port_names={"ring1:in1" : "in",
                                            "ring4:out1" : "out"}
                       )

For the layout, we incrementally rotate the 4 rings over 90 degrees:

layout = pr.Layout(child_transformations={"ring1": (0, 0),
                                          "ring2": i3.Rotation(rotation=90) + i3.Translation((25.0, 25.0)),
                                          "ring3": i3.Rotation(rotation=180) + i3.Translation((0.0, 50.0)),
                                          "ring4": i3.Rotation(rotation=-90) + i3.Translation((-25.0, 25.0))
                                          }
                   )

This results in the following layout

Four Rings generated by PlaceAndAutoRoute

Making our Customized Place-and-route PCell

Now we placed 4 rings using the PlaceAndAutoRoute directly. However, if we want to reuse this type of component and make it more parametric, we can define our own PCell class by subclassing from PlaceAndRoute. This subclass is defined in four_rings.py and the example is executed using execute3.py. We then have to calculate the default values for the ‘child_cells’ and other properties by adding the necessary methods. Let’s assume that we want to provide the single ring resonator as a parameter. For this, we use a ChildCellProperty:

from ipkiss3 import all as i3
from picazzo3.routing.place_route import PlaceAndAutoRoute

class MyFourRings(PlaceAndAutoRoute):

    ring = i3.ChildCellProperty(doc="The Ring I want to Cascade")

    def _default_child_cells(self):
        return {"ring1" : self.ring,
                "ring2" : self.ring,
                "ring3" : self.ring,
                "ring4" : self.ring}

By adding the method _default_child_cells, our new PCell will calculate the correct value of ‘child_cells’ and we no longer have to provide it. The same we can do for the property ‘links’:

def _default_links(self):
    return [("ring1:out1", "ring2:in1" ),
            ("ring1:in2" , "ring2:out2"),
            ("ring2:out1", "ring3:in1" ),
            ("ring2:in2" , "ring3:out2"),
            ("ring3:out1", "ring4:in1" ),
            ("ring3:in2" , "ring4:out2"),
            ("ring4:in2" , "ring1:out2"),
            ]

If we want to properly name the external ports of our component, we can define a mapping of the available ports to the names we want to use:

def _default_external_port_names(self):
    return {"ring1:in1":   "in",
            "ring4:out1" : "out"}

On the netlist side, we don’t have to do anything special: everything will be handled by the parent class PlaceAndAutoRoute.Netlist. For the layout, we want to add some intelligence: we would like to adapt the placement to the size of the rings, rather than using a hard-coded value as we did in the previous section. We introduce a property ‘spacing’, which we calculate based on the bend size (The layout of PlaceAndAutoRoute has a method ‘get_bend90_size’ which returns the dimensions of a 90-degree bend). We will also use the bend radius of our ring resonator as a default:

class Layout(PlaceAndAutoRoute.Layout):

    spacing = i3.PositiveNumberProperty(doc="The spacing between our rings")

    # Let's use the radius of the ring as our default radius
    def _default_bend_radius(self):
        return self.ring.bend_radius


    def _default_spacing(self):/
        # calculates the bend size based on the the bend radius
        return max(self.get_bend90_size()) + i3.TECH.WG.SHORT_STRAIGHT

With this information, we can now calculate the transformations for the child cells:

def _default_child_transformations(self):
    si = self.ring.size_info()
    s = 0.5 * si.width + 0.5 * si.height + self.spacing

    return {"ring1": (0, 0),
            "ring2": i3.Rotation(rotation=90) + i3.Translation((s, s)),
            "ring3": i3.Rotation(rotation=180) + i3.Translation((0.0, 2 * s)),
            "ring4": i3.Rotation(rotation=-90) + i3.Translation((-s, s))
            }

That is all we have to do to make a quite complex PCell based on PlaceAndAutoRoute. We can now use our PCell:

from technologies import silicon_photonics
from ipkiss3 import all as i3

from picazzo3.filters.ring import RingRect180DropFilter

from four_rings import MyFourRings

my_ring = RingRect180DropFilter(name="my_ring")
my_ring.Layout(bend_radius=15.0)

pr = MyFourRings(name="my_four_rings",
                 ring = my_ring
                 )

layout = pr.Layout()

The resulting layout looks like this:

Four Rings generated by a custom place and route cell.

Simulating the MyFourRings component

We can also simulate our component (or small circuit) using Caphe. The PlaceAndAutoroute cell automatically constructs a Netlist and defines a Caphe circuit model based on that netlist. If all the model parameters of the subcomponents are properly defined, we can simulate the entire circuit easily.

First, we need to define the model parameters of the individual ring we wish to use. This is similar to defining the layout parameters (‘bend_radius’ in the last example):

from technologies import silicon_photonics
from ipkiss3 import all as i3

from picazzo3.filters.ring import RingRect180DropFilter

from four_rings import MyFourRings

my_ring = RingRect180DropFilter(name="my_ring")

cp = dict(cross_coupling1=1j*0.05**0.5,           # The coupling from bus to ring and back
          straight_coupling1=0.92**0.5,   # Straight coupling
          reflection_in1=0.002
          )

my_ring_cm = my_ring.CircuitModel(ring_length=40.0,             # we can manually specify the ring length
                                  coupler_parameters=[cp, cp]) # 2 couplers
my_ring_cm.couplers

As the ring resonator has two coupler sections, we need to supply two sets of coupler parameters. Here we assume them to be identical, and we pass the parameters as a dictionary. The last line makes sure that the coupler models are properly initialized before we use them.

Now we have a ring with a well defined model. We can now use that ring in the MyFourRings component:

pr = MyFourRings(name="my_four_rings",
                 ring=my_ring
                 )

And we can then use the circuit model and define wavelength sweep simulation to calculate the scatter matrix:

pr_cm = pr.CircuitModel() # by default, automatically derived from the netlist

import numpy as np
import pylab as plt

wavelengths =  np.linspace(1.53, 1.58, 3000)
S = pr_cm.get_smatrix(wavelengths=wavelengths)

plt.plot(wavelengths, np.abs(R['in', 'out']) ** 2, 'b', label='transmission')
plt.plot(wavelengths, np.abs(R['in', 'in']) ** 2, 'g', label='reflection')
plt.legend()
plt.show()

We get the following filter response:

Transmission and reflection of the four rings circuit.

Recap

In this example we illustrated the use of the PlaceAndAutoRoute component, and we have shown how to subclass it to make your own complex PCell, which can be used for layout but also simulation.