Building block creation¶
This section addresses the workflow to create a new building block. It includes a deep explanation of the Drawer classes
and PCell classes
needed for the scripting design, two examples of building blocks design, as well as the steps to include the new components into a custom library.
Drawer classes and PCell classes¶
As previously discussed, the design of building blocks is done by coding a Drawer class
. These Python classes will be registered by KLayout as parametric cells (PCell classes
) through the Alcyon’s Technology Generation tool, thus making your building blocks available at the KLayout Libraries menu.
The main characteristics of the Drawer classes
and PCell classes
are described here below.
NOTE: KLayout’s Python module is called
pya
.
Drawer classes¶
A
Drawer class
contains the methods for building the shapes and contact points of an individual component.Every
Drawer class
name must end with the word Drawer. For instance:SomethingDrawer
.Each
Drawer class
must be coded in a separate Python file having the same name as the class. For instance, theCircleDrawer
class code must be written in a single file namedCircleDrawer.py
.The
Drawer classes
are NOT directly imported by Klayout. Instead, a different set of classes, thePCell classes
, will use them to build the corresponding parametric cells that are registered by Klayout.As illustrated in the figure below, the user-defined drawer inherits from the
BasicDrawer
class (provided in the Alcyon PDK) and must override itsdraw()
method.The overriden
draw()
method must returnshapes_dict
,outline
andlist_of_pins
, where:shapes_dict
is the list containing the cell shapes (geometries) list and their related mapping layer. Therefore, the keys of theshapes_dict
must bepya.LayerInfo
and their associated lists ofpya
shapes.outline
is a boundary box (pya.DSimplePolygon) around the cell, usually with upper and lower safety margins, intended to prevent the overlap between cells.list_of_pins
is the list of pya.DPaths that constitute the cell’s contact points. A pin is apath
centered at the desired location of its contact point and which extends 0.05 um inwards and outwards from the cell outline. The first point of a pin path should lie inside the cell outline, thus, the last one should lie outside.return shapes_dict, outline, list_of_pins
NOTE: the
BasicDrawer
class already includes the member functionget_pin_list()
which generates thelist_of_pins
from the list holding their contact points.The illustration below shows an example of a cell shapes, outline, contact point and pin parts.
PCell classes¶
PCell classes
are the Python classes that inherit frompya.PCellDeclarationHelper
and are registered by Klayout.The user does not need to implement the code for the
PCell classes
of a custom library. In fact,PCell classes
are automatically generated when running the Alcyon Technology Generation tool.In order to register the custom libraries in KLayout, the user does not need to code any script. The scripts are also automatically generated.
Coding examples¶
In this section we will see how to create a new building block either by using KLayout shapes (e.g. polygons, paths, points, etc) or from predefined Alcyon basic blocks (waveguides, tapers, bends, etc).
In any case, it is strongly recommended to use the Jupyter Notebook drawers_visualizer.ipynb
to visualize the Drawer Class
and easily check that the designed geometry looks as expected. See the Drawers visualizer section for further details.
Using KLayout shapes¶
We will create a new building block with the shape of a circle, which in KLayout would look like the cell represented below.
In this image, the pink region is the shape to be included in a desired layer, the green rectangle is the outline of the cell and the blue rectangles on the sides are the cell contact points.

