How-Tos

These are a few examples of use of the Gdstk library that go beyond basic geometry building. They should serve as reference for more complex tasks.

Parametric Cell

A parametric cell is a concept present in a few layout editors to facilitate the creation of geometries based on user-defined parameters. Gdstk does not have a parameterized cell class, but since we are building the layout from a programming language, the full flexibility of the language can be used.

In this example we define a function that returns a grating coupler based on user-defined parameters.

def grating(
    period, fill_frac=0.5, length=20, width=25, layer=0, datatype=0, cell_name="Grating"
):
    """
    Straight grating:

    Args:
        period: Grating period.
        fill_frac: Filling fraction of the teeth (wrt period).
        length: Length of the grating.
        width: Width of the grating.
        layer: GDSII layer number
        datatype: GDSII data type number

    Return:
        gdstk.Cell
    """
    result = gdstk.Cell(cell_name)
    x = width / 2
    w = period * fill_frac
    result.add(
        gdstk.rectangle(
            (-x, y * period), (x, y * period + w), layer=layer, datatype=datatype
        )
        for y in range(int(length / period))
    )
    return result

This function can be used in the following manner:

if __name__ == "__main__":
    lib = gdstk.Library()

    length = 20
    grat1 = grating(3.5, length=length, layer=1, cell_name="Grating 1")
    grat2 = grating(3.0, length=length, layer=1, cell_name="Grating 2")
    lib.add(grat1, grat2)

    main = lib.new_cell("Main")
    main.add(gdstk.rectangle((0, -10), (150, 10)))
    main.add(gdstk.Reference(grat1, (length, 0), rotation=numpy.pi / 2))
    main.add(gdstk.Reference(grat2, (150 - length, 0), rotation=-numpy.pi / 2))

    path = pathlib.Path(__file__).parent.absolute()
    lib.write_gds(path / "pcell.gds")
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

Cell* grating(double period, double fill_frac, double length, double width, Tag tag,
              const char* name) {
    double x = width / 2;
    double w = period * fill_frac;
    int64_t num = (int64_t)(length / period);

    Cell* result = (Cell*)allocate_clear(sizeof(Cell));
    result->name = copy_string(name, NULL);
    result->polygon_array.ensure_slots(num);
    for (int64_t i = 0; i < num; i++) {
        double y = i * period;
        Polygon* rect = (Polygon*)allocate(sizeof(Polygon));
        *rect = rectangle(Vec2{-x, y}, Vec2{x, y + w}, tag);
        result->polygon_array.append(rect);
    }

    return result;
}

int main(int argc, char* argv[]) {
    Library lib = {};
    lib.init("library", 1e-6, 1e-9);

    double length = 20;

    Cell* grat1 = grating(3.5, 0.5, length, 25, make_tag(1, 0), "Grating 1");
    lib.cell_array.append(grat1);

    Cell* grat2 = grating(3.0, 0.5, length, 25, make_tag(1, 0), "Grating 2");
    lib.cell_array.append(grat2);

    Cell* main_cell = (Cell*)allocate_clear(sizeof(Cell));
    main_cell->name = copy_string("Main", NULL);
    lib.cell_array.append(main_cell);

    Polygon* rect = (Polygon*)allocate(sizeof(Polygon));
    *rect = rectangle(Vec2{0, -10}, Vec2{150, 10}, 0);
    main_cell->polygon_array.append(rect);

    Reference* ref1 = (Reference*)allocate_clear(sizeof(Reference));
    ref1->init(grat1);
    ref1->origin = Vec2{length, 0};
    ref1->rotation = M_PI / 2;
    main_cell->reference_array.append(ref1);

    Reference* ref2 = (Reference*)allocate_clear(sizeof(Reference));
    ref2->type = ReferenceType::Cell, ref2->cell = grat2, ref2->origin = Vec2{150 - length, 0},
    ref2->rotation = -M_PI / 2, ref2->magnification = 1, main_cell->reference_array.append(ref2);

    lib.write_gds("pcell.gds", 0, NULL);

    lib.free_all();
    return 0;
}
_images/parametric_cell.svg

Parts Library

Creating a Library

A GDSII parts library can be used when there are several devices that are often used in different layouts. It can be a personal library of devices, or part of a process design kit (PDK) offered by the company responsible for fabrication.

Here we create a simple personal library with 3 components: an alignment mark, a directional coupler and a Mach-Zehnder interferometer. All parts are added to a GDSII file and saved for later. Note that the interferometer already uses the directional coupler as a subcomponent.

import pathlib
import numpy
import gdstk


def alignment_mark(lib):
    cross = gdstk.cross((0, 0), 50, 3, layer=1)
    lib.new_cell("Alignment Mark").add(cross)


def directional_coupler(lib):
    path = gdstk.RobustPath((0, 0), [0.5, 0.5], 2, simple_path=True, layer=1)
    path.segment((0.1, 0), relative=True)
    path.segment((2.2, 0), offset=(0.6, "smooth"), relative=True)
    path.segment((0.4, 0), relative=True)
    path.segment((2.2, 0), offset=(2, "smooth"), relative=True)
    path.segment((0.1, 0), relative=True)
    lib.new_cell("Directinal Coupler").add(path)


def mach_zehnder_interferometer(lib):
    cell = lib.new_cell("MZI")
    cell.add(gdstk.Reference("Directinal Coupler", (0, 0)))
    cell.add(gdstk.Reference("Directinal Coupler", (75, 0)))

    points = numpy.array([(5, 1), (25, 1), (25, 40), (55, 40), (55, 1), (75, 1)])
    arm1 = gdstk.FlexPath(points, 0.5, bend_radius=15, simple_path=True, layer=1)
    points[:, 1] *= -1
    arm2 = gdstk.FlexPath(points, 0.5, bend_radius=15, simple_path=True, layer=1)
    points = numpy.array([(25, 20), (25, 40), (55, 40), (55, 20)])
    heater1 = gdstk.FlexPath(points, 2, bend_radius=15, simple_path=True, layer=10)
    points[:, 1] *= -1
    heater2 = gdstk.FlexPath(points, 2, bend_radius=15, simple_path=True, layer=10)
    cell.add(arm1, arm2, heater1, heater2)


if __name__ == "__main__":
    lib = gdstk.Library("Photonics")

    alignment_mark(lib)
    directional_coupler(lib)
    mach_zehnder_interferometer(lib)

    path = pathlib.Path(__file__).parent.absolute()
    lib.write_gds(path / "photonics.gds")
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

Cell* alignment_mark() {
    Cell* cell = (Cell*)allocate_clear(sizeof(Cell));
    cell->name = copy_string("Alignment Mark", NULL);

    Polygon* cross_ = (Polygon*)allocate(sizeof(Polygon));
    *cross_ = cross(Vec2{0, 0}, 50, 3, make_tag(1, 0));
    cell->polygon_array.append(cross_);

    return cell;
}

