1. Several contributions to one tape-out run

1.1. Introduction

Combining the designs from within a design team or even across design teams is a non-trivial task. Whether a proper automation flow is in place determines if the final mask assembly process is efficient and reliable.

In this tutorial, we propose a design project organization method which facilitates the final mask merging. Together with the Luceda design framework, and a version control-based design process, it guarantees maximal individual and collective productivity.

1.2. A Design Project Example

Let’s take a team design project as an example. Contributions to this project come from four designers: Chiara, Jiejun, Pierre, and Ruping. Each of them has some test structure designs to be loaded onto the next tape-out run, and they’re sharing the same design block area (a 2.5 mm * 1.25 mm block). Some examples of the designs are as follows:

../../../_images/splitter_tree.png ../../../_images/spiral.png ../../../_images/zig_zag.png ../../../_images/splitter_tree2.png

Someone needs to collect and combine these designs before submitting the mask layout to the foundry for tape-out. Ruping volunteers to perform this task. She faces several challenges:

  • Collecting the designs from her colleagues presents a huge communication burden.

  • She lacks a tool to write the final GDS mask fast.

  • Maintaining the merged GDS mask is tedious and error-prone (e.g. with colleagues making last-minute design changes).

Luckily, the team has set up a good project structure from the beginning, before everyone started their own designs, and everyone follows the common practice to carry out designs using a version control system (e.g. Github).

The structure of the tapeout_202008_si_fab project looks like this:

├── chiara
├── jiejun
├── pierre
├── ruping
├── template
└── execute_merge.py

Each designer has a design folder. The execute_merge.py script takes the GDS files from each designer folder and merges them into a single mask layout, including the template folder which provides the die template being used as the frame.

1.2.1. The regenerate script

Before using the execute_merge.py script, we need to have the GDS files ready from each of the designers. Ruping will be responsible for merging the designs, as she knows which version of the designs to use. It is recommended that the entire code to generate and merge the designs would be tracked by a Version Control System (e.g. Git) to ensure that you have complete traceability and repeatability.

On top of that, it is good practice to have one single script for the regeneration of each design, so that anybody in your organization would be able to replicate earlier work.

For example, in the chiara folder, the Luceda IPKISS design scripts are located in splitter_height_sweep and splitter_length_sweep and there is a regenerate.py script for each design.

class SplitterRouted(i3.Circuit):
    """This routes a 2-stage splitter made of 1 by 2 splitter elements.
    The input and output of the splitters are routed to fiber grating couplers."""

    splitter = i3.ChildCellProperty(doc="Splitter used")
    spacing_x = i3.PositiveNumberProperty(default=80.0, doc="Horizontal spacing between components")
    splitter_length = i3.PositiveNumberProperty(default=4.0, doc="Length of the MMI splitter")
    splitter_height = i3.PositiveNumberProperty(default=10.0, doc="Height of the MMI splitter")
    trace_template = i3.TraceTemplateProperty(doc="Trace template for connectors")
    grating_coupler = i3.ChildCellProperty(doc="Grating coupler used")
from si_fab import all as pdk  # noqa: F401
from splitter_routed import SplitterHeightSweep
import numpy
import os

output_folder = os.path.join(os.pardir, "gds_to_merge")
design_name = "splitter_tree_height_sweep"  # Name of your design

print("Regenerate {}".format(design_name))

# Instantiate the design and export it to gds
cell = SplitterHeightSweep(name=design_name.upper())
cell_lv = cell.Layout()
cell_lv.write_gdsii(os.path.join(output_folder, design_name + ".gds"))

# Extract file info and export it
si = cell_lv.size_info()
size_info_file = os.path.join(output_folder, design_name + "_si.txt")
numpy.savetxt(size_info_file, si.bounding_box.points)


In this instance, Ruping will regenerate all the designs for this run using the regenerate.py scripts provided by each designer. Alternatively, the designers could regenerate their designs and push the final version of the GDSII to the VCS.

The generated files include the .gds files and the .txt files which store design block size information for use when arranging the design blocks relative to each other. In the regenerate.py script, after instantiating the IPKISS design cell, the GDSII format files are written, and its size information is written to a text file.

After executing regenerate.py, the folder structure will look like this:

├── chiara
    ├── gds_to_merge
    │   ├── splitter_tree_height_sweep.gds
    │   ├── splitter_tree_height_sweep_si.txt
    │   ├── splitter_tree_length_sweep.gds
    │   └── splitter_tree_length_sweep_si.txt
    ├── splitter_height_sweep
    │   ├── regenerate.py
    │   └── splitter_routed.py
    └── splitter_length_sweep
        ├── regenerate.py
        └── splitter_routed.py

Pierre’s designs are based on a custom library (libraries/pteam_library) that is built on top of the SiFab PDK. How this is managed is explained in the next tutorial Develop and distribute your component library.

Designers are free to change their designs. The latest designs are available to Ruping via the version control system. All that needs to be ensured is that the new GDS files are regenerated before the final mask merging.

1.2.2. The merge script

To merge, we use the execute_merge.py script. We start by appending the list designs which contains the designs to be merged. The list element needs to be a dictionary type with definitions of:

  • source_path: the path to the GDS file of the design to be merged.

  • prefix: the prefix name to be assigned to the GDS cell.

  • transformations: the list of transformations of the cell. Each list element of transformations is written as a list of translation in x direction, translation in y direction, rotation, and mirroring in order. Note that the rotation can only be a multiple of 90 degrees and is rounded to the nearest multiple of 90 degrees otherwise. Note also that the mirroring is with respect to the x-axis and occurs is you set that value to True.

The following first code block in execute_merge.py add the die template to the designs:

# Add the template frame
si_info_template = i3.size_info_from_numpyarray(
    numpy.loadtxt(open(os.path.join(base_dir, "template", "template_si.txt")))
        "source_path": os.path.join(base_dir, "template", "template.gds"),
        "prefix": "TEMPLATE",
        "transformations": [[0.0, 0.0, 0.0, False]],

We also notice si_info_template from the code, which stores the cell layout size information such as the cell bounding box west-most/east-most x-coordinate value, and north-most/south-most y-coordinate value. It’s loaded from a .txt file which is written with cell layout size information (we will mention this later with the regenerate.py script in each design folder).

With the same principle, we add the designs from each of the designers, for example Chiara’s splitter designs:


# User 1: Chiara
designer_name = "chiara"
user_path = os.path.join(base_dir, designer_name, "gds_to_merge")
gds_files = [fl for fl in os.listdir(user_path) if fl.endswith(".gds")]
si_info_chiara = [
    i3.size_info_from_numpyarray(numpy.loadtxt(open(os.path.join(user_path, os.path.splitext(fl)[0]) + "_si.txt")))
    for fl in gds_files
spacing = 50  # Desired spacing between designs
margin = 50  # Margin to the block edge

# Design 1
        "source_path": os.path.join(user_path, gds_files[0]),
        "prefix": designer_name.upper() + "_DESIGN_1",
        "transformations": [[0.0 + margin, 0.0 - si_info_chiara[0].south + margin, 0.0, False]],

# Design 2
        "source_path": os.path.join(user_path, gds_files[1]),
        "prefix": designer_name.upper() + "_DESIGN_2",
        "transformations": [

In the end, we will update the final merged GDS with the appended designs list:

from gds_utils.klayout.merge import merge_with_klayout

base_dir = os.path.dirname(os.path.abspath(__file__))


Note that you will need an installation of KLayout in order to run the execute_merge.py script, as it uses KLayout as the interpreter to run the Ruby script which we created to perform fast mask merging.

1.2.3. Running the merging script

Ruping runs the merge script to update the merged design, and moves the blocks around if necessary.

In this process, she is confident that she always uses the latest designs from each designer, and the updated merged mask can be written in a matter of a few seconds.

We obtain our final merged mask:


1.3. Test your knowledge

Now, it’s up to you to try to add your own design project next to Chiara, Jiejun, Pierre, and Ruping’s ones. For example, you may need to organize the project following the same structure we mentioned previously:

├── chiara
├── jiejun
├── pierre
├── ruping
├── template
├── <my_tapeout>
│    ├── gds_to_merge
│    │   ...
│    ├── <my_design_1>
│    │   ├── regenerate.py
│    │   └── <my_design_1>.py
│   ...
└── execute_merge.py

Aside from adding your own IPKISS code to the project, remember that you will also need to adapt the regenerate.py and the execute_merge.py file.