Step 1: Layout level hierarchy: Two Rings

Result

In this example we show how to use hierarchy on the layout level by placing two instances of a RingResonator (see ring.py) in an instance of TwoRings (see two_rings.py).

Two Rings

A first hierarchical layout of two rings.

Illustrates

  1. how to use ChildCells. ChildCells are used to add sub-cells to a PCell.

  2. how to do transformations (Translation and VMirror).

  3. how to place layout instances of the ChildCells in the layout view.

  4. how to define default generation methods for ChildCells.

Files (see: tutorials/layout_advanced/01-two-ring)

There are three files contained in this step.

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

  • two_rings.py : this is a self-contained python file that contains a description of a pcell containing two ring resonators.

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

How to run this example

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

Using ChildCellProperty to create Hierarchical PCells

A hierarchical PCell is a PCell that contains references to other PCells (which we call Child PCells). In this step we will create a PCell TwoRings that is composed of two RingResonator cells. To declare that a PCell has child PCells we use the property ChildCellProperty.

Our TwoRings has two child PCells, therefore we define ring_1 and ring_2 as child cells:

class TwoRings(i3.PCell):
    # The rings are defined as ChildCellProperties. This is a special property
    # for defining hierarchical PCells. In each view of the PCell of a certain type (e.g. Layout),
    # the corresponding viewtype (e.g. Layout view) of the ChildCell will automatically be available.

    ring_1 = i3.ChildCellProperty()
    ring_2 = i3.ChildCellProperty()

    def some_method_using_ring_1(self):
       print(self.ring_1) # In this method, self.ring_1 refers to the PCell ring_1.

    class Layout(i3.LayoutView):

        def some_method_using_ring_1(self):
            self.ring_1 # In this method, self.ring_1 refers to the layout view of ring_1.
            self.cell.ring_1 # In this method, self.cell.ring_1 refers the cell of ring_1.

ChildCellProperties behave a bit different than other properties. They allow access to the views of the child cells in the corresponding views of the parent cell. Here this means that for the PCell tworings=TwoRings():

  • tworings.ring_1 -> refers to the PCell instance of ring_1 .

  • tworings.get_default_view(i3.LayoutView).ring_1 -> refers to the Layout view of ring_1.

In the cases where you do need the child cell itself and not the view, you can always access the cell through the cell attribute: - tworings.get_default_view(i3.LayoutView).cell.ring_1 -> refers to the PCell instance of ring_1.

The following piece of code helps to understand this behavior.

>>> # first we specify a dummy ring, with an empty layout view
>>> class DummyRing(i3.PCell):
...     class Layout(i3.LayoutView):
...          pass # pass just tells Python that this view is empty. It contains no methods other than those in i3.LayoutView.
>>> ring_1 = DummyRing(name="myring1")
>>> ring_2 = DummyRing(name="myring2")
>>> # now we instantiate the TwoRings pcell
>>> tworings = TwoRings(ring_1=ring_1, ring_2=ring_2)
>>> tworings.ring_1 is ring_1 # Check if tworings.ring_1 is ring_1
True
>>> tworings.ring_2 is ring_2 # Check if tworings.ring_2 is ring_2
True
>>> # so far this is not surprising
>>> tworings_layout = tworings.Layout()
>>> # when we now retrieve ring_1 from the layout it will be layoutview of the ring_1 cell
... # and not the ring_1 cell itself ( as would be the case if we would use a normal property)
>>> isinstance(tworings_layout.ring_1, DummyRing.Layout) # Check if two_ring_layout.ring_1 object is a python instance of DummyRing.Layout
True
>>> isinstance(tworings_layout.ring_1, DummyRing) # Check if two_ring_layout.ring_1 object is a python instance of DummyRing
False
>>> isinstance(tworings_layout.cell.ring_1, DummyRing) # Check if two_ring_layout.cell.ring_1 object is a python instance of DummyRing
True

This behavior allows easy access to the correct views of your childcell, while maintaining access to the PCell inside the views.