Cell* directional_coupler() {
    Cell* cell = (Cell*)allocate_clear(sizeof(Cell));
    cell->name = copy_string("Directional Coupler", NULL);

    double widths[] = {0.5, 0.5};
    double offsets[] = {-1, 1};
    Tag tags[] = {make_tag(1, 0), make_tag(1, 0)};
    RobustPath* path = (RobustPath*)allocate_clear(sizeof(RobustPath));
    path->init(Vec2{0, 0}, 2, widths, offsets, 0.01, 1000, tags);

    Interpolation offset[4];
    offset[0].type = InterpolationType::Smooth;
    offset[0].initial_value = -1;
    offset[0].final_value = -0.3;
    offset[1].type = InterpolationType::Smooth;
    offset[1].initial_value = 1;
    offset[1].final_value = 0.3;
    offset[2].type = InterpolationType::Smooth;
    offset[2].initial_value = -0.3;
    offset[2].final_value = -1;
    offset[3].type = InterpolationType::Smooth;
    offset[3].initial_value = 0.3;
    offset[3].final_value = 1;

    path->segment(Vec2{0.1, 0}, NULL, NULL, true);
    path->segment(Vec2{2.2, 0}, NULL, offset, true);
    path->segment(Vec2{0.4, 0}, NULL, NULL, true);
    path->segment(Vec2{2.2, 0}, NULL, offset + 2, true);
    path->segment(Vec2{0.1, 0}, NULL, NULL, true);
    cell->robustpath_array.append(path);

    return cell;
}

Cell* mach_zenhder_interferometer(Cell* directional_coupler_cell) {
    Cell* cell = (Cell*)allocate_clear(sizeof(Cell));
    cell->name = copy_string("MZI", NULL);

    Reference* ref = (Reference*)allocate_clear(sizeof(Reference));
    ref->init(directional_coupler_cell);
    cell->reference_array.append(ref);

    ref = (Reference*)allocate_clear(sizeof(Reference));
    ref->init(directional_coupler_cell);
    ref->origin.x = 75;
    cell->reference_array.append(ref);

    const Vec2 starting_points[] = {{5, 1}, {5, -1}, {25, 20}, {25, -20}};
    FlexPath* path[4];
    for (int64_t i = 0; i < COUNT(path); i++) {
        path[i] = (FlexPath*)allocate_clear(sizeof(FlexPath));
        path[i]->simple_path = true;
        path[i]->init(starting_points[i], 1, i < 2 ? 0.5 : 2, 0, 0.01, make_tag(i < 2 ? 1 : 10, 0));
        path[i]->elements[0].bend_type = BendType::Circular;
        path[i]->elements[0].bend_radius = 15;
    }

    Vec2 arm_points[] = {{25, 1}, {25, 40}, {55, 40}, {55, 1}, {75, 1}};
    path[0]->segment({.capacity = 0, .count = COUNT(arm_points), .items = arm_points}, NULL, NULL,
                     false);

    for (int64_t i = 0; i < COUNT(arm_points); i++) arm_points[i].y = -arm_points[i].y;
    path[1]->segment({.capacity = 0, .count = COUNT(arm_points), .items = arm_points}, NULL, NULL,
                     false);

    Vec2 heater_points[] = {{25, 40}, {55, 40}, {55, 20}};
    path[2]->segment({.capacity = 0, .count = COUNT(heater_points), .items = heater_points}, NULL,
                     NULL, false);

    for (int64_t i = 0; i < COUNT(heater_points); i++) heater_points[i].y = -heater_points[i].y;
    path[3]->segment({.capacity = 0, .count = COUNT(heater_points), .items = heater_points}, NULL,
                     NULL, false);

    cell->flexpath_array.extend({.capacity = 0, .count = COUNT(path), .items = path});

    return cell;
}

int main(int argc, char* argv[]) {
    Library lib = {};
    lib.init("Photonics", 1e-6, 1e-9);
    lib.cell_array.append(alignment_mark());
    Cell* directional_coupler_cell = directional_coupler();
    lib.cell_array.append(directional_coupler_cell);
    lib.cell_array.append(mach_zenhder_interferometer(directional_coupler_cell));
    lib.write_gds("photonics.gds", 0, NULL);
    lib.free_all();
    return 0;
}

Using a Library

The library “photonics.gds” created above is used in the design of a larger layout. It is imported through gdstk.read_rawcells() so that is uses as little memory and processing power as possible.

Important

Using gdstk.RawCell will only work properly when both the library and the current layout use the same unit and precision.

Another option for creating libraries is to store them as python modules. The grating example in Parametric Cell can be saved in a file “photonics.py” and imported as a Python module, as long as it can be found in the Python path (leaving it in the current working directory is sufficient).

import pathlib
import numpy
import gdstk
import pcell


if __name__ == "__main__":
    path = pathlib.Path(__file__).parent.absolute()

    # Check library units. In this case it is using the default units.
    units = gdstk.gds_units(path / "photonics.gds")
    print(f"Using unit = {units[0]}, precision = {units[1]}")

    # Load the library as a dictionary of RawCell
    pdk = gdstk.read_rawcells(path / "photonics.gds")

    # Cell holding a single device (MZI)
    dev_cell = gdstk.Cell("Device")
    dev_cell.add(gdstk.Reference(pdk["MZI"], (-40, 0)))

    # Create a grating coupler using the function imported from the
    # pcell module (pcell.py) created earlier.
    grating = pcell.grating(0.62, layer=2)
    # Add 4 grating couplers to the device cell: one for each port.
    dev_cell.add(
        gdstk.Reference(
            grating, (-200, -150), rotation=numpy.pi / 2, columns=2, spacing=(300, 0)
        ),
        gdstk.Reference(
            grating, (200, 150), rotation=-numpy.pi / 2, columns=2, spacing=(300, 0)
        ),
    )

    # Create a waveguide connecting a grating to a MZI port.
    waveguide = gdstk.FlexPath((-220, -150), 20, bend_radius=15, layer=1)
    # Grating background
    waveguide.segment((20, 0), relative=True)
    # Linear taper
    waveguide.segment((-100, -150), 0.5)
    # Connection to MZI
    waveguide.segment([(-70, -150), (-70, -1), (-40, -1)])
    # Since the device is symmetrical, we can create a cell with the
    # waveguide geometry and reuse it for all 4 ports.
    wg_cell = gdstk.Cell("Waveguide")
    wg_cell.add(waveguide)

    dev_cell.add(gdstk.Reference(wg_cell))
    dev_cell.add(gdstk.Reference(wg_cell, x_reflection=True))
    dev_cell.add(gdstk.Reference(wg_cell, rotation=numpy.pi))
    dev_cell.add(gdstk.Reference(wg_cell, rotation=numpy.pi, x_reflection=True))

    # Main cell with 2 devices and lithography alignment marks
    main = gdstk.Cell("Main")
    main.add(
        gdstk.Reference(dev_cell, (250, 250)),
        gdstk.Reference(dev_cell, (250, 750)),
        gdstk.Reference(pdk["Alignment Mark"], columns=2, rows=3, spacing=(500, 500)),
    )

    lib = gdstk.Library()
    lib.add(main, *main.dependencies(True))
    lib.write_gds(path / "layout.gds")
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

// We redefine the grating function from pcell.cpp here instead of including
// the file because it contains a main function already.  In practice, the
// library source would only contain the relevant functions, but no main.
Cell* grating(double period, double fill_frac, double length, double width, Tag tag,
              const char* name) {
    double x = width / 2;
    double w = period * fill_frac;
    int64_t num = (int64_t)(length / period);

    Cell* result = (Cell*)allocate_clear(sizeof(Cell));
    result->name = copy_string(name, NULL);
    result->polygon_array.ensure_slots(num);
    for (int64_t i = 0; i < num; i++) {
        double y = i * period;
        Polygon* rect = (Polygon*)allocate(sizeof(Polygon));
        *rect = rectangle(Vec2{-x, y}, Vec2{x, y + w}, tag);
        result->polygon_array.append(rect);
    }

    return result;
}

