KCell¶
A KCell
is like an empty canvas, where you can add polygons, instances to other Cells and ports (to connect to other cells)
In KLayout geometries are in datatabase units (dbu) or microns. GDS uses an integer grid as a basis for geometries. The default is 0.001
, i.e. 1nm grid size (0.001 microns)
Point
,Box
,Polygon
,Edge
,Region
are in dbuDPoint
,DBox
,DPolygon
,DEdge
are in microns
Most Shape types are available as microns and dbu parts. They can be converted with <ShapeTypeDBU>.to_dtype(dbu)
to microns and with <ShapeTypeMicrons>.to_itype(dbu)
where dbu
is the the conversion of one database unit to microns.
Alternatively they can be converted with c.kcl.to_um(<ShapeTypeDBU>)
or c.kcl.to_dbu(<ShapeTypeMicrons>)
where c.kcl
is the KCell and kcl
is the KCLayout
which owns the KCell.
import kfactory as kf
import numpy as np
# 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 (essentially an empty GDS cell with some special features)
c = kf.KCell()
# Create and add a polygon from separate lists of x points and y points
# (Can also be added like [(x1,y1), (x2,y2), (x3,y3), ... ]
poly1 = kf.kdb.DPolygon(
[
kf.kdb.DPoint(-8, -6),
kf.kdb.DPoint(6, 8),
kf.kdb.DPoint(7, 17),
kf.kdb.DPoint(9, 5),
]
)
c.shapes(c.kcl.find_layer(1, 0)).insert(poly1)
polygon (-8000,-6000;6000,8000;7000,17000;9000,5000)
c.show() # show in klayout
c.plot()
Exercise :
Make a cell similar to the one above that has a second polygon in layer (2, 0)
Solution :
c = kf.KCell()
points = np.array([(-8, -6), (6, 8), (7, 17), (9, 5)])
poly = kf.polygon_from_array(points)
c.shapes(c.kcl.find_layer(1, 0)).insert(poly1)
c.shapes(c.kcl.find_layer(2, 0)).insert(poly1)
c
c = kf.KCell()
textgenerator = kf.kdb.TextGenerator.default_generator()
t = textgenerator.text("Hello!", c.kcl.dbu)
c.shapes(kf.kcl.find_layer(1, 0)).insert(t)
c.show() # show in klayout
c.plot()
r = kf.kdb.DBox(-2.5, -5, 2.5, 5)
r
(-2.5,-5;2.5,5)
Add instances to the new geometry to c, our blank cell
c = kf.KCell()
c.shapes(c.kcl.find_layer(1, 0)).insert(r)
c.shapes(c.kcl.find_layer(2, 0)).insert(r)
c
c = kf.KCell()
textgenerator = kf.kdb.TextGenerator.default_generator()
text1 = t.transformed(
kf.kdb.DTrans(0.0, 10.0).to_itype(c.kcl.dbu)
) # DTrans is a transformation in microns with arguments (<rotation in 90° increments>, <mirror boolean>, <x in microns>, <y in microns>)
# Now that the geometry has been added to "c", we can move everything around:
### complex transformation example:ce
# magnification(float): magnification, DO NOT USE on cells or instances, only shapes, most foundries will not allow magnifications on actual cell instances or cells
# rotation(float): rotation in degrees
# mirror(bool): boolean to mirror at x axis and then rotate if true
# x(float): x coordinate
# y(float): y coordinate
#
text2 = t.transformed(
kf.kdb.DCplxTrans(2.0, 45.0, False, 5.0, 30.0).to_itrans(c.kcl.dbu)
)
# text1.movey(25)
# text2.move([5, 30])
# text2.rotate(45)
r.move(
-5, 0
) # boxes can be moved like this, other shapes and cellss/refs need to be moved with .transform
r.move(-5, 0)
(-12.5,-5;-7.5,5)
c.shapes(c.kcl.find_layer(1, 0)).insert(text1)
c.shapes(c.kcl.find_layer(2, 0)).insert(text2)
c.shapes(c.kcl.find_layer(2, 0)).insert(r)
box (-12500,-5000;-7500,5000)
c.show() # show in klayout
c.plot()
connect ports¶
Cells can have a "Port" that allows you to connect Instances together like legos.
You can write a simple function to make a rectangular straight, assign ports to the ends, and then connect those rectangles together.
@kf.cell
def straight(length=10, width=1, layer=(1, 0)):
wg = kf.KCell()
box = kf.kdb.DBox(length, width)
int_box = wg.kcl.to_dbu(box)
_layer = kf.kcl.find_layer(*layer)
wg.shapes(_layer).insert(box)
wg.add_port(
kf.Port(
name="o1",
dwidth=width,
dcplx_trans=kf.kdb.DCplxTrans(1, 180, False, box.left, box.center().y),
layer=_layer,
)
)
wg.create_port(
name="o2",
trans=kf.kdb.Trans(int_box.right, int_box.center().y),
layer=_layer,
width=int_box.height(),
)
return wg
c = kf.KCell()
wg1 = c << straight(length=1, width=2.5, layer=(1, 0))
wg2 = c << straight(length=2, width=2.5, layer=(1, 0))
wg3 = c << straight(length=3, width=2.5, layer=(1, 0))
c
Now we can align everything together using the ports:
Each straight has two ports: 'o1' and 'o2'. These are arbitrary names defined in our straight() function above
# Let's keep wg1 in place on the bottom, and connect the other straights to it.
# To do that, on wg2 we'll grab the "W0" port and connect it to the "E0" on wg1:
wg2.connect("o1", wg1.ports["o2"])
# Next, on wg3 let's grab the "W0" port and connect it to the "E0" on wg2:
wg3.connect("o1", wg2.ports["o2"])
c
c.add_port(name="o1", port=wg1.ports["o1"])
c.add_port(name="o2", port=wg3.ports["o2"])
c.show() # show in klayout
c.plot()
As you can see the red
labels are for the cell ports while
blue
labels are for the sub-ports (children ports)
Move and rotate instances¶
You can move, rotate, and reflect instances to Cells.
c = kf.KCell()
# Create and add a polygon from separate lists of x points and y points
# e.g. [(x1, x2, x3, ...), (y1, y2, y3, ...)]
c.shapes(c.kcl.find_layer(4, 0)).insert(
kf.kdb.DPolygon([kf.kdb.DPoint(x, y) for x, y in zip((8, 6, 7, 9), (6, 8, 9, 5))])
)
polygon (9000,5000;6000,8000;7000,9000)
# Alternatively, create and add a polygon from a list of points
# e.g. [(x1,y1), (x2,y2), (x3,y3), ...] using the same function
c.shapes(c.kcl.find_layer(4, 0)).insert(
kf.kdb.DPolygon(
[kf.kdb.DPoint(x, y) for (x, y) in ((0, 0), (1, 1), (1, 3), (-3, 3))]
)
)
polygon (0,0;-3000,3000;1000,3000;1000,1000)
c.show() # show in klayout
c.plot()
Ports¶
Your straights wg1/wg2/wg3 are instances to other straight cells.
If you want to add ports to the new Cell c
you can use add_port
, where you can create a new port or use an instance an existing port from the underlying instance.
You can access the ports of a Cell or Instance
wg2.ports
['Port(name: o1, dwidth: 2.5, trans: r180 *1 0.5,0, layer: WG (1/0), port_type: optical)', 'Port(name: o2, dwidth: 2.5, trans: r0 *1 2.5,0, layer: WG (1/0), port_type: optical)']
Instances¶
Now that we have your cell c
is a multi-straight cell, you can add instances to that cell in a new blank Cell c2
, then add two instances and shift one to see the movement.
c2 = kf.KCell(name="MultiMultiWaveguide")
wg1 = straight(layer=(2, 0))
wg2 = straight(layer=(2, 0))
mwg1_ref = c2.create_inst(wg1)
mwg2_ref = c2.create_inst(wg2)
mwg2_ref.transform(kf.kdb.DTrans(10, 10))
c2
# Like before, let's connect mwg1 and mwg2 together
mwg1_ref.connect("o2", mwg2_ref.ports["o1"])
c2
c = kf.KCell()
bend = kf.cells.euler.bend_euler(radius=10, width=1, layer=LAYER.WG)
b1 = c << bend
b2 = c << bend
b2.mirror_x(x=0)
c
c = kf.KCell()
bend = kf.cells.euler.bend_euler(radius=10, width=1, layer=LAYER.WG)
b1 = c << bend
b2 = c << bend
b2.mirror_y(y=0)
c
c = kf.KCell()
bend = kf.cells.euler.bend_euler(radius=10, width=1, layer=LAYER.WG)
b1 = c << bend
b2 = c << bend
b2.mirror_y(y=0)
b1.ymin = b2.ymax
c
Labels¶
You can add abstract GDS labels (annotate) to your Cells, in order to record information directly into the final GDS file without putting any extra geometry onto any layer This label will display in a GDS viewer, but will not be rendered or printed like the polygons created by gf.cells.text().
c2.shapes(c2.kcl.find_layer(1, 0)).insert(kf.kdb.Text("First label", mwg1_ref.trans))
c2.shapes(c2.kcl.find_layer(1, 0)).insert(kf.kdb.Text("Second label", mwg2_ref.trans))
text ('Second label',m90 0,0)
# It's very useful for recording information about the devices or layout
c2.shapes(c2.kcl.find_layer(10, 0)).insert(
kf.kdb.Text(
f"The x size of this\nlayout is {c2.dbbox().width()}",
kf.kdb.Trans(c2.bbox().right, c2.bbox().top),
)
)
c2.show()
c2.plot()
Boolean shapes¶
If you want to subtract one shape from another, merge two shapes, or
perform an XOR on them, you can do that with the boolean()
function.
The operation
argument should be {not, and, or, xor, 'A-B', 'B-A', 'A+B'}.
Note that 'A+B' is equivalent to 'or', 'A-B' is equivalent to 'not', and
'B-A' is equivalent to 'not' with the operands switched
e1 = kf.kdb.DPolygon.ellipse(kf.kdb.DBox(10, 8), 64)
e2 = kf.kdb.DPolygon.ellipse(kf.kdb.DBox(10, 6), 64).transformed(
kf.kdb.DTrans(2.0, 0.0)
)
c = kf.KCell()
c.shapes(c.kcl.find_layer(2, 0)).insert(e1)
c.shapes(c.kcl.find_layer(4, 0)).insert(e2)
c
# e1 NOT e2
c = kf.KCell()
e3 = kf.kdb.Region(c.kcl.to_dbu(e1)) - kf.kdb.Region(c.kcl.to_dbu(e2))
c.shapes(c.kcl.find_layer(1, 0)).insert(e3)
c
# e1 AND e2
c = kf.KCell()
e3 = kf.kdb.Region(c.kcl.to_dbu(e1)) & kf.kdb.Region(c.kcl.to_dbu(e2))
c.shapes(c.kcl.find_layer(1, 0)).insert(e3)
c
# e1 OR e2
c = kf.KCell()
e3 = kf.kdb.Region(c.kcl.to_dbu(e1)) + kf.kdb.Region(c.kcl.to_dbu(e2))
c.shapes(c.kcl.find_layer(1, 0)).insert(e3)
c
# e1 OR e2 (merged)
c = kf.KCell()
e3 = (
kf.kdb.Region(c.kcl.to_dbu(e1)) + kf.kdb.Region(c.kcl.to_dbu(e2))
).merge()
c.shapes(c.kcl.find_layer(1, 0)).insert(e3)
c
# e1 XOR e2
c = kf.KCell()
e3 = kf.kdb.Region(c.kcl.to_dbu(e1)) ^ kf.kdb.Region(c.kcl.to_dbu(e2))
c.shapes(c.kcl.find_layer(1, 0)).insert(e3)
c
Move instance by port¶
c = kf.KCell()
wg = c << kf.cells.straight.straight(width=0.5, length=1, layer=LAYER.WG)
bend = c << kf.cells.euler.bend_euler(width=0.5, radius=1, layer=LAYER.WG)
bend.connect("o1", wg.ports["o2"]) # connects follow Source, destination syntax
c
Rotate instance¶
You can rotate in degrees using Instance.drotate
or in multiples of 90 deg.
c = kf.KCell("mirror_example3")
bend = kf.cells.euler.bend_euler(width=0.5, radius=1, layer=LAYER.WG)
b1 = c << bend
b2 = c << bend
b2.drotate(90)
c
c = kf.KCell("mirror_example4")
bend = kf.cells.euler.bend_euler(width=0.5, radius=1, layer=LAYER.WG)
b1 = c << bend
b2 = c << bend
b2.rotate(1)
c
By default the mirror works along the x=0 axis.
c.write("demo.gds")