Placing layout instances

To use hierarchy on the layout level we need to place an instance of the layout of each child ring in the layout of two_rings. Of course, for this to be useful, we need to be able to place the instance of each child ring at any location in the layout of two_rings. More generally, we need to apply a transformation to each child ring before placing its instance into the layout of two_rings as illustrated below.

Note

For IPKISS 2.x users, it is interesting to note that the layout instance is equivalent to the SRef.

childcell and srefs

SRefs and transformations

The following code excerpt shows how to implement this using the _generate_instances(self, insts) function in the Layout view.

class TwoRings(i3.PCell):

    ring_1 = i3.ChildCellProperty()
    ring_2 = i3.ChildCellProperty()

    # Layout view
    class Layout(i3.LayoutView):

        def _generate_instances(self, insts):

            # 1. calculate the transformations of the rings based on their properties
            t1 = i3.Translation((0.0, self.ring_1.ring_radius + 2.0))
            t2 = i3.VMirror() + i3.Translation((0.0, -self.ring_2.ring_radius - 5.0))

            # 2. Generating the instances
            # self.ring1 refers to the layout view of ring1.
            insts += i3.SRef(name="ring_1", reference=self.ring_1, transformation=t1)
            # self.ring1 refers to the layout view of ring1.
            insts += i3.SRef(name="ring_2", reference=self.ring_2, transformation=t2)

            return insts
  1. Here we first calculate the transformations t1 and t2:

    • t1: is a translation which translates the entire ring along the vector (0.0, self.ring_1.ring_radius + 2.0)

    • t2: is a sum of two transformations: First our ring is mirrored along the x-axis and then translated along (0.0, -self.ring_2.ring_radius - 5.0)

  2. Here we use SRef to create the instance and add it to the list of instances for this view. The layout view knows it should add all instances to the actual layout. Note that, as explained in the previous section, in this context, self.ring_1 points to the layout view of the ring_1 cell.

Tip

Give a sensible name to each instance. This makes it a lot easier later on when using information contained in those references.

Instantiating a hierarchical cell

Here we show how to instantiate a PCell that contains child cells.

import si_fab.all as pdk  # noqa

# load the file with our RingResonator component
from ring import RingResonator
from two_rings import TwoRings, TwoRingsWithDefaults

# 1. Creating the layouts of the two rings
my_ring_1 = RingResonator()
my_ring_1.Layout(ring_radius=5.0)
my_ring_2 = RingResonator()
my_ring_2.Layout(ring_radius=6.0)
# 2. Creating the layout view of the TwoRings class
my_two_rings = TwoRings(name="mytworings", ring_1=my_ring_1, ring_2=my_ring_2)
layout = my_two_rings.Layout()

# 3. Writing to GDSII and visualization
layout.write_gdsii("tworings.gds")
layout.visualize()

  1. We create my_ring_1 and my_ring_2, two instances of RingResonator. For both instances we choose different layout parameters by instantiating their respective layout views.

    Note

    The layout view that you instantiate here is the one that will be used inside TwoRings. If you do not explicitly instantiate the layout view, the default values for each property of the layout will be used.

  2. We instantiate the TwoRings class passing the my_ring_1 and the my_ring_2 as parameters.

  3. We write the layout to GDSII and visualize the result.

Using defaults

In the last example, we created all the necessary childcells and explicitly passed them to the hierarchical cell as a property. However, there are many use cases where we want to assign default values to our childcells so that you don’t have to create them a run time. This is done through the use of _default_xxx methods as illustrated in the following code.