int main(int argc, char* argv[]) {
    double unit = 0;
    double precision = 0;

    ErrorCode error_code = gds_units("photonics.gds", unit, precision);
    if (error_code != ErrorCode::NoError) exit(EXIT_FAILURE);

    printf("Using unit = %.3g, precision = %.3g\n", unit, precision);

    Map<RawCell*> pdk = read_rawcells("photonics.gds", NULL);

    Cell* dev_cell = (Cell*)allocate_clear(sizeof(Cell));
    dev_cell->name = copy_string("Device", NULL);

    Reference* mzi_ref = (Reference*)allocate_clear(sizeof(Reference));
    mzi_ref->init(pdk.get("MZI"));
    mzi_ref->origin = Vec2{-40, 0};
    dev_cell->reference_array.append(mzi_ref);

    Cell* grating_cell = grating(0.62, 0.5, 20, 25, make_tag(2, 0), "Grating");

    // We set type of these references to Regular so that we can apply the
    // rotation to the translation vectors v1 and v2 of the repetition. This
    // way, the GDSII writer will create and AREF element instead of multiple
    // SREFs. If x_reflection was set to true, that would also have to be
    // applied to v2 for an AREF to be created.
    Reference* grating_ref1 = (Reference*)allocate_clear(sizeof(Reference));
    grating_ref1->init(grating_cell);
    grating_ref1->origin = Vec2{-200, -150};
    grating_ref1->rotation = M_PI / 2;
    grating_ref1->repetition.type = RepetitionType::Regular;
    grating_ref1->repetition.columns = 2;
    grating_ref1->repetition.rows = 1;
    grating_ref1->repetition.v1 = Vec2{0, 300};
    grating_ref1->repetition.v2 = Vec2{1, 0};
    dev_cell->reference_array.append(grating_ref1);

    Reference* grating_ref2 = (Reference*)allocate_clear(sizeof(Reference));
    grating_ref2->init(grating_cell);
    grating_ref2->origin = Vec2{200, 150};
    grating_ref2->rotation = -M_PI / 2;
    grating_ref2->repetition.type = RepetitionType::Regular;
    grating_ref2->repetition.columns = 2;
    grating_ref2->repetition.rows = 1;
    grating_ref2->repetition.v1 = Vec2{0, -300};
    grating_ref2->repetition.v2 = Vec2{1, 0};

    dev_cell->reference_array.append(grating_ref2);

    FlexPath* waveguide = (FlexPath*)allocate_clear(sizeof(FlexPath));
    waveguide->init(Vec2{-220, -150}, 1, 20, 0, 0.01, make_tag(1, 0));
    waveguide->elements[0].bend_type = BendType::Circular;
    waveguide->elements[0].bend_radius = 15;

    waveguide->segment(Vec2{20, 0}, NULL, NULL, true);

    const double w = 0.5;
    waveguide->segment(Vec2{-100, -150}, &w, NULL, false);

    Vec2 p[] = {{-70, -150}, {-70, -1}, {-40, -1}};
    waveguide->segment({.capacity = 0, .count = COUNT(p), .items = p}, NULL, NULL, false);

    Cell* wg_cell = (Cell*)allocate_clear(sizeof(Cell));
    wg_cell->name = copy_string("Waveguide", NULL);
    wg_cell->flexpath_array.append(waveguide);

    for (uint64_t i = 0; i < 4; i++) {
        Reference* wg_ref = (Reference*)allocate_clear(sizeof(Reference));
        wg_ref->init(wg_cell);
        if (i == 1) {
            wg_ref->x_reflection = true;
        } else if (i == 2) {
            wg_ref->rotation = M_PI;
        } else if (i == 3) {
            wg_ref->rotation = M_PI;
            wg_ref->x_reflection = true;
        }
        dev_cell->reference_array.append(wg_ref);
    }

    Cell* main_cell = (Cell*)allocate_clear(sizeof(Cell));
    main_cell->name = copy_string("Main", NULL);

    for (uint64_t i = 0; i < 2; i++) {
        Reference* dev_ref = (Reference*)allocate_clear(sizeof(Reference));
        dev_ref->init(dev_cell);
        dev_ref->origin = i == 0 ? Vec2{250, 250} : Vec2{250, 750};
        main_cell->reference_array.append(dev_ref);
    }

    Reference* align_ref = (Reference*)allocate_clear(sizeof(Reference));
    align_ref->init(pdk.get("Alignment Mark"));
    align_ref->repetition = {RepetitionType::Rectangular, 2, 3, Vec2{500, 500}};
    main_cell->reference_array.append(align_ref);

    Library lib = {};
    lib.init("library", unit, precision);
    lib.cell_array.append(main_cell);

    Map<Cell*> dependencies = {};
    main_cell->get_dependencies(true, dependencies);
    dependencies.to_array(lib.cell_array);
    dependencies.clear();

    Map<RawCell*> raw_dependencies = {};
    main_cell->get_raw_dependencies(true, raw_dependencies);
    raw_dependencies.to_array(lib.rawcell_array);
    raw_dependencies.clear();

    lib.write_gds("layout.gds", 0, NULL);

    lib.free_all();

    // Library::free_all does not free RawCells
    for (MapItem<RawCell*>* item = pdk.next(NULL); item; item = pdk.next(item)) {
        item->value->clear();
        free_allocation(item->value);
    }
    pdk.clear();

    return 0;
}
_images/layout.svg

Merging Libraries

Merging two or more libraries is only a matter of adding all the cells from one into the other. Extra cells can be latter added, of course, to create new top level cells with references from both originals. In this example, we only merge two GDSII files into a new one—which will end up with 2 top level cells—and take care of renaming all cells so that they don’t collide.

import pathlib
import gdstk


def make_first_lib(filename):
    lib = gdstk.Library("First")
    main = lib.new_cell("Main")
    main.add(*gdstk.text("First library", 10, (0, 0)))
    ref1 = lib.new_cell("Square")
    ref1.add(gdstk.rectangle((-15, 0), (-5, 10)))
    main.add(gdstk.Reference(ref1))
    ref2 = lib.new_cell("Circle")
    ref2.add(gdstk.ellipse((0, 0), 4))
    ref1.add(gdstk.Reference(ref2, (-10, 5)))
    lib.write_gds(filename)


def make_second_lib(filename):
    lib = gdstk.Library("Second")
    main = lib.new_cell("Main")
    main.add(*gdstk.text("Second library", 10, (0, 0)))
    ref = lib.new_cell("Circle")
    ref.add(gdstk.ellipse((-10, 5), 5))
    main.add(gdstk.Reference(ref))
    lib.write_gds(filename)


if __name__ == "__main__":
    path = pathlib.Path(__file__).parent.absolute()

    # First we create the two libraries we'll be merging
    make_first_lib(path / "lib1.gds")
    make_second_lib(path / "lib2.gds")

    # Now we load the existing libraries
    lib1 = gdstk.read_gds(path / "lib1.gds")
    lib2 = gdstk.read_gds(path / "lib2.gds")

    # We add all cells from the second library to the first
    lib1_cell_names = {c.name for c in lib1.cells}
    for cell in lib2.cells:
        # We must check that all names are unique within the merged library
        if cell.name in lib1_cell_names:
            cell.name += "-lib2"
            assert cell.name not in lib1_cell_names
        # Now we add the cell and update the set of names
        lib1.add(cell)
        lib1_cell_names.add(cell.name)

    lib1.write_gds(path / "merging.gds")
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

