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
).
Illustrates
how to use ChildCells. ChildCells are used to add sub-cells to a PCell.
how to do transformations (Translation and VMirror).
how to place layout instances of the ChildCells in the layout view.
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 ofring_1
.tworings.get_default_view(i3.LayoutView).ring_1
-> refers to the Layout view ofring_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.
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
Here we first calculate the transformations
t1
andt2
: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)
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 thering_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()
We create
my_ring_1
andmy_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.
We instantiate the TwoRings class passing the
my_ring_1
and themy_ring_2
as parameters.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 ofring_1
(with its default properties).ring_1.set(ring_radius=5.0)
: Set thering_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.