Instances and ports¶
GDS allows your the cell once in memory and instance or Instance the cell multiple times.
As you build cells you can instantiate other cells. Adding an instance is like having a pointer to a cell.
The GDSII specification allows the use of instances, and similarly kfactory uses them (with the create_inst()
function).
what is an instance? Simply put: An instance does not contain any geometry. It only points to an existing geometry.
Say you have a ridiculously large polygon with 100 billion vertices that you call BigPolygon. It's huge, and you need to use it in your design 250 times. Well, a single copy of BigPolygon takes up 1MB of memory, so you don't want to make 250 copies of it You can instead instances the polygon 250 times. Each instance only uses a few bytes of memory -- it only needs to know the memory address of BigPolygon, position, rotation and mirror. This way, you can keep one copy of BigPolygon and use it again and again.
You can start by making a blank KFactory
and add a single polygon to it.
import kfactory as kf
# Define Layers
class LayerInfos(kf.LayerInfos):
WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1,0)
WGEX: kf.kdb.LayerInfo = kf.kdb.LayerInfo(2,0) # WG Exclude
CLAD: kf.kdb.LayerInfo = kf.kdb.LayerInfo(4,0) # cladding
FLOORPLAN: kf.kdb.LayerInfo = kf.kdb.LayerInfo(10,0)
# Make the layout object aware of the new layers:
LAYER = LayerInfos()
kf.kcl.infos = LAYER
# Create a blank Cell
p = kf.KCell()
# Add a polygon
xpts = [0, 0, 5, 6, 9, 12]
ypts = [0, 1, 1, 2, 2, 0]
p.shapes(p.kcl.find_layer(2, 0)).insert(
kf.kdb.DPolygon([kf.kdb.DPoint(x, y) for x, y in zip(xpts, ypts)])
)
# plot the Cell with the polygon in it
p
Now, you want to reuse this polygon repeatedly without creating multiple copies of it.
To do so, you need to make a second blank Cell
, this time called c
.
In this new Cell you instance our Cell p
which contains our polygon.
c = kf.KCell(name="Cell_with_instances") # Create a new blank Cell
poly_ref = c.create_inst(p) # instance the Cell "p" that has the polygon in it
c
you just made a copy of your polygon -- but remember, you didn't actually
make a second polygon, you just made a instance (aka pointer) to the original
polygon. Let's add two more instances to c
:
poly_ref2 = c.create_inst(p) # instance the Cell "p" that has the polygon in it
poly_ref3 = c.create_inst(p) # instance the Cell "p" that has the polygon in it
c
Now you have 3x polygons all on top of each other. Again, this would appear
useless, except that you can manipulate each instance independently. Notice that
when you called c.add_ref(p)
above, we saved the result to a new variable each
time (poly_ref
, poly_ref2
, and poly_ref3
)? You can use those variables to
reposition the instances.
poly_ref2.transform(
kf.kdb.DCplxTrans(1, 15, False, 0, 0)
) # Rotate the 2nd instance we made 15 degrees
poly_ref3.transform(
kf.kdb.DCplxTrans(1, 30, False, 0, 0)
) # Rotate the 3rd instance we made 30 degrees
c
Now you're getting somewhere! You've only had to make the polygon once, but you're able to reuse it as many times as you want.
Modifying the instances¶
What happens when you change the original geometry of the instance? In your case, your instances in
c
all point to the Cell p
that with the original polygon. Let's try
adding a second polygon to p
.
First you add the second polygon and make sure P
looks like you expect:
# Add a 2nd polygon to "p"
xpts = [14, 14, 16, 16]
ypts = [0, 2, 2, 0]
p.shapes(p.kcl.find_layer(1, 0)).insert(
kf.kdb.DPolygon([kf.kdb.DPoint(x, y) for x, y in zip(xpts, ypts)])
)
p
That looks good. Now let's find out what happened to c
that contains the
three instances. Keep in mind that you have not modified c
or executed any
functions/operations on c
-- all you have done is modify p
.
c
When you modify the original geometry, all of the instances automatically reflect the modifications. This is very powerful, because you can use this to make very complicated designs from relatively simple elements in a computation- and memory-efficient way.
Let's try making instances a level deeper by referencing c
. Note here we use
the <<
operator to add the instances -- this is just shorthand, and is
exactly equivalent to using add_ref()
c2 = kf.KCell(name="array_sample") # Create a new blank Cell
d_ref1 = c2.create_inst(c) # instance the Cell "c" that 3 instances in it
d_ref2 = c2 << c # Use the "<<" operator to create a 2nd instance to c
d_ref3 = c2 << c # Use the "<<" operator to create a 3rd instance to c
d_ref1.transform(kf.kdb.DTrans(20.0, 0.0))
d_ref2.transform(kf.kdb.DTrans(40.0, 0.0))
c2
As you've seen you have two ways to add an instance to our cell:
- create the instance and add it to the cell
c = kf.KCell(name="instance_sample")
w = kf.cells.straight.straight(length=10, width=0.6, layer=LAYER.WG)
wr = kf.kdb.CellInstArray(w._kdb_cell, kf.kdb.Trans.R0)
c.insert(wr)
c
- or do it in a single line
c = kf.KCell(name="instance_sample_shorter_syntax")
wr = c << kf.cells.straight.straight(length=10, width=0.6, layer=LAYER.WG)
c
in both cases you can move the instance wr
after created
c = kf.KCell(name="two_instances")
wr1 = c << kf.cells.straight.straight(length=10, width=0.6, layer=LAYER.WG)
wr2 = c << kf.cells.straight.straight(length=10, width=0.6, layer=LAYER.WG)
wr2.transform(kf.kdb.DTrans(0.0, 10.0))
c.add_ports(wr1.ports, prefix="top_")
c.add_ports(wr2.ports, prefix="bot_")
c.ports
['Port(name: top_o1, dwidth: 0.6, trans: r180 *1 0,0, layer: WG, port_type: optical)', 'Port(name: top_o2, dwidth: 0.6, trans: r0 *1 10,0, layer: WG, port_type: optical)', 'Port(name: bot_o1, dwidth: 0.6, trans: r180 *1 0,10, layer: WG, port_type: optical)', 'Port(name: bot_o2, dwidth: 0.6, trans: r0 *1 10,10, layer: WG, port_type: optical)']
You can also auto_rename ports using gdsfactory default convention, where ports are numbered clockwise starting from the bottom left
c.auto_rename_ports()
c.ports
['Port(name: o1, dwidth: 0.6, trans: r180 *1 0,0, layer: WG, port_type: optical)', 'Port(name: o4, dwidth: 0.6, trans: r0 *1 10,0, layer: WG, port_type: optical)', 'Port(name: o2, dwidth: 0.6, trans: r180 *1 0,10, layer: WG, port_type: optical)', 'Port(name: o3, dwidth: 0.6, trans: r0 *1 10,10, layer: WG, port_type: optical)']
c
Arrays of instances¶
In GDS, there's a type of structure called a "Instance" which takes a cell and repeats it NxM times on a fixed grid spacing. For convenience, Cell
includes this functionality with the add_array() function.
Note that CellArrays are not compatible with ports (since there is no way to access/modify individual elements in a GDS cellarray)
Let's make a new Cell and put a big array of our Cell c
in it:
import kfactory as kf
print(kf.__version__)
c = kf.cells.straight.straight(length=10, width=0.6, layer=LAYER.WG)
c3 = kf.KCell() # Create a new blank Cell
aref = c3.create_inst(
c, na=1, nb=3, a=kf.kdb.Vector(20000, 0), b=kf.kdb.Vector(0, 15000)
) # instance the Cell "c" 3 instances in it with a 3 rows, 1 columns array
c3.add_ports(aref.ports)
c3.draw_ports()
c3.plot()
0.21.7
You can still access the ports for each instance
aref['o1', 0, 1]
Port(name: o1, dwidth: 0.6, trans: r180 *1 0,15, layer: WG, port_type: optical)
c.ports
['Port(name: o1, dwidth: 0.6, trans: r180 *1 0,0, layer: WG, port_type: optical)', 'Port(name: o2, dwidth: 0.6, trans: r0 *1 10,0, layer: WG, port_type: optical)']
connect instances¶
We have seen that once you create a instance you can manipulate the instance to move it to a location. Here we are going to connect that instance to a port. Remember that we follow that a certain instance source
connects to a destination
port
c = kf.KCell()
bend = kf.cells.euler.bend_euler(radius=5, width=1, layer=LAYER.WG)
b1 = c << bend
b2 = c << bend
b2.connect("o1", b1.ports["o2"])
c
c = kf.KCell()
b1 = c << kf.cells.euler.bend_euler(radius=5, width=1, layer=LAYER.WG, angle=30)
b2 = c << kf.cells.euler.bend_euler(radius=5, width=1, layer=LAYER.WG, angle=30)
b2.connect("o1", b1.ports["o2"])
c.show()
c
This non-manhattan connect will create less than 1nm gaps that you can fix by flattening the references.
c = kf.KCell()
b1 = c << kf.cells.euler.bend_euler(radius=5, width=1, layer=LAYER.WG, angle=30)
b2 = c << kf.cells.euler.bend_euler(radius=5, width=1, layer=LAYER.WG, angle=30)
b2.connect("o1", b1.ports["o2"])
b2.flatten()
c.show()
c
Which fixes the issue.
Port naming¶
You have the freedom to name the ports as you want, and you can use c.auto_rename_ports
to rename them later on.
Here is the default naming convention.
Ports are numbered clock-wise starting from the bottom left corner
Optical ports have o
prefix and Electrical ports e
prefix
Here is the default one we use (clockwise starting from bottom left west facing port)
3 4
|___|_
2 -| |- 5
| |
1 -|______|- 6
| |
8 7
pins¶
You can add pins (port markers) to each port. Each foundry PDK does this differently, so gdsfactory supports all of them.
- square with port inside the cell
- square centered (half inside, half outside cell)
- triangular
- path (SiEPIC)
by default KCell.show()
will add triangular pins, so you can see the direction of the port in Klayout.
c = kf.cells.euler.bend_euler(radius=5, width=1, layer=LAYER.WG, angle=90)
c.draw_ports()
c