void make_first_lib(const char* filename) {
    char lib_name[] = "First";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};

    char main_cell_name[] = "Main";
    Cell main_cell = {.name = main_cell_name};
    lib.cell_array.append(&main_cell);

    Array<Polygon*> allocated_polygons = {};
    text("First library", 10, Vec2{0, 0}, false, 0, allocated_polygons);
    main_cell.polygon_array.extend(allocated_polygons);

    char square_cell_name[] = "Square";
    Cell square_cell = {.name = square_cell_name};
    lib.cell_array.append(&square_cell);

    Polygon square = rectangle(Vec2{-15, 0}, Vec2{-5, 10}, 0);
    square_cell.polygon_array.append(&square);

    Reference square_referece = {
        .type = ReferenceType::Cell,
        .cell = &square_cell,
        .magnification = 1,
    };
    main_cell.reference_array.append(&square_referece);

    char circle_cell_name[] = "Circle";
    Cell circle_cell = {.name = circle_cell_name};
    lib.cell_array.append(&circle_cell);

    Polygon circle = ellipse(Vec2{0, 0}, 4, 4, 0, 0, 0, 0, 0.01, 0);
    circle_cell.polygon_array.append(&circle);

    Reference circle_referece = {
        .type = ReferenceType::Cell,
        .cell = &circle_cell,
        .origin = {-10, 5},
        .magnification = 1,
    };
    square_cell.reference_array.append(&circle_referece);

    lib.write_gds(filename, 0, NULL);

    lib.cell_array.clear();
    main_cell.polygon_array.clear();
    main_cell.reference_array.clear();
    square_cell.polygon_array.clear();
    square_cell.reference_array.clear();
    circle_cell.polygon_array.clear();
    square.clear();
    circle.clear();
    for (uint64_t i = 0; i < allocated_polygons.count; i++) {
        allocated_polygons[i]->clear();
        free_allocation(allocated_polygons[i]);
    }
    allocated_polygons.clear();
}

void make_second_lib(const char* filename) {
    char lib_name[] = "Second";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};

    char main_cell_name[] = "Main";
    Cell main_cell = {.name = main_cell_name};
    lib.cell_array.append(&main_cell);

    Array<Polygon*> allocated_polygons = {};
    text("Second library", 10, Vec2{0, 0}, false, 0, allocated_polygons);
    main_cell.polygon_array.extend(allocated_polygons);

    char circle_cell_name[] = "Circle";
    Cell circle_cell = {.name = circle_cell_name};
    lib.cell_array.append(&circle_cell);

    Polygon circle = ellipse(Vec2{-10, 5}, 5, 5, 0, 0, 0, 0, 0.01, 0);
    circle_cell.polygon_array.append(&circle);

    Reference circle_referece = {
        .type = ReferenceType::Cell,
        .cell = &circle_cell,
        .magnification = 1,
    };
    main_cell.reference_array.append(&circle_referece);

    lib.write_gds(filename, 0, NULL);

    lib.cell_array.clear();
    main_cell.polygon_array.clear();
    main_cell.reference_array.clear();
    circle_cell.polygon_array.clear();
    circle.clear();
    for (uint64_t i = 0; i < allocated_polygons.count; i++) {
        allocated_polygons[i]->clear();
        free_allocation(allocated_polygons[i]);
    }
    allocated_polygons.clear();
}

int main(int argc, char* argv[]) {
    make_first_lib("lib1.gds");
    make_second_lib("lib2.gds");

    Library lib1 = read_gds("lib1.gds", 0, 1e-2, NULL, NULL);
    Library lib2 = read_gds("lib2.gds", 0, 1e-2, NULL, NULL);

    // We could use a hash table to make this more efficient, but we're aiming
    // for simplicity.
    for (uint64_t i = 0; i < lib2.cell_array.count; i++) {
        Cell* cell = lib2.cell_array[i];
        for (uint64_t j = 0; j < lib1.cell_array.count; j++) {
            if (strcmp(cell->name, lib1.cell_array[j]->name) == 0) {
                uint64_t len = strlen(cell->name);
                cell->name = (char*)reallocate(cell->name, len + 6);
                strcpy(cell->name + len, "-lib2");
                // We should make sure the new name is also unique, but we are
                // skipping that.
                break;
            }
        }
        lib1.cell_array.append(cell);
    }

    lib1.write_gds("merging.gds", 0, NULL);

    // Avoid double-freeing cells from lib2
    lib2.cell_array.clear();
    lib2.free_all();
    lib1.free_all();

    return 0;
}

Transformations

Geometry transformations can be accomplished in several ways. Individual polygons or paths can be transformed by their respective methods (gdstk.Polygon.scale(), gdstk.FlexPath.rotate(), gdstk.RobustPath.translate(), etc.).

In order to transform an entire gdstk.Cell, we can use a gdstk.Reference with the desired transformation or create a transformed copy with gdstk.Cell.copy(). The former has the advantage of using less memory, because it does not create actual copies of the geometry or labels, so it is generally preferable. The latter is particularly useful when changes to the transformed cell contents are needed and the original should not be modified.

import numpy
import gdstk