Create a new Python file named
CircleDrawer.py
and place it inside the drawers subfolder of our previously created CustomLibrary folder.Please note that a single
Drawer class
must be coded for each component in a library.Import the
BasicDrawer
class.import pya import numpy as np from AlcyonPDK.baseclasses.base_classes import BasicDrawer
NOTE: should your AlcyonPDK repository/folder name be different, replace
AlcyonPDK
appropriately. For instance, if your AlcyonPDK repository folder is named AlcyonPDK_Beta, then the above lines would be replaced by the following:import pya import numpy as np from AlcyonPDK_Beta.baseclasses.base_classes import BasicDrawer
Create the
Drawer class
. Remember that its name must end with the word Drawer. In this case, it would beCircleDrawer
.class CircleDrawer(BasicDrawer): # Drawer which implements a circle def __init__(self, origin: tuple = (0, 0), num_points: int = 100, radius: float = 1.0, outline_width = 1.0): """ Create circle Args: * **origin**: circle's center * **num_points**: number of points * **radius**: circle's radius * **outline_width**: circle's outline width """ self.origin = origin self.num_points = num_points self.radius = radius self.rotation = 0 self.outline_width = outline_width
The
draw()
method is an abstract method inherited from theBasicDrawer
. It must be overridden so that it returnsshapes_dict
,outline
andlist_of_pins
.def draw(self): """ Draw the cell/component 1. The component is generated with c_0 at (0, 0) and with a rotation angle of 0 radians 2. All paths are rotated around c_0 (0, 0) 3. All paths are translated to desired self.origin, so c_0 becomes self.origin Return: * **list_of_paths** (list): list of pya shapes * **outline** (pya.DSimplePolygon): cell's outline * **[c_0, c_1]**: list of contact points (pya.DPoints) """
The following code parts must be included in the
draw()
method in order to override it. First, we castself.origin
to a tuple if it is given as apya.DPoint
for safety. Otherwise, check whether it has been assigned as an iterable with 2 elements.if isinstance(self.origin, pya.DPoint) or isinstance(self.origin, pya.Point): self.origin = (self.origin.x, self.origin.y) else: try: iter(self.origin) if not len(self.origin) == 2: raise TypeError(f"Origin point must be an iterable with 2 elements") except: raise TypeError(f"Origin point must be an iterable with 2 elements or a pya.DPoint")
Next we define the shapes that will be inserted in a certain layer of the layout. In this case, we will generate the circle points.
Note that up to this moment, the center of the circle is the point (0, 0). These coordiantes are referred to the TOP cell’s coordinate system where this cell will be inserted.
# Circle points (first, centered at (0, 0)) c = np.cos s = np.sin pts = [pya.DPoint(c(theta), s(theta))*self.radius for theta in np.linspace(0, 2*np.pi, self.num_points)] # Circle as a simple (without holes) polygon circle_polygon = pya.DSimplePolygon(pts)
Then, we translate the points so that the circle center will be located at
self.origin
. We can also rotate the points if a rotation angle is given.# Rotate the points by the given angle (rotation can be = 0) circle_polygon = self.rotate_paths(paths=circle_polygon, rotation_angle=self.rotation, rot_point=(0, 0)) # Translate the points to origin circle_polygon = self.translate_points(paths=circle_polygon, dst_point=self.origin)
The cell outline is a boundary box with set upper and lower safety placing margins. These margins are intended to prevent placing cells too close.
Currently, only
pya.DPaths
,pya.DSimplePolygon
orpya.DPolygon
are allowed as outline shapes. As with the circle polygon, the outline is also translated and rotated.# Outline (a box containing the circle) # Currently, only pya.DPaths or pya.DSimplePolygon... outline = pya.DSimplePolygon([pya.DPoint(-self.radius, -(self.radius + self.outline_width)), pya.DPoint(-self.radius, (self.radius + self.outline_width)), pya.DPoint(self.radius, (self.radius + self.outline_width)), pya.DPoint(self.radius, -(self.radius + self.outline_width))]) outline = self.rotate_paths(paths=outline, rotation_angle=self.rotation, rot_point=(0, 0)) outline = self.translate_points(paths=outline, dst_point=self.origin)
Two contact points (c_points) will be added at (-radius, 0) and (radius, 0). The pin associated to a contact point is created as follows:
KLayout generates a path centered at the contact point. The path is generated from 2 points and a given width.
The distance in the x-coordinate between the two path points is 0.1 um.
This path initially has a rotation of 0º with respect to the x-axis.
The path (pin) can be rotated as desired.
The list of pins is obtained through the
get_pin_list()
method, which is called with the following arguments:points_w_larger_p0x
: list containing those c_points whose pins have their p0.x coordinate > p1.x (before the rotation)points_w_larger_p1x
: list containing those c_points whose pins have their p0.x coordinate < p1.x (before the rotation).
The rationale for separating points according to this condition (p0.x > p1.x) is to control the requirement that the first point of the pin of a contact point must lie inside the cell outline. However, a better approach can be taken by providing the outline shape to the
get_pin_list()
method and computing the path so that it is tangent to the outline at the contact point and placing its first point inside the outline.# Contact points c_0 = pya.DPoint(-self.radius, 0) c_1 = pya.DPoint(self.radius, 0) # Create pins from contact points list_of_pins = self.get_pin_list(points_w_larger_p0x=[{"c_point": c_0, "r_deg":0, "width": .25}], points_w_larger_p1x=[{"c_point": c_1, "r_deg": 0, "width": .25}]) list_of_pins = self.rotate_paths(paths=list_of_pins, rotation_angle=self.rotation, rot_point=(0, 0)) list_of_pins = self.translate_points(paths=list_of_pins, dst_point=self.origin)
Finally, the
draw()
method must return theshapes_dict
, theoutline
and thelist_of_pins
.The
shapes_dict
is a dictionary which maps a given list of shapes to a desired layer so that the designer can insert as many shapes in as many layers as she/he would like to:shapes_dict = {pya.LayerInfo(1,0): [circle_polygon]}
The shapes in the list
[circle_polygon]
(which for this case is a list of only one shape) will be further inserted by thePCell
code in the layer given by thepya.LayerInfo(1,0)
object.The return line should look like:
return shapes_dict, outline, list_of_pins
The full code for the CircleDrawer.py
should look like:
import pya
import numpy as np
from AlcyonPDK.baseclasses.base_classes import BasicDrawer
class CircleDrawer(BasicDrawer):
# Drawer which implements a circle
def __init__(self, origin: tuple = (0, 0),
num_points: int = 100,
radius: float = 1.0,
outline_width = 1.0):
"""
Create circle
Args:
* **origin**: circle's center
* **num_points**: number of points
* **radius**: circle's radius
* **outline_width**: circle's outline width
"""
self.origin = origin
self.num_points = num_points
self.radius = radius
self.rotation = 0
self.outline_width = outline_width
def draw(self):
"""
Draw the cell/component
1. The component is generated with c_0 at (0, 0)
and with a rotation angle of 0 radians
2. All paths are rotated around c_0 (0, 0)
3. All paths are translated to desired self.origin, so c_0
becomes self.origin
Return:
* **list_of_paths** (list): list of pya shapes
* **outline** (pya.DSimplePolygon): cell's outline
* **[c_0, c_1]**: list of contact points (pya.DPoints)
"""
if isinstance(self.origin, pya.DPoint) or isinstance(self.origin, pya.Point):
self.origin = (self.origin.x, self.origin.y)
else:
try:
iter(self.origin)
if not len(self.origin) == 2:
raise TypeError(f"Origin point must be an iterable with 2 elements")
except:
raise TypeError(f"Origin point must be an iterable with 2 elements or a pya.DPoint")
# Circle points (first, centered at (0, 0))
c = np.cos
s = np.sin
pts = [pya.DPoint(c(theta), s(theta))*self.radius for theta
in np.linspace(0, 2*np.pi, self.num_points)]
# Circle as a simple (without holes) polygon
circle_polygon = pya.DSimplePolygon(pts)
# Rotate the points by the given angle (rotation can be = 0)
circle_polygon = self.rotate_paths(paths=circle_polygon, rotation_angle=self.rotation, rot_point=(0, 0))
# Translate the points to origin
circle_polygon = self.translate_points(paths=circle_polygon, dst_point=self.origin)
# Outline (a box containing the circle)
# Currently, only pya.DPaths or pya.DSimplePolygon...
outline = pya.DSimplePolygon([pya.DPoint(-self.radius, -(self.radius + self.outline_width)),
pya.DPoint(-self.radius, (self.radius + self.outline_width)),
pya.DPoint(self.radius, (self.radius + self.outline_width)),
pya.DPoint(self.radius, -(self.radius + self.outline_width))])
outline = self.rotate_paths(paths=outline, rotation_angle=self.rotation, rot_point=(0, 0))
outline = self.translate_points(paths=outline, dst_point=self.origin)
# Contact points
c_0 = pya.DPoint(-self.radius, 0)
c_1 = pya.DPoint(self.radius, 0)
# Create pins from contact points
list_of_pins = self.get_pin_list(points_w_larger_p0x=[{"c_point": c_0, "r_deg":0, "width": .25}],
points_w_larger_p1x=[{"c_point": c_1, "r_deg": 0, "width": .25}])
list_of_pins = self.rotate_paths(paths=list_of_pins, rotation_angle=self.rotation, rot_point=(0, 0))
list_of_pins = self.translate_points(paths=list_of_pins, dst_point=self.origin)
shapes_dict = {pya.LayerInfo(1,0): [circle_polygon]}
return shapes_dict, outline, list_of_pins
As mentioned before, it is highly recommended to use the drawers_visualizer.ipynb
Jupyter Notebook to check that the script is working as expected. This tool would help to identify any code error or geometric issue regarding the desired block. Details on how to use it can be found in Drawers visualizer.
Using Alcyon basic blocks¶
Geometrically complex devices can be easily created by using predefined building blocks from the library Alcyon-basics.
In this example we will build a 2x2 MMI coupler combining taper waveguides imported from TaperDrawer.py
and a multimode region defined by using the KLayout shape pya.DPolygon
. Note that there are multitude of strategies to create the desired design.
The image below shows the target geometry.