class TwoRingsWithDefaults(i3.PCell):

    ring_1 = i3.ChildCellProperty()
    ring_2 = i3.ChildCellProperty()

    # Here, we provide a default value for ring_1
    def _default_ring_1(self):
        ring_1 = RingResonator(name=self.name+"_ring1")
        return ring_1
    # Here, we provide a default value for ring_2
    def _default_ring_2(self):
        ring_2 = RingResonator(name=self.name+"_ring2")
        return ring_2

    # Layout view
    class Layout(i3.LayoutView):

        # Here, we provide a default value for ring_1
        def _default_ring_1(self):
            ring_1 = self.cell.ring_1.get_default_view(i3.LayoutView) # Retrieve the layout view of ring_1
            ring_1.set(ring_radius=5.0) # Set the ring radius to 5.0
            return ring_1
        # Here, we provide a default value for ring_2
        def _default_ring_2(self):
            ring_2 = self.cell.ring_2.get_default_view(i3.LayoutView) # Retrieve the layout view of ring_2
            ring_2.set(ring_radius=4.0) # Set the ring radius to 4.0
            return ring_2

        def _generate_instances(self, insts):

        # ...

In the code above you can observe that we added two default_xxx methods for each child cell. One at the the PCell level and one in the Layout view.

At the PCell level _default_ring_1(self) creates the default value for the ChildCellProperty ring_1, which is a cell of the RingResonator() PCell type. Similarly, _default_ring_2(self) returns a cell of the RingResonator() PCell type.

Caution

It is important to provide a unique name for these child cells, otherwise you risk creating new child cells over and over again. A good practice is to use the name of the parent cell (self.name) and adding a unique suffix, such as the name of the property with an underscore (_ring or _bus).

At the layout level _default_ring_1 sets the correct parameters on the Layout view of ring_1. This happens in two steps:

  • ring_1 = self.cell.ring_1.get_default_view(i3.LayoutView) : Retrieve the default Layout view of ring_1 (with its default properties).

  • ring_1.set(ring_radius=5.0) : Set the ring_radius to 5.0.

This approach for setting the parameters of the Layout view of the childcell is highly recommended (although no strictly required). If you keep this habit, it will be a lot easier later, when programming cells with multiple views.

You can now instantiate TwoRingsWithDefaults without specifying any parameters. The values for the properties ring_1 and ring_2 will automatically be calculated by _default_ring_1 and _default_ring_2 both at the PCell and the Layout view level.

# 4. Creating a PCell with ring using the default values
my_two_rings_2 = TwoRingsWithDefaults(name="mytworings2")
my_two_rings_2.Layout().visualize()

Of course you can also override the default values by explicitly setting the property.

# 5. Defaults can also be overridden. Here we override ring_2.
my_two_rings_2.ring_2 = my_ring_2
my_two_rings_2.Layout().visualize()

Note

By overriding ring_2 the _default_ring_2 methods at the PCell level and at the view level will not be executed. In that case, the overridden cell ring_2 will be used at the PCell level of two_rings and ring_2.get_default_view(i3.LayoutView) will be used in the layout View of two_rings.

Caution setting properties of child cells of TwoRingsWithDefaults

If the child cells are automatically calculated by the parent cell, you should not modify properties on the child cell using the pattern below, since it may lead to undefined behaviour:

my_two_rings_2 = TwoRingsWithDefaults(name="mytworings2")
lay = my_two_rings_2.Layout()
lay.ring_1.ring_radius = 10.0 # This is not OK - undefined behaviour

The reason is that in this case, the parent cell is responsible of calculating its child cells. Since you are not the owner of that child cell, it is difficult to predict what would happen since the parent cell might have defined interdependencies.

If on the other hand you created the child cell yourself, it is possible to modify parameters:

ring = RingResonator(name="testring")
ring_lay = ring.Layout(ring_radius=3.0)

my_two_rings_2 = TwoRingsWithDefaults(name="mytworings2", ring_1=ring)
ring_lay.ring_radius = 10.0 # this is OK

Recap

In this step we explained how we can create a hierarchical PCell using the ChildCellProperty and how to use _default_xxx methods to calculate default values of properties.

Hierarchical PCells are an important concept to manage more complex PCells. It allows you to split your PCells up into smaller and simpler subcomponents which are easier to understand.

Now that we understand how we can build hierarchical PCells, we’ll learn how we can connect our childcells together using waveguides.