if __name__ == "__main__":
    n = 3  # Number of unit cells around defect
    d = 0.2  # Unit cell size
    r = 0.05  # Circle radius
    s = 1.5  # Scaling factor

    # Create a simple unit cell
    unit_cell = gdstk.Cell("Unit Cell")
    unit_cell.add(gdstk.ellipse((0, 0), r, tolerance=1e-3))

    # Build a resonator from a unit cell grid with a defect inside
    ressonator = gdstk.Cell("Resonator")
    patches = [
        gdstk.Reference(
            unit_cell, (-n * d, -n * d), columns=2 * n + 1, rows=n, spacing=(d, d)
        ),
        gdstk.Reference(
            unit_cell, (-n * d, d), columns=2 * n + 1, rows=n, spacing=(d, d)
        ),
        gdstk.Reference(unit_cell, (-n * d, 0), columns=n, rows=1, spacing=(d, d)),
        gdstk.Reference(unit_cell, (d, 0), columns=n, rows=1, spacing=(d, d)),
    ]
    # Defect
    rect = gdstk.rectangle((-r / 2, -r / 2), (r / 2, r / 2))
    # Path for illustration
    path = gdstk.FlexPath(
        [(-n * d, 0), (n * d, 0)],
        r,
        ends=(r, r),
        simple_path=True,
        scale_width=False,
        layer=1,
    )
    ressonator.add(rect, path, *patches)

    # Main output cell with the original resonator,…
    main = gdstk.Cell("Main")
    main.add(gdstk.Reference(ressonator))
    main.add(*gdstk.text("Original", d, ((n + 1) * d, -d / 2)))

    # … a copy created by scaling a reference to the original resonator,…
    main.add(gdstk.Reference(ressonator, (0, (1 + s) * (n + 1) * d), magnification=s))
    main.add(
        *gdstk.text("Reference\nscaling", d, (s * (n + 1) * d, (1 + s) * (n + 1) * d))
    )

    # … and another copy created by copying and scaling the Cell itself.
    ressonator_copy = ressonator.copy("Resonator Copy", magnification=s)
    main.add(gdstk.Reference(ressonator_copy, (0, (1 + 3 * s) * (n + 1) * d)))
    main.add(
        *gdstk.text(
            "Cell copy\nscaling", d, (s * (n + 1) * d, (1 + 3 * s) * (n + 1) * d)
        )
    )
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    char lib_name[] = "library";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};

    int64_t n = 3;    // Unit cells around defect
    double d = 0.2;   // Unit cell size
    double r = 0.05;  // Circle radius
    double s = 1.5;   // Scaling factor

    char unit_cell_name[] = "Unit Cell";
    Cell unit_cell = {.name = unit_cell_name};
    lib.cell_array.append(&unit_cell);

    Polygon circle = ellipse(Vec2{0, 0}, r, r, 0, 0, 0, 0, 1e-3, 0);
    unit_cell.polygon_array.append(&circle);

    char resonator_cell_name[] = "Resonator";
    Cell resonator_cell = {.name = resonator_cell_name};
    lib.cell_array.append(&resonator_cell);

    Reference unit_refs[] = {
        {
            .type = ReferenceType::Cell,
            .cell = &unit_cell,
            .origin = Vec2{-n * d, -n * d},
            .magnification = 1,
            .repetition = {RepetitionType::Rectangular, (uint16_t)(2 * n + 1), (uint16_t)n,
                           Vec2{d, d}},
        },
        {
            .type = ReferenceType::Cell,
            .cell = &unit_cell,
            .origin = Vec2{-n * d, d},
            .magnification = 1,
            .repetition = {RepetitionType::Rectangular, (uint16_t)(2 * n + 1), (uint16_t)n,
                           Vec2{d, d}},
        },
        {
            .type = ReferenceType::Cell,
            .cell = &unit_cell,
            .origin = Vec2{-n * d, 0},
            .magnification = 1,
            .repetition = {RepetitionType::Rectangular, (uint16_t)n, 1, Vec2{d, d}},
        },
        {
            .type = ReferenceType::Cell,
            .cell = &unit_cell,
            .origin = Vec2{d, 0},
            .magnification = 1,
            .repetition = {RepetitionType::Rectangular, (uint16_t)n, 1, Vec2{d, d}},
        },
    };
    Reference* unit_refs_p[] = {unit_refs, unit_refs + 1, unit_refs + 2, unit_refs + 3};
    resonator_cell.reference_array.extend({.capacity = 0, .count = 4, .items = unit_refs_p});

    Polygon rect = rectangle(Vec2{-r / 2, -r / 2}, Vec2{r / 2, r / 2}, 0);
    resonator_cell.polygon_array.append(&rect);

    FlexPath path = {
        .simple_path = true,
        .scale_width = false,
    };
    path.init(Vec2{-n * d, 0}, 1, r, 0, 0.01, make_tag(1, 0));
    path.elements[0].end_type = EndType::Extended;
    path.elements[0].end_extensions = Vec2{r, r};
    path.segment(Vec2{n * d, 0}, NULL, NULL, false);
    resonator_cell.flexpath_array.append(&path);

    char main_cell_name[] = "Main";
    Cell main_cell = {.name = main_cell_name};
    lib.cell_array.append(&main_cell);

    Cell resonator_cell_copy = {};
    lib.cell_array.append(&resonator_cell_copy);
    resonator_cell_copy.copy_from(resonator_cell, "Resonator Copy", true);
    for (int64_t i = 0; i < resonator_cell_copy.polygon_array.count; i++) {
        Polygon* p = resonator_cell_copy.polygon_array[i];
        p->scale(Vec2{s, s}, Vec2{0, 0});
    }
    for (int64_t i = 0; i < resonator_cell_copy.flexpath_array.count; i++) {
        FlexPath* fp = resonator_cell_copy.flexpath_array[i];
        fp->scale(s, Vec2{0, 0});
    }
    for (int64_t i = 0; i < resonator_cell_copy.robustpath_array.count; i++) {
        RobustPath* rp = resonator_cell_copy.robustpath_array[i];
        rp->scale(s, Vec2{0, 0});
    }
    for (int64_t i = 0; i < resonator_cell_copy.reference_array.count; i++) {
        Reference* ref = resonator_cell_copy.reference_array[i];
        ref->transform(s, false, 0, Vec2{0, 0});
        ref->repetition.transform(s, false, 0);
    }

    Reference resonator_refs[] = {
        {
            .type = ReferenceType::Cell,
            .cell = &resonator_cell,
            .magnification = 1,
        },
        {
            .type = ReferenceType::Cell,
            .cell = &resonator_cell,
            .origin = Vec2{0, (1 + s) * (n + 1) * d},
            .magnification = s,
        },
        {
            .type = ReferenceType::Cell,
            .cell = &resonator_cell_copy,
            .origin = Vec2{0, (1 + 3 * s) * (n + 1) * d},
            .magnification = 1,
        },
    };
    Reference* resonator_refs_p[] = {resonator_refs, resonator_refs + 1, resonator_refs + 2};
    main_cell.reference_array.extend({.capacity = 0, .count = 3, .items = resonator_refs_p});

    Array<Polygon*> all_text = {};
    text("Original", d, Vec2{(n + 1) * d, -d / 2}, false, 0, all_text);
    text("Reference\nscaling", d, Vec2{s * (n + 1) * d, (1 + s) * (n + 1) * d}, false, 0, all_text);
    text("Cell copy\nscaling", d, Vec2{s * (n + 1) * d, (1 + 3 * s) * (n + 1) * d}, false, 0,
         all_text);
    main_cell.polygon_array.extend(all_text);

    lib.write_gds("transforms.gds", 0, NULL);

    for (uint16_t i = 0; i < all_text.count; i++) {
        all_text[i]->clear();
        free_allocation(all_text[i]);
    }
    all_text.clear();
    circle.clear();
    rect.clear();
    path.clear();
    unit_cell.polygon_array.clear();
    resonator_cell.reference_array.clear();
    resonator_cell.polygon_array.clear();
    resonator_cell.flexpath_array.clear();
    resonator_cell_copy.free_all();
    main_cell.reference_array.clear();
    main_cell.polygon_array.clear();
    lib.cell_array.clear();
    return 0;
}
_images/transforms.svg

Note

The SVG output does not support the scale_width attribute of paths, that is why the width of the path in the referencer-scaled version of the geometry is wider that the original. When using gdstk.Cell.copy(), the attribute is respected. This is also a problem for some GDSII viewers and editors.

Repetitions

References can be effectively used to instantiate repetitive geometry across a layout. Repetition is an extension of that idea which allows the reuse of any element without the need for creating a gdstk.Cell. In fact, the creation of a gdstk.Reference as an array is only a shortcut to the creation of a single reference with a rectangular (or regular) repetition. The following example demonstrates the use of different forms of repetition to avoid creating all objects in memory (the final GDSII file will contain all copies).

import numpy
import gdstk