Create a new Python file named
mmiDrawer.py
and place it inside the drawers subfolder of our previously created CustomLibrary folder.Please note that a single
Drawer class
must be coded for each component in a library.Import the
BasicDrawer
andTaperDrawer
classes.import pya import numpy as np from AlcyonPDK.baseclasses.base_classes import BasicDrawer from AlcyonPDK.libs.basics.drawers.TaperDrawer import TaperDrawer
NOTE: should your AlcyonPDK repository/folder name be different, replace
AlcyonPDK
appropriately. For instance, if your AlcyonPDK repository folder is named AlcyonPDK_Beta, then the above lines would be replaced by the following:import pya import numpy as np from AlcyonPDK_Beta.baseclasses.base_classes import BasicDrawer from AlcyonPDK_Beta.libs.basics.drawers.TaperDrawer import TaperDrawer
Create the
Drawer class
. Remember that its name must end with the word Drawer. In this case, it would bemmiDrawer
.class mmiDrawer(BasicDrawer): @classmethod def block_name(cls): return "mmi" def __init__(self, origin: tuple = (0, 0), rotation: float = 0, outline_width: float = 1, tap_length: float = 20, tap_wini: float = 0.5, tap_wfin: float = 1.7, port_sep: float = 2, mmi_width: float = 3.8, mmi_length: float = 50, return_c_points: bool=False): """ MMI2x2 Drawer Args: * **origin** : origin of the cell * **outline_width** : outline's extra width * **rotation** : cell's rotation in RADIANS. Rotation is performed around c_0 * **tap_wini** : Interior tap initial width * **tap_wfin** : Taper's final width * **s** : Port separation * **mmi_width** : MMI's width * **mmi_length** : MMI's length * **return_c_points** : whether to return the contact points as points or pins. Useful when this drawer is called from another drawer so that the latter's pins are computed from the former's contact points. """ self.origin = origin self.rotation = rotation self.outline_width = outline_width self.tap_length = tap_length self.tap_wini = tap_wini self.tap_wfin = tap_wfin self.port_sep = port_sep self.mmi_width = mmi_width self.mmi_length = mmi_length self.return_c_points = return_c_points
The
draw()
method is an abstract method inherited from theBasicDrawer
. It must be overridden so that it returnsshapes_dict
,outline
andlist_of_pins
.def draw(self): """ Draw the cell/component 1. The component is generated with c_0 at (0, 0) and with a rotation angle of 0 radians 2. All paths are rotated around c_0 (0, 0) 3. All paths are translated to desired self.origin, so c_0 becomes self.origin Return: * **list_of_paths** (list): list of pya shapes * **outline** (pya.DSimplePolygon): cell's outline * **[c_0, c_1]**: list of contact points (pya.DPoints) """
The following code parts must be included in the
draw()
method in order to override it. First, we castself.origin
to a tuple if it is given as apya.DPoint
for safety.if isinstance(self.origin, pya.DPoint) or isinstance(self.origin, pya.Point): self.origin = (self.origin.x, self.origin.y) else: self.origin = self.origin
Next we define the shapes that will be inserted in a certain layer of the layout. In this case, we will instance the
TaperDrawer
class for the 2 input and 2 output tapered waveguides. The main region will be defined using apya.DPolygon
shape.# Taper drawer taper_drawer = TaperDrawer(origin=(0, 0), length=self.tap_length, initial_width=self.tap_wini, final_width=self.tap_wfin, outline_width=self.outline_width, rotation=0, return_c_points=True) taper_1_origin = (0, 0) taper_2_origin = (0, -self.port_sep) # Taper 1 taper_drawer.origin = taper_1_origin taper_1_shapes_dict, taper_1_outline, [taper_1_c_0, taper_1_c_1] = taper_drawer.draw() len_taper=np.abs(taper_1_c_1.x-taper_1_c_0.x) # Taper 2 taper_drawer.origin = taper_2_origin taper_2_shapes_dict, taper_2_outline, [taper_2_c_0, taper_2_c_1] = taper_drawer.draw() # Core MMI mmi_origin = pya.DPoint(0, -self.port_sep/2) + taper_1_c_1 len_mmi=self.mmi_length width_mmi = self.mmi_width # Polygon of Core MMI pt1 = mmi_origin + pya.DPoint(0, -width_mmi * 0.5) pt2 = mmi_origin + pya.DPoint(0, +width_mmi * 0.5) pt4 = mmi_origin + pya.DPoint(len_mmi, -width_mmi * 0.5) pt3 = mmi_origin + pya.DPoint(len_mmi, +width_mmi * 0.5) mmi_shapes_list = [pya.DPolygon([pt1, pt2, pt3, pt4])] # Outline of Core MMI pt1 = mmi_origin + pya.DPoint(0, -width_mmi * 0.5 - self.outline_width) pt2 = mmi_origin + pya.DPoint(0, +width_mmi * 0.5 + self.outline_width) pt4 = mmi_origin + pya.DPoint(len_mmi, -width_mmi * 0.5 - self.outline_width) pt3 = mmi_origin + pya.DPoint(len_mmi, +width_mmi * 0.5 + self.outline_width) mmi_outline = pya.DPolygon([pt1, pt2, pt3, pt4]) #coreMmiDrawer = TaperDrawer(origin=mmi_origin, # length=self.mmi_length, # initial_width=self.mmi_width, # final_width=self.mmi_width, # outline_width=self.outline_width, # rotation=0, # return_c_points=True) #mmi_shapes_dict, mmi_outline, [mmi_c_0, mmi_c_1] = coreMmiDrawer.draw() # Taper 3 taper_3_origin = taper_1_c_1+pya.DPoint(len_mmi+len_taper, 0) taper_drawer.origin = taper_3_origin taper_drawer.rotation=np.pi taper_3_shapes_dict, taper_3_outline, [taper_3_c_0, taper_3_c_1] = taper_drawer.draw() # Taper 4 taper_4_origin = taper_3_c_0+pya.DPoint(0,-self.port_sep) taper_drawer.origin = taper_4_origin taper_drawer.rotation=np.pi taper_4_shapes_dict, taper_4_outline, [taper_4_c_0, taper_4_c_1] = taper_drawer.draw()
All these shapes are added to the
list_of_paths
in order to later map them to a desired layer.# Add paths list_of_paths = mmi_shapes_list for layer, shapes_list in taper_1_shapes_dict.items(): list_of_paths.extend(shapes_list) for layer, shapes_list in taper_2_shapes_dict.items(): list_of_paths.extend(shapes_list) for layer, shapes_list in taper_3_shapes_dict.items(): list_of_paths.extend(shapes_list) for layer, shapes_list in taper_4_shapes_dict.items(): list_of_paths.extend(shapes_list)
The cell outline is a boundary box with set upper and lower safety placing margins. These margins are intended to prevent placing cells too close.
Currently, only
pya.DPaths
,pya.DSimplePolygon
orpya.DPolygon
are allowed as outline shapes# Outline middle_pt_1 = taper_1_c_0 + pya.DPoint(0, -abs(taper_2_c_0.y - taper_1_c_0.y) * 0.5) middle_pt_2 = taper_3_c_0 + pya.DPoint(0, -abs(taper_3_c_0.y - taper_4_c_0.y) * 0.5) max_y, min_y = self.get_y_boundaries(outline_list=[taper_1_outline, taper_2_outline, taper_3_outline, taper_4_outline, mmi_outline], rot_point=(0, 0), rotation=0) max_w = max_y - min_y outline = pya.DPath([middle_pt_1, middle_pt_2], max_w).simple_polygon()
Four pins will be added at the narrower side of the input/output tapers. The corresponding contact points are those of the tapered waveguides.
The list of pins is obtained through the
get_pin_list()
method, which is called with the following arguments:points_w_larger_p0x
: list containing those c_points whose pins have their p0.x coordinate > p1.x (before the rotation)points_w_larger_p1x
: list containing those c_points whose pins have their p0.x coordinate < p1.x (before the rotation)
The rationale for separating points according to this condition (p0.x > p1.x) is to control the requirement that the first point of the pin of a contact point must lie inside the cell outline.
# Pins if not self.return_c_points: list_of_pins = self.get_pin_list(points_w_larger_p0x=[{"c_point":taper_1_c_0, "r_deg":0, "width": self.tap_wini}, {"c_point":taper_2_c_0, "r_deg":0, "width": self.tap_wini}], points_w_larger_p1x=[{"c_point": taper_3_c_0, "r_deg": 0, "width": self.tap_wini}, {"c_point": taper_4_c_0, "r_deg": 0, "width": self.tap_wini}]) else: list_of_pins = [taper_1_c_0, taper_2_c_0, taper_3_c_0, taper_4_c_0]
Then, we translate the geometry paths, the outline and the pins so that the MMI first input taper will be located at
self.origin
. We can also rotate the same properties in case a rotation angle is given.# Rotate and translate list_of_paths = self.rotate_paths(paths=list_of_paths, rotation_angle=self.rotation, rot_point=(0, 0)) list_of_paths = self.translate_points(paths=list_of_paths, dst_point=self.origin) outline = self.rotate_paths(paths=outline, rotation_angle=self.rotation, rot_point=(0, 0)) outline = self.translate_points(paths=outline, dst_point=self.origin) list_of_pins = self.rotate_paths(paths=list_of_pins, rotation_angle=self.rotation, rot_point=(0, 0)) list_of_pins = self.translate_points(paths=list_of_pins, dst_point=self.origin)
Finally, the
draw()
method must return theshapes_dict
, theoutline
and thelist_of_pins
.The
shapes_dict
is a dictionary which maps a given list of shapes to a desired layer so that the designer can insert as many shapes in as many layers as she/he would like to:shapes_dict = {pya.LayerInfo(1,0): list_of_paths}
The shapes included in the
list_of_paths
will be further inserted by thePCell
code in the layer given by thepya.LayerInfo(1,0)
object.The return line should look like the following:
return shapes_dict, outline, list_of_pins
The full code for the mmiDrawer.py
should look like:
import pya
import numpy as np
from AlcyonPDK.baseclasses.base_classes import BasicDrawer
from AlcyonPDK.libs.basics.drawers.TaperDrawer import TaperDrawer
class mmiDrawer(BasicDrawer):
@classmethod
def block_name(cls):
return "mmi"
def __init__(self,
origin: tuple = (0, 0),
rotation: float = 0,
outline_width: float = 1,
tap_length: float = 20,
tap_wini: float = 0.5,
tap_wfin: float = 1.7,
port_sep: float = 2,
mmi_width: float = 3.8,
mmi_length: float = 50,
return_c_points: bool=False):
"""
MMI2x2 Drawer
Args:
* **origin** : origin of the cell
* **outline_width** : outline's extra width
* **rotation** : cell's rotation in RADIANS. Rotation is performed around c_0
* **tap_wini** : Interior tap initial width
* **tap_wfin** : Taper's final width
* **s** : Port separation
* **mmi_width** : MMI's width
* **mmi_length** : MMI's length
* **return_c_points** : whether to return the contact points as points or pins. Useful when this
drawer is called from another drawer so that the latter's pins are computed from the
former's contact points.
"""
self.origin = origin
self.rotation = rotation
self.outline_width = outline_width
self.tap_length = tap_length
self.tap_wini = tap_wini
self.tap_wfin = tap_wfin
self.port_sep = port_sep
self.mmi_width = mmi_width
self.mmi_length = mmi_length
self.return_c_points = return_c_points
def draw(self):
"""
Draw the cell/component
1. The component is generated with c_0 at (0, 0)
and with a rotation angle of 0 radians
2. All paths are rotated around c_0 (0, 0)
3. All paths are translated to desired self.origin, so c_0
becomes self.origin
Return:
* **list_of_paths** (list): list of pya shapes
* **outline** (pya.DSimplePolygon): cell's outline
* **[c_0, c_1]**: list of contact points (pya.DPoints)
"""
if isinstance(self.origin, pya.DPoint) or isinstance(self.origin, pya.Point):
self.origin = (self.origin.x, self.origin.y)
else:
self.origin = self.origin
# Taper drawer
taper_drawer = TaperDrawer(origin=(0, 0),
length=self.tap_length,
initial_width=self.tap_wini,
final_width=self.tap_wfin,
outline_width=self.outline_width,
rotation=0,
return_c_points=True)
taper_1_origin = (0, 0)
taper_2_origin = (0, -self.port_sep)
# Taper 1
taper_drawer.origin = taper_1_origin
taper_1_shapes_dict, taper_1_outline, [taper_1_c_0, taper_1_c_1] = taper_drawer.draw()
len_taper=np.abs(taper_1_c_1.x-taper_1_c_0.x)
# Taper 2
taper_drawer.origin = taper_2_origin
taper_2_shapes_dict, taper_2_outline, [taper_2_c_0, taper_2_c_1] = taper_drawer.draw()
# Core MMI
mmi_origin = pya.DPoint(0, -self.port_sep/2) + taper_1_c_1
len_mmi=self.mmi_length
width_mmi = self.mmi_width
# Polygon of Core MMI
pt1 = mmi_origin + pya.DPoint(0, -width_mmi * 0.5)
pt2 = mmi_origin + pya.DPoint(0, +width_mmi * 0.5)
pt4 = mmi_origin + pya.DPoint(len_mmi, -width_mmi * 0.5)
pt3 = mmi_origin + pya.DPoint(len_mmi, +width_mmi * 0.5)
mmi_shapes_list = [pya.DPolygon([pt1, pt2, pt3, pt4])]
# Outline of Core MMI
pt1 = mmi_origin + pya.DPoint(0, -width_mmi * 0.5 - self.outline_width)
pt2 = mmi_origin + pya.DPoint(0, +width_mmi * 0.5 + self.outline_width)
pt4 = mmi_origin + pya.DPoint(len_mmi, -width_mmi * 0.5 - self.outline_width)
pt3 = mmi_origin + pya.DPoint(len_mmi, +width_mmi * 0.5 + self.outline_width)
mmi_outline = pya.DPolygon([pt1, pt2, pt3, pt4])
#coreMmiDrawer = TaperDrawer(origin=mmi_origin,
# length=self.mmi_length,
# initial_width=self.mmi_width,
# final_width=self.mmi_width,
# outline_width=self.outline_width,
# rotation=0,
# return_c_points=True)
#mmi_shapes_dict, mmi_outline, [mmi_c_0, mmi_c_1] = coreMmiDrawer.draw()
# Taper 3
taper_3_origin = taper_1_c_1+pya.DPoint(len_mmi+len_taper, 0)
taper_drawer.origin = taper_3_origin
taper_drawer.rotation=np.pi
taper_3_shapes_dict, taper_3_outline, [taper_3_c_0, taper_3_c_1] = taper_drawer.draw()
# Taper 4
taper_4_origin = taper_3_c_0+pya.DPoint(0,-self.port_sep)
taper_drawer.origin = taper_4_origin
taper_drawer.rotation=np.pi
taper_4_shapes_dict, taper_4_outline, [taper_4_c_0, taper_4_c_1] = taper_drawer.draw()
# Add paths
list_of_paths = mmi_shapes_list
for layer, shapes_list in taper_1_shapes_dict.items():
list_of_paths.extend(shapes_list)
for layer, shapes_list in taper_2_shapes_dict.items():
list_of_paths.extend(shapes_list)
for layer, shapes_list in taper_3_shapes_dict.items():
list_of_paths.extend(shapes_list)
for layer, shapes_list in taper_4_shapes_dict.items():
list_of_paths.extend(shapes_list)
# Outline
middle_pt_1 = taper_1_c_0 + pya.DPoint(0, -abs(taper_2_c_0.y - taper_1_c_0.y) * 0.5)
middle_pt_2 = taper_3_c_0 + pya.DPoint(0, -abs(taper_3_c_0.y - taper_4_c_0.y) * 0.5)
max_y, min_y = self.get_y_boundaries(outline_list=[taper_1_outline, taper_2_outline,
taper_3_outline, taper_4_outline,
mmi_outline],
rot_point=(0, 0),
rotation=0)
max_w = max_y - min_y
outline = pya.DPath([middle_pt_1, middle_pt_2], max_w).simple_polygon()
# Pins
if not self.return_c_points:
list_of_pins = self.get_pin_list(points_w_larger_p0x=[{"c_point":taper_1_c_0, "r_deg":0, "width": self.tap_wini},
{"c_point":taper_2_c_0, "r_deg":0, "width": self.tap_wini}],
points_w_larger_p1x=[{"c_point": taper_3_c_0, "r_deg": 0, "width": self.tap_wini},
{"c_point": taper_4_c_0, "r_deg": 0, "width": self.tap_wini}])
else:
list_of_pins = [taper_1_c_0, taper_2_c_0, taper_3_c_0, taper_4_c_0]
# Rotate and translate
list_of_paths = self.rotate_paths(paths=list_of_paths, rotation_angle=self.rotation, rot_point=(0, 0))
list_of_paths = self.translate_points(paths=list_of_paths, dst_point=self.origin)
outline = self.rotate_paths(paths=outline, rotation_angle=self.rotation, rot_point=(0, 0))
outline = self.translate_points(paths=outline, dst_point=self.origin)
list_of_pins = self.rotate_paths(paths=list_of_pins, rotation_angle=self.rotation, rot_point=(0, 0))
list_of_pins = self.translate_points(paths=list_of_pins, dst_point=self.origin)
shapes_dict = {pya.LayerInfo(1,0): list_of_paths}
return shapes_dict, outline, list_of_pins
As mentioned before, it is highly recommended to use the drawers_visualizer.ipynb
Jupyter Notebook to check that the script is working as expected. This tool would help to identify any code error or geometric issue regarding the desired block. Details on how to use it can be found in the following section: Drawers visualizer.
Drawers visualizer¶
The Jupyter Notebook drawers_visualizer.ipynb
allows to visualize the geometries defined in the draw()
method of the Drawer classes
. It is of great help when programmming new blocks since it allows the user to check:
Code errors
If the geometry of the block looks as expected
If the geometry varies accordingly when changing the cell parameters
For these reasons, it is strongly recommended to check the drawer classes with the notebook before registering the libraries in KLayout in order to save time.
This notebook includes pre-coded examples to try out, and can be edited to add new user-created drawers. It can be found in the AlcyonPDK package at: ~\KLayout\salt\AlcyonPDK\python\AlcyonPDK\notebooks
NOTE: If an error arises regading the module pya, the following steps are recommended:
Uninstall pya if it is already installed in your environment. To do this, you can run the following code in one of the jupyter notebook cells:
!pip uninstall pyaInstall klayout python library version 0.28.6. analogously, in a jupyter notebook cell:
!pip install klayout==0.28.6ensuring your environment of choice is active.
Adding new building blocks to a library¶
This section describes how to add a new building block defined by a Drawer class
into a custom library.
As mentioned before, each component requires a file with a single Drawer class
. To incorporate the new block into a KLayout library, place the Python file in the drawers subfolder of the desired library.
Following the examples of this User Guide, the drawers folder of our CustomLibrary should contain:

NOTE: Adding new blocks to Alcyon’s IP libraries should be avoided as these libraries may be updated and the user’s files deleted.
One should now run the Alcyon Technology Generation tool from Klayout menu so that the PCell classes
and the registration scripts will be automatically generated from the content of the drawers folder.

After this step, the user will notice that a new file and a new folder have been created inside the library folder:
The pcells folder stores the automatically generated code of the parametric cell classes.
The
register_lib.py
script registers the custom library into KLayout.
After re-starting KLayout, the new library and components should be available from the Library dropdown menu.
NOTE: If the library or blocks don’t appear in KLayout, it is likely that there exists a coding error within one of the
Drawer classes
. You can make use of thedrawers_visualizer
Jupyter Notebook to check any problem (see Drawers visualizer section).NOTE: If no
Drawer classes
are coded and manually added to the drawers folder in the library, no components will be registered by KLayout.NOTE: Whenever any new
Drawer class
or custom libraries are added to the PDK, the Alcyon Tehcnology Generation tool must be run again.