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, the CircleDrawer class code must be written in a single file named CircleDrawer.py.

  • The Drawer classes are NOT directly imported by Klayout. Instead, a different set of classes, the PCell 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 its draw() method.

    Drawer Class
  • The overriden draw() method must return shapes_dict, outline and list_of_pins, where:

    • shapes_dict is the list containing the cell shapes (geometries) list and their related mapping layer. Therefore, the keys of the shapes_dict must be pya.LayerInfo and their associated lists of pya 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 a path 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 function get_pin_list() which generates the list_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.

      Cell's pin

PCell classes

  • PCell classes are the Python classes that inherit from pya.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.

CircleDrawer in KLayout
  1. 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.

    Drawer file
  2. 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
    
  3. Create the Drawer class. Remember that its name must end with the word Drawer. In this case, it would be CircleDrawer.

    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
    
  4. The draw() method is an abstract method inherited from the BasicDrawer. It must be overridden so that it returns shapes_dict, outline and list_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)
        """
    
  5. The following code parts must be included in the draw() method in order to override it. First, we cast self.origin to a tuple if it is given as a pya.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")
    
  6. 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)
    
  7. 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)
    
  8. 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 or pya.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)
    
  9. 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:

    1. KLayout generates a path centered at the contact point. The path is generated from 2 points and a given width.

    2. The distance in the x-coordinate between the two path points is 0.1 um.

    3. This path initially has a rotation of 0º with respect to the x-axis.

    4. 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)
    
  10. Finally, the draw() method must return the shapes_dict, the outline and the list_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 the PCell code in the layer given by the pya.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.

CircleDrawer in KLayout
  1. 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.

    Drawer file
  2. Import the BasicDrawer and TaperDrawer 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
    
  3. Create the Drawer class. Remember that its name must end with the word Drawer. In this case, it would be mmiDrawer.

    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
    
  4. The draw() method is an abstract method inherited from the BasicDrawer. It must be overridden so that it returns shapes_dict, outline and list_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)
        """
    
  5. The following code parts must be included in the draw() method in order to override it. First, we cast self.origin to a tuple if it is given as a pya.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
    
  6. 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 a pya.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()
    
  7. 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)
    
  8. 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 or pya.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()
    
  9. 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]
    
  10. 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)
    
  11. Finally, the draw() method must return the shapes_dict, the outline and the list_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 the PCell code in the layer given by the pya.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 pya
    
  • Install klayout python library version 0.28.6. analogously, in a jupyter notebook cell:

    !pip install klayout==0.28.6
    

    ensuring 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:

customLib_drawers_folder

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.

AlcyonTech_GenerateTechnology

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 the drawers_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.