if __name__ == "__main__":
    # Rectangular repetition
    square = gdstk.regular_polygon((0, 0), 0.2, 4)
    square.repetition = gdstk.Repetition(3, 2, spacing=(1, 1))

    # Regular repetition
    triangle = gdstk.regular_polygon((0, 2.5), 0.2, 3)
    triangle.repetition = gdstk.Repetition(3, 5, v1=(0.4, -0.3), v2=(0.4, 0.2))

    # Explicit repetition
    circle = gdstk.ellipse((3.5, 0), 0.1)
    circle.repetition = gdstk.Repetition(offsets=[(0.5, 1), (2, 0), (1.5, 0.5)])

    # X-explicit repetition
    vline = gdstk.FlexPath([(3, 2), (3, 3.5)], 0.1, simple_path=True)
    vline.repetition = gdstk.Repetition(x_offsets=[0.2, 0.6, 1.4, 3.0])

    # Y-explicit repetition
    hline = gdstk.RobustPath((3, 2), 0.05, simple_path=True)
    hline.segment((6, 2))
    hline.repetition = gdstk.Repetition(y_offsets=[0.1, 0.3, 0.7, 1.5])

    main = gdstk.Cell("Main")
    main.add(square, triangle, circle, vline, hline)
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    char lib_name[] = "library";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};

    char main_cell_name[] = "Main";
    Cell main_cell = {.name = main_cell_name};
    lib.cell_array.append(&main_cell);

    Polygon square = regular_polygon(Vec2{0, 0}, 0.2, 4, 0, 0);
    square.repetition = {RepetitionType::Rectangular, 3, 2, Vec2{1, 1}};
    main_cell.polygon_array.append(&square);

    Polygon triangle = regular_polygon(Vec2{0, 2.5}, 0.2, 3, 0, 0);
    triangle.repetition.type = RepetitionType::Regular;
    triangle.repetition.columns = 3;
    triangle.repetition.rows = 5;
    triangle.repetition.v1 = Vec2{0.4, -0.3};
    triangle.repetition.v2 = Vec2{0.4, 0.2};
    main_cell.polygon_array.append(&triangle);

    Polygon circle = ellipse(Vec2{3.5, 0}, 0.1, 0.1, 0, 0, 0, 0, 0.01, 0);
    Vec2 offsets[] = {{0.5, 1}, {2, 0}, {1.5, 0.5}};
    circle.repetition.type = RepetitionType::Explicit;
    circle.repetition.offsets.extend({.capacity = 0, .count = COUNT(offsets), .items = offsets});
    main_cell.polygon_array.append(&circle);

    FlexPath vline = {};
    vline.init(Vec2{3, 2}, 1, 0.1, 0, 0.01, 0);
    vline.simple_path = true;
    vline.segment(Vec2{3, 3.5}, NULL, NULL, false);
    double vcoords[] = {0.2, 0.6, 1.4, 3.0};
    vline.repetition.type = RepetitionType::ExplicitX;
    vline.repetition.coords.extend({.capacity = 0, .count = COUNT(vcoords), .items = vcoords});
    main_cell.flexpath_array.append(&vline);

    RobustPath hline = {};
    hline.init(Vec2{3, 2}, 1, 0.05, 0, 0.01, 1000, 0);
    hline.simple_path = true;
    hline.segment(Vec2{6, 2}, NULL, NULL, false);
    double hcoords[] = {0.1, 0.3, 0.7, 1.5};
    hline.repetition.type = RepetitionType::ExplicitY;
    hline.repetition.coords.extend({.capacity = 0, .count = COUNT(hcoords), .items = hcoords});
    main_cell.robustpath_array.append(&hline);

    lib.write_gds("repetitions.gds", 0, NULL);

    square.clear();
    triangle.clear();
    circle.clear();
    vline.clear();
    hline.clear();
    main_cell.polygon_array.clear();
    main_cell.flexpath_array.clear();
    main_cell.robustpath_array.clear();
    lib.cell_array.clear();
    return 0;
}
_images/repetitions.svg

When geometry operations are applied to elements with repetitions, they are not automatically applied. If desired the repetition can be manually applied before executing the desired operation. The following example demonstrates this use:

import numpy
import gdstk


if __name__ == "__main__":
    # X-explicit repetition
    vline = gdstk.FlexPath([(3, 2), (3, 3.5)], 0.1, simple_path=True)
    vline.repetition = gdstk.Repetition(x_offsets=[0.2, 0.6, 1.4, 3.0])

    # Y-explicit repetition
    hline = gdstk.RobustPath((3, 2), 0.05, simple_path=True)
    hline.segment((6, 2))
    hline.repetition = gdstk.Repetition(y_offsets=[0.1, 0.3, 0.7, 1.5])

    # Create all copies
    vlines = vline.apply_repetition()
    hlines = hline.apply_repetition()

    # Include original elements for boolean operation
    vlines.append(vline)
    hlines.append(hline)

    result = gdstk.boolean(vlines, hlines, "or")

    main = gdstk.Cell("Main")
    main.add(*result)
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    Library lib = {};
    lib.init("library", 1e-6, 1e-9);

    Cell main_cell = {};
    main_cell.name = copy_string("Main", NULL);
    lib.cell_array.append(&main_cell);

    FlexPath vline = {};
    vline.init(Vec2{3, 2}, 1, 0.1, 0, 0.01, 0);
    vline.simple_path = true;
    vline.segment(Vec2{3, 3.5}, NULL, NULL, false);
    double vcoords[] = {0.2, 0.6, 1.4, 3.0};
    vline.repetition.type = RepetitionType::ExplicitX;
    vline.repetition.coords.extend({.capacity = 0, .count = COUNT(vcoords), .items = vcoords});

    Array<Polygon*> vlines = {};
    vline.to_polygons(false, 0, vlines);
    vline.clear();

    // Because we know there is only a single resulting polygon we dont need to
    // loop here.
    vlines[0]->apply_repetition(vlines);

    RobustPath hline = {};
    hline.init(Vec2{3, 2}, 1, 0.05, 0, 0.01, 1000, 0);
    hline.simple_path = true;
    hline.segment(Vec2{6, 2}, NULL, NULL, false);
    double hcoords[] = {0.1, 0.3, 0.7, 1.5};
    hline.repetition.type = RepetitionType::ExplicitY;
    hline.repetition.coords.extend({.capacity = 0, .count = COUNT(hcoords), .items = hcoords});

    Array<Polygon*> hlines = {};
    hline.to_polygons(false, 0, hlines);
    hline.clear();

    // Once again, no loop needed.
    hlines[0]->apply_repetition(vlines);

    Array<Polygon*> result = {};
    boolean(vlines, hlines, Operation::Or, 1000, result);
    for (uint64_t i = 0; i < vlines.count; i++) {
        vlines[i]->clear();
        free_allocation(vlines[i]);
    }
    vlines.clear();
    for (uint64_t i = 0; i < hlines.count; i++) {
        hlines[i]->clear();
        free_allocation(hlines[i]);
    }
    hlines.clear();

    main_cell.polygon_array.extend(result);
    result.clear();

    lib.write_gds("apply_repetition.gds", 0, NULL);

    lib.clear();
    main_cell.free_all();
    return 0;
}
_images/apply_repetition.svg

Geometry Filtering

Filtering the geometry of a loaded library requires only iterating over the desired cells and objects, testing and removing those not wanted. In this example we load the layout created in Using a Library and remove the polygons in layer 2 (grating teeth) and paths in layer 10 (in the MZI).

import pathlib
import gdstk


if __name__ == "__main__":
    path = pathlib.Path(__file__).parent.absolute()

    # Load existing library
    lib = gdstk.read_gds(path / "layout.gds")

    for cell in lib.cells:
        # Remove any polygons in layer 2
        cell.filter([(2, 0)], paths=False, labels=False)
        # Remove any paths in layer 10
        cell.filter([(10, 0)], polygons=False, labels=False)

    lib.write_gds(path / "filtered-layout.gds")
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    ErrorCode error_code = ErrorCode::NoError;
    Library lib = read_gds("layout.gds", 0, 1e-2, NULL, &error_code);
    if (error_code != ErrorCode::NoError) exit(EXIT_FAILURE);

    for (int64_t i = 0; i < lib.cell_array.count; i++) {
        Cell* cell = lib.cell_array[i];
        for (int64_t j = 0; j < cell->polygon_array.count; j++) {
            Polygon* poly = cell->polygon_array[j];
            // Decrement j so that we don't skip over the next polygon.
            if (get_layer(poly->tag) == 2) {
                cell->polygon_array.remove(j--);
                poly->clear();
                free_allocation(poly);
            }
        }
        // Loaded libraries have no RobustPath elements
        for (int64_t j = 0; j < cell->flexpath_array.count; j++) {
            FlexPath* fp = cell->flexpath_array[j];
            // All paths in loaded libraries have only 1 element.
            // Decrement j so that we don't skip over the next path.
            if (get_layer(fp->elements[0].tag) == 10) {
                cell->flexpath_array.remove(j--);
                fp->clear();
                free_allocation(fp);
            }
        }
    }

    lib.write_gds("filtered-layout.gds", 0, NULL);
    lib.free_all();
    return 0;
}
_images/filtering.svg

Another common use of filtering is to remove geometry in a particular region. In this example we create a periodic background and remove all elements that overlap a particular shape using gdstk.inside() to test.

import pathlib
import gdstk


if __name__ == "__main__":
    path = pathlib.Path(__file__).parent.absolute()

    unit = gdstk.Cell("Unit")
    unit.add(gdstk.cross((0, 0), 1, 0.2))

    main = gdstk.Cell("Main")

    # Create repeating pattern using references
    d = 2
    ref1 = gdstk.Reference(unit, columns=11, rows=6, spacing=(d, d * 3**0.5))
    ref2 = gdstk.Reference(
        unit, (d / 2, d * 3**0.5 / 2), columns=10, rows=5, spacing=(d, d * 3**0.5)
    )
    main.add(ref1, ref2)
    main.flatten()

    hole = gdstk.text("PY", 8 * d, (0.5 * d, 0), layer=1)
    for pol in main.polygons:
        if gdstk.any_inside(pol.points, hole):
            main.remove(pol)

    main.add(*hole)

    gdstk.Library().add(main).write_gds(path / "pos_filtering.gds")
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    char lib_name[] = "library";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};

    char unit_cell_name[] = "Unit";
    Cell unit_cell = {.name = unit_cell_name};

    Polygon cross_ = cross(Vec2{0, 0}, 1, 0.2, 0);
    unit_cell.polygon_array.append(&cross_);

    char main_cell_name[] = "Main";
    Cell main_cell = {.name = main_cell_name};
    lib.cell_array.append(&main_cell);

    double d = 2;
    Reference unit_refs[] = {
        {
            .type = ReferenceType::Cell,
            .cell = &unit_cell,
            .magnification = 1,
            .repetition = {RepetitionType::Rectangular, 11, 6, Vec2{d, d * sqrt(3)}},
        },
        {
            .type = ReferenceType::Cell,
            .cell = &unit_cell,
            .origin = Vec2{d / 2, d * sqrt(3) / 2},
            .magnification = 1,
            .repetition = {RepetitionType::Rectangular, 10, 5, Vec2{d, d * sqrt(3)}},
        },
    };
    main_cell.reference_array.append(unit_refs);
    main_cell.reference_array.append(unit_refs + 1);

    Array<Reference*> removed_references = {};
    main_cell.flatten(true, removed_references);
    removed_references.clear();

    Array<Polygon*> txt = {};
    text("PY", 8 * d, Vec2{0.5 * d, 0}, false, make_tag(1, 0), txt);
    for (uint64_t i = 0; i < main_cell.polygon_array.count; i++) {
        Polygon* poly = main_cell.polygon_array[i];
        if (any_inside(poly->point_array, txt)) {
            poly->clear();
            free_allocation(poly);
            main_cell.polygon_array.remove(i--);
        }
    }

    main_cell.polygon_array.extend(txt);
    txt.clear();

    lib.write_gds("pos_filtering.gds", 0, NULL);

    cross_.clear();
    for (uint64_t i = 0; i < main_cell.polygon_array.count; i++) {
        main_cell.polygon_array[i]->clear();
        free_allocation(main_cell.polygon_array[i]);
    }
    main_cell.reference_array.clear();
    main_cell.polygon_array.clear();
    unit_cell.polygon_array.clear();
    lib.cell_array.clear();
    return 0;
}
_images/pos_filtering.svg

Finally, gdstk.read_gds() provides a way to only load specific layers and data types from a GDSII file, and methods gdstk.Cell.get_polygons(), gdstk.Cell.get_paths(), gdstk.Cell.get_labels() can also be used to gather elements only from specific layers and types.

Points Along a Path

The following example shows how to add markers along a gdstk.RobustPath. It uses the original parameterization of the path to locate the markers, following the construction sections. Markers positioned at a fixed distance must be calculated for each section independently.

import numpy
import gdstk


if __name__ == "__main__":
    main = gdstk.Cell("Main")

    # Create a path
    path = gdstk.RobustPath((0, 0), 0.5)
    path.segment((8, 0))
    path.interpolation(
        [(2, -4), (-2, -6), (-5, -8), (-4, -12)],
        angles=[0, None, None, None, -numpy.pi / 4],
        relative=True,
    )
    path.segment((3, -3), relative=True)
    main.add(path)

    # Major and minor markers
    major = gdstk.regular_polygon((0, 0), 0.5, 6, layer=1)
    minor = gdstk.rectangle((-0.1, -0.5), (0.1, 0.5), layer=1)
    for s in range(path.size):
        # A major marker is added at the start of each path section
        m = major.copy()
        m.translate(path.position(s))
        main.add(m)
        for u in numpy.linspace(0, 1, 5)[1:-1]:
            # Each section receives 3 equally-spaced minor markers
            # rotated to be aligned to the path direction
            m = minor.copy()
            grad = path.gradient(s + u)
            m.rotate(numpy.arctan2(grad[1], grad[0]))
            m.translate(path.position(s + u))
            main.add(m)
    # Add a major marker at the end of the path
    major.translate(path.position(path.size))
    main.add(major)
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    Library lib = {};
    lib.init("library", 1e-6, 1e-9);

    Cell* main_cell = (Cell*)allocate_clear(sizeof(Cell));
    main_cell->name = copy_string("Main", NULL);
    lib.cell_array.append(main_cell);

    RobustPath* path = (RobustPath*)allocate_clear(sizeof(RobustPath));
    path->init(Vec2{0, 0}, 1, 0.5, 0, 0.01, 1000, 0);
    path->segment(Vec2{8, 0}, NULL, NULL, false);

    Vec2 points[] = {{2, -4}, {-2, -6}, {-5, -8}, {-4, -12}};
    double angles[] = {0, 0, 0, 0, -M_PI / 4};
    bool angle_constraints[] = {true, false, false, false, true};
    Vec2 tension[] = {{1, 1}, {1, 1}, {1, 1}, {1, 1}, {1, 1}};
    path->interpolation({.capacity = 0, .count = COUNT(points), .items = points}, angles,
                        angle_constraints, tension, 1, 1, false, NULL, NULL, true);

    path->segment(Vec2{3, -3}, NULL, NULL, true);
    main_cell->robustpath_array.append(path);

    Polygon* major = (Polygon*)allocate_clear(sizeof(Polygon));
    *major = regular_polygon(Vec2{0, 0}, 0.5, 6, 0, make_tag(1, 0));
    Polygon minor = rectangle(Vec2{-0.1, -0.5}, Vec2{0.1, 0.5}, make_tag(1, 0));

    // Reserve space for all markers in the main cell
    int64_t count = path->subpath_array.count;
    main_cell->polygon_array.ensure_slots(1 + 4 * count);

    for (int64_t i = 0; i < count; i++) {
        Polygon* poly = (Polygon*)allocate_clear(sizeof(Polygon));
        poly->copy_from(*major);
        poly->translate(path->position(i, true));
        main_cell->polygon_array.append(poly);
        for (int64_t j = 1; j < 4; j++) {
            poly = (Polygon*)allocate_clear(sizeof(Polygon));
            poly->copy_from(minor);
            double u = i + j / 4.0;
            poly->rotate(path->gradient(u, true).angle(), Vec2{0, 0});
            poly->translate(path->position(u, true));
            main_cell->polygon_array.append(poly);
        }
    }

    // Last marker: we use the original major marker
    major->translate(path->end_point);
    main_cell->polygon_array.append(major);
    minor.clear();

    lib.write_gds("path_markers.gds", 0, NULL);

    lib.free_all();
    return 0;
}
_images/path_markers.svg

Connection Pads

In this example, a custom end function is used to provide connection pads for electrical traces. For simplicity, it assumes that the path width does not change in the first and last segments, which also must be long enough to support the pad shapes, and that the pad diameter is larger than the path width. The point where the pad connects to the trace can, optionally, be filleted.

import numpy
import gdstk


if __name__ == "__main__":

    def filleted_pad(pad_radius, fillet_radius=0, tolerance=0.01):
        def _f(p0, v0, p1, v1):
            p0 = numpy.array(p0)
            v0 = numpy.array(v0)
            p1 = numpy.array(p1)
            v1 = numpy.array(v1)

            half_trace_width = 0.5 * numpy.sqrt(numpy.sum((p0 - p1) ** 2))
            a = half_trace_width + fillet_radius
            c = pad_radius + fillet_radius
            b = (c**2 - a**2) ** 0.5
            alpha = numpy.arccos(a / c)
            gamma = numpy.arctan2(v0[1], v0[0]) + 0.5 * numpy.pi

            curve = gdstk.Curve(p0 - v0 * b, tolerance=tolerance)
            if fillet_radius > 0:
                curve.arc(fillet_radius, gamma, gamma - alpha)
            curve.arc(pad_radius, gamma - numpy.pi - alpha, gamma + alpha)
            if fillet_radius > 0:
                curve.arc(fillet_radius, gamma - numpy.pi + alpha, gamma - numpy.pi)

            return curve.points()

        return _f

    main = gdstk.Cell("Main")

    # Create a bus with 4 traces
    bus = gdstk.FlexPath(
        [(0, 0), (10, 5)], [3] * 4, offset=15, joins="round", ends=filleted_pad(5, 3)
    )
    bus.segment((20, 10), offset=6)
    bus.segment([(40, 20), (40, 50), (80, 50)])
    bus.segment((100, 50), offset=12)
    main.add(bus)
#include <math.h>
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

struct FilletedPadData {
    double pad_radius;
    double fillet_radius;
    double tolerance;
};

Array<Vec2> filleted_pad(const Vec2 p0, const Vec2 v0, const Vec2 p1, const Vec2 v1, void* data) {
    FilletedPadData* pad_data = (FilletedPadData*)data;
    double pad_radius = pad_data->pad_radius;
    double fillet_radius = pad_data->fillet_radius;

    Vec2 dp = p0 - p1;
    double half_trace_width = 0.5 * sqrt(dp.x * dp.x + dp.y * dp.y);
    double a = half_trace_width + fillet_radius;
    double c = pad_radius + fillet_radius;
    double b = sqrt(c * c + a * a);
    double alpha = acos(a / c);
    double gamma = atan2(v0.y, v0.x) + 0.5 * M_PI;

    Curve curve = {};
    curve.init(p0 - b * v0, pad_data->tolerance);
    if (fillet_radius > 0) {
        curve.arc(fillet_radius, fillet_radius, gamma, gamma - alpha, 0);
    }
    curve.arc(pad_radius, pad_radius, gamma - M_PI - alpha, gamma + alpha, 0);
    if (fillet_radius > 0) {
        curve.arc(fillet_radius, fillet_radius, gamma - M_PI + alpha, gamma - M_PI, 0);
    }

    return curve.point_array;
}

int main(int argc, char* argv[]) {
    Library lib = {};
    lib.init("library", 1e-6, 1e-9);

    Cell* main_cell = (Cell*)allocate_clear(sizeof(Cell));
    main_cell->name = copy_string("Main", NULL);
    lib.cell_array.append(main_cell);

    FlexPath* bus = (FlexPath*)allocate_clear(sizeof(FlexPath));
    bus->init(Vec2{0, 0}, 4, 3, 15, 0.01, 0);
    FilletedPadData data = {5, 3, 0.01};
    for (int64_t i = 0; i < 4; i++) {
        bus->elements[i].join_type = JoinType::Round;
        bus->elements[i].end_type = EndType::Function;
        bus->elements[i].end_function = filleted_pad;
        bus->elements[i].end_function_data = (void*)&data;
    }
    bus->segment(Vec2{10, 5}, NULL, NULL, false);
    double offsets1[] = {-9, -3, 3, 9};
    bus->segment(Vec2{20, 10}, NULL, offsets1, false);
    Vec2 points[] = {{40, 20}, {40, 50}, {80, 50}};
    const Array<Vec2> point_array = {.capacity = 0, .count = COUNT(points), .items = points};
    bus->segment(point_array, NULL, NULL, false);
    double offsets2[] = {-18, -6, 6, 18};
    bus->segment(Vec2{100, 50}, NULL, offsets2, false);

    main_cell->flexpath_array.append(bus);

    lib.write_gds("pads.gds", 0, NULL);

    lib.free_all();
    return 0;
}
_images/pads.svg

System Fonts

This example uses matplotlib to render text using any typeface present in the system. The glyph paths are then transformed into polygon arrays that can be used to create gdstk.Polygon objects.

import gdstk
from matplotlib.font_manager import FontProperties
from matplotlib.textpath import TextPath


def render_text(text, size=None, position=(0, 0), font_prop=None, tolerance=0.1):
    precision = 0.1 * tolerance
    path = TextPath(position, text, size=size, prop=font_prop)
    polys = []
    xmax = position[0]
    for points, code in path.iter_segments():
        if code == path.MOVETO:
            c = gdstk.Curve(points, tolerance=tolerance)
        elif code == path.LINETO:
            c.segment(points.reshape(points.size // 2, 2))
        elif code == path.CURVE3:
            c.quadratic(points.reshape(points.size // 2, 2))
        elif code == path.CURVE4:
            c.cubic(points.reshape(points.size // 2, 2))
        elif code == path.CLOSEPOLY:
            pts = c.points()
            if pts.size > 0:
                poly = gdstk.Polygon(pts)
                if pts[:, 0].min() < xmax:
                    i = len(polys) - 1
                    while i >= 0:
                        if polys[i].contain_any(*poly.points):
                            p = polys.pop(i)
                            poly = gdstk.boolean(p, poly, "xor", precision)[0]
                            break
                        elif poly.contain_any(*polys[i].points):
                            p = polys.pop(i)
                            poly = gdstk.boolean(p, poly, "xor", precision)[0]
                        i -= 1
                xmax = max(xmax, poly.points[:, 0].max())
                polys.append(poly)
    return polys


if __name__ == "__main__":
    cell = gdstk.Cell("fonts")
    fp = FontProperties(family="serif", style="italic")
    polygons = render_text("Text rendering", 10, font_prop=fp)
    cell.add(*polygons)
_images/fonts.svg