Getting Started

GDSII files contain a hierarchical representation of any polygonal geometry. They are mainly used in the microelectronics industry for the design of mask layouts, but are also employed in other areas.

Because it is a hierarchical format, repeated structures, such as identical transistors, can be defined once and referenced multiple times in the layout, reducing the file size.

There is one important limitation in the GDSII format: it only supports weakly simple polygons, that is, polygons whose segments are allowed to intersect, but not cross.

In particular, curves and shapes with holes are not directly supported. Holes can be defined, nonetheless, by connecting their boundary to the boundary of the enclosing shape. In the case of curves, they must be approximated by a polygon. The number of points in the polygonal approximation can be increased to better approximate the original curve up to some acceptable error.

The original GDSII format limits the number of vertices in a polygon to 199. This limit seems arbitrary, as the maximal number of vertices that can be stored in a GDSII record is 8190. Nonetheless, most modern software disregard both limits and allow an arbitrary number of points per polygon. Gdstk follows the modern version of GDSII, but this is an important issue to keep in mind if the generated file is to be used in older systems.

The units used to represent shapes in the GDSII format are defined by the user. The default unit in Gdstk is 1 µm (10⁻⁶ m), but that can be easily changed by the user.

First Layout

Let’s create our first layout file:

import gdstk

# The GDSII file is called a library, which contains multiple cells.
lib = gdstk.Library()

# Geometry must be placed in cells.
cell = lib.new_cell("FIRST")

# Create the geometry (a single rectangle) and add it to the cell.
rect = gdstk.rectangle((0, 0), (2, 1))
cell.add(rect)

# Save the library in a GDSII or OASIS file.
lib.write_gds("first.gds")
lib.write_oas("first.oas")

# Optionally, save an image of the cell as SVG.
cell.write_svg("first.svg")
#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 cell = {};
    cell.name = copy_string("FIRST", NULL);
    lib.cell_array.append(&cell);

    Polygon rect = rectangle(Vec2{0, 0}, Vec2{2, 1}, make_tag(0, 0));
    cell.polygon_array.append(&rect);

    lib.write_gds("first.gds", 0, NULL);
    lib.write_oas("first.oas", 0, 6, OASIS_CONFIG_DETECT_ALL);
    cell.write_svg("first.svg", 10, 6, NULL, NULL, "#222222", 5, true, NULL);

    rect.clear();
    cell.clear();
    lib.clear();
    return 0;
}

After importing the gdstk module, we create a library lib to hold the design.

All layout elements must be added to cells, which can be though of pieces of papers where the geometry is drawn. Later, a cell can reference others to build hierarchical designs, as if stamping the referenced cell into the first, as we’ll see in References.

We create a gdstk.Cell with name “FIRST” (cells are identified by name, therefore cell names must be unique within a library) and add a gdstk.rectangle() to it.

Finally, the whole library is saved in a GDSII file called “first.gds” and an OASIS file “first.oas” in the current directory. The GDSII or OASIS files can be opened in a number of viewers and editors, such as KLayout.

Polygons

General polygons can be defined by an ordered list of vertices. The orientation of the vertices (clockwise/counter-clockwise) is not important: they will be ordered internally.

# Create a polygon from a list of vertices
points = [(0, 0), (2, 2), (2, 6), (-6, 6), (-6, -6), (-4, -4), (-4, 4), (0, 4)]
poly = gdstk.Polygon(points)
void example_polygons(Cell& out_cell) {
    Vec2 points[] = {{0, 0}, {2, 2}, {2, 6}, {-6, 6}, {-6, -6}, {-4, -4}, {-4, 4}, {0, 4}};

    // This has to be heap-allocated so that it doesn't go out of scope once
    // the function returns.  We also don't worry about leaking it at the end
    // of the program.  The OS will take care of it.
    Polygon* poly = (Polygon*)allocate_clear(sizeof(Polygon));
    poly->point_array.extend({.capacity = 0, .count = COUNT(points), .items = points});
    out_cell.polygon_array.append(poly);
}
_images/polygons.svg

Holes

As mentioned in Getting Started, holes have to be connected to the outer boundary of the polygon, as in the following example:

# Manually connect the hole to the outer boundary
cutout = gdstk.Polygon(
    [(0, 0), (5, 0), (5, 5), (0, 5), (0, 0), (2, 2), (2, 3), (3, 3), (3, 2), (2, 2)]
)
void example_holes(Cell& out_cell) {
    Vec2 points[] = {{0, 0}, {5, 0}, {5, 5}, {0, 5}, {0, 0},
                     {2, 2}, {2, 3}, {3, 3}, {3, 2}, {2, 2}};
    Polygon* poly = (Polygon*)allocate_clear(sizeof(Polygon));
    poly->point_array.extend({.capacity = 0, .count = COUNT(points), .items = points});
    out_cell.polygon_array.append(poly);
}
_images/holes.svg

Circles

The gdstk.ellipse() function creates circles, ellipses, doughnuts, arcs and slices. In all cases, the argument tolerance will control the number of vertices used to approximate the curved shapes.

When saving a library with gdstk.Library.write_gds(), if the number of vertices in the polygon is larger than max_points (199 by default), it will be fractured in many smaller polygons with at most max_points vertices each.

OASIS files don’t have a limit to the number of polygon vertices, so no polygons are fractured when saving. They also have support for circles. When saving an OASIS file, polygonal circles will be detected within a predefined tolerance and automatically converted.

# Circle centered at (0, 0), with radius 2 and tolerance 0.1
circle = gdstk.ellipse((0, 0), 2, tolerance=0.01)

# To create an ellipse, simply pass a list with 2 radii.
# Because the tolerance is small (resulting a large number of
# vertices), the ellipse is fractured in 2 polygons.
ellipse = gdstk.ellipse((4, 0), [1, 2], tolerance=1e-4)

# Circular arc example
arc = gdstk.ellipse(
    (2, 4),
    2,
    inner_radius=1,
    initial_angle=-0.2 * numpy.pi,
    final_angle=1.2 * numpy.pi,
    tolerance=0.01,
)
void example_circles(Cell& out_cell) {
    Polygon* circle = (Polygon*)allocate_clear(sizeof(Polygon));
    *circle = ellipse(Vec2{0, 0}, 2, 2, 0, 0, 0, 0, 0.01, 0);
    out_cell.polygon_array.append(circle);

    Polygon* ellipse_ = (Polygon*)allocate_clear(sizeof(Polygon));
    *ellipse_ = ellipse(Vec2{4, 0}, 1, 2, 0, 0, 0, 0, 1e-4, 0);
    out_cell.polygon_array.append(ellipse_);

    Polygon* arc = (Polygon*)allocate_clear(sizeof(Polygon));
    *arc = ellipse(Vec2{2, 4}, 2, 2, 1, 1, -0.2 * M_PI, 1.2 * M_PI, 0.01, 0);
    out_cell.polygon_array.append(arc);
}
_images/circles.svg

Curves

Constructing complex polygons by manually listing all vertices in gdstk.Polygon can be challenging. The class gdstk.Curve can be used to facilitate the creation of polygons by drawing their shapes step-by-step. The syntax is inspired by the SVG path specification.

# Construct a curve made of a sequence of line segments
c1 = gdstk.Curve((0, 0)).segment([(1, 0), (2, 1), (2, 2), (0, 2)])
p1 = gdstk.Polygon(c1.points())

# Construct another curve using relative coordinates
c2 = gdstk.Curve((3, 1)).segment([(1, 0), (2, 1), (2, 2), (0, 2)], relative=True)
p2 = gdstk.Polygon(c2.points())
void example_curves1(Cell& out_cell) {
    Vec2 points[] = {{1, 0}, {2, 1}, {2, 2}, {0, 2}};

    // Curve points will be copied to the polygons, so allocating the curve on
    // the stack is fine.
    Curve c1 = {};
    c1.init(Vec2{0, 0}, 0.01);
    c1.segment({.capacity = 0, .count = COUNT(points), .items = points}, false);

    Polygon* p1 = (Polygon*)allocate_clear(sizeof(Polygon));
    p1->point_array.extend(c1.point_array);
    out_cell.polygon_array.append(p1);
    c1.clear();

    Curve c2 = {};
    c2.init(Vec2{3, 1}, 0.01);
    c2.segment({.capacity = 0, .count = COUNT(points), .items = points}, true);

    Polygon* p2 = (Polygon*)allocate_clear(sizeof(Polygon));
    p2->point_array.extend(c2.point_array);
    out_cell.polygon_array.append(p2);
    c2.clear();
}
_images/curves.svg

Coordinate pairs can be given as a complex number: real and imaginary parts are used as x and y coordinates, respectively. That is useful to define points in polar coordinates.

Elliptical arcs have syntax similar to gdstk.ellipse(), but they allow for an extra rotation of the major axis of the ellipse.

# Use complex numbers to facilitate writing polar coordinates
c3 = gdstk.Curve(2j).segment(4 * numpy.exp(1j * numpy.pi / 6), relative=True)
# Elliptical arcs have syntax similar to gdstk.ellipse
c3.arc((4, 2), 0.5 * numpy.pi, -0.5 * numpy.pi)
p3 = gdstk.Polygon(c3.points())
void example_curves2(Cell& out_cell) {
    Curve c3 = {};
    c3.init(Vec2{0, 2}, 0.01);
    c3.segment(4 * cplx_from_angle(M_PI / 6), true);
    c3.arc(4, 2, M_PI / 2, -M_PI / 2, 0);

    Polygon* p3 = (Polygon*)allocate_clear(sizeof(Polygon));
    p3->point_array.extend(c3.point_array);
    out_cell.polygon_array.append(p3);
    c3.clear();
}
_images/curves_1.svg

Curves sections can be constructed as cubic, quadratic and general-degree Bézier curves. Additionally, a smooth interpolating curve can be calculated with the method gdstk.Curve.interpolation(), which has a number of arguments to control the shape of the curve.

# Cubic Bezier curves can be easily created
c4 = gdstk.Curve((0, 0), tolerance=1e-3)
c4.cubic([(0, 1), (1, 1), (1, 0)])
# Smooth continuation:
c4.cubic_smooth([(1, -1), (1, 0)], relative=True)

# Similarly for quadratic Bezier curves
c4.quadratic([(0.5, 1), (1, 0)], relative=True)
c4.quadratic_smooth((1, 0), relative=True)

# Smooth interpolating curve
c4.interpolation([(4, -1), (3, -2), (2, -1.5), (1, -2), (0, -1), (0, 0)])
p4 = gdstk.Polygon(c4.points())
void example_curves3(Cell& out_cell) {
    Curve c4 = {};
    c4.init(Vec2{0, 0}, 1e-3);

    Vec2 points1[] = {{0, 1}, {1, 1}, {1, 0}};
    c4.cubic({.capacity = 0, .count = COUNT(points1), .items = points1}, false);

    Vec2 points2[] = {{1, -1}, {1, 0}};
    c4.cubic_smooth({.capacity = 0, .count = COUNT(points2), .items = points2}, true);

    Vec2 points3[] = {{0.5, 1}, {1, 0}};
    c4.quadratic({.capacity = 0, .count = COUNT(points3), .items = points3}, true);

    c4.quadratic_smooth(Vec2{1, 0}, true);

    Vec2 points4[] = {{4, -1}, {3, -2}, {2, -1.5}, {1, -2}, {0, -1}, {0, 0}};
    double angles[COUNT(points4) + 1] = {};
    bool angle_constraints[COUNT(points4) + 1] = {};
    Vec2 tension[COUNT(points4) + 1] = {{1, 1}, {1, 1}, {1, 1}, {1, 1}, {1, 1}, {1, 1}, {1, 1}};

    c4.interpolation({.capacity = 0, .count = COUNT(points4), .items = points4}, angles,
                     angle_constraints, tension, 1, 1, false, false);

    // The last point will coincide with the first (this can be checked at
    // runtime with the `closed` method), so we remove it.
    if (c4.closed()) {
        c4.remove(c4.point_array.count - 1);
    }
    Polygon* p4 = (Polygon*)allocate_clear(sizeof(Polygon));
    p4->point_array.extend(c4.point_array);
    out_cell.polygon_array.append(p4);
    c4.clear();
}
_images/curves_2.svg

Transformations

All polygons can be transformed through gdstk.Polygon.translate(), gdstk.Polygon.rotate(), gdstk.Polygon.scale(), and gdstk.Polygon.mirror(). The transformations are applied in-place, i.e., no new polygons are created.

poly = gdstk.rectangle((-2, -2), (2, 2))
poly.rotate(numpy.pi / 4)
poly.scale(1, 0.5)
void example_transformations(Cell& out_cell) {
    Polygon* poly = (Polygon*)allocate_clear(sizeof(Polygon));
    *poly = rectangle(Vec2{-2, -2}, Vec2{2, 2}, 0);
    poly->rotate(M_PI / 4, Vec2{0, 0});
    poly->scale(Vec2{1, 0.5}, Vec2{0, 0});
    out_cell.polygon_array.append(poly);
}
_images/transformations.svg

Layer and Datatype

All shapes are tagged with 2 properties: layer and data type (or text type in the case of gdstk.Label). They are always 0 by default, but can be any integer in the range from 0 to 255.

These properties have no predefined meaning. It is up to the system using the file to chose with to do with those tags. For example, in the CMOS fabrication process, each layer could represent a different lithography level.

In the example below, a single file stores different fabrication masks in separate layer and data type configurations. Python dictionaries are used to simplify the assignment to each polygon.

# Layer/datatype definitions for each step in the fabrication
ld = {
    "full etch": {"layer": 1, "datatype": 3},
    "partial etch": {"layer": 2, "datatype": 3},
    "lift-off": {"layer": 0, "datatype": 7},
}

p1 = gdstk.rectangle((-3, -3), (3, 3), **ld["full etch"])
p2 = gdstk.rectangle((-5, -3), (-3, 3), **ld["partial etch"])
p3 = gdstk.rectangle((5, -3), (3, 3), **ld["partial etch"])
p4 = gdstk.regular_polygon((0, 0), 2, 6, **ld["lift-off"])
void example_layerdatatype(Cell& out_cell) {
    Tag t_full_etch = make_tag(1, 3);
    Tag t_partial_etch = make_tag(2, 3);
    Tag t_lift_off = make_tag(0, 7);

    Polygon* poly[4];
    for (uint64_t i = 0; i < COUNT(poly); i++) poly[i] = (Polygon*)allocate_clear(sizeof(Polygon));

    *poly[0] = rectangle(Vec2{-3, -3}, Vec2{3, 3}, t_full_etch);
    *poly[1] = rectangle(Vec2{-5, -3}, Vec2{-3, 3}, t_partial_etch);
    *poly[2] = rectangle(Vec2{5, -3}, Vec2{3, 3}, t_partial_etch);
    *poly[3] = regular_polygon(Vec2{0, 0}, 2, 6, 0, t_lift_off);

    out_cell.polygon_array.extend({.capacity = 0, .count = COUNT(poly), .items = poly});
}
_images/layer_and_datatype.svg

References

References are responsible for the hierarchical structure of the layout. Through references, the cell content can be reused in another cell (without actually copying the whole geometry). As an example, imagine the we are designing an electronic circuit that uses hundreds of transistors, all with the same shape. We can draw the transistor just once and reference it throughout the circuit, rotating or mirroring each instance as necessary.

Besides creating single references, it is also possible to create full 2D arrays with a single entity, both using gdstk.Reference. Both uses are exemplified below.

# Create a cell with a component that is used repeatedly
contact = gdstk.Cell("CONTACT")
contact.add(p1, p2, p3, p4)

# Create a cell with the complete device
device = gdstk.Cell("DEVICE")
device.add(cutout)
# Add 2 references to the component changing size and orientation
ref1 = gdstk.Reference(contact, (3.5, 1), magnification=0.25)
ref2 = gdstk.Reference(contact, (1, 3.5), magnification=0.25, rotation=numpy.pi / 2)
device.add(ref1, ref2)

# The final layout has several repetitions of the complete device
main = gdstk.Cell("MAIN")
main.add(gdstk.Reference(device, (0, 0), columns=3, rows=2, spacing=(6, 7)))
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

int main(int argc, char* argv[]) {
    Tag t_full_etch = make_tag(1, 3);
    Tag t_partial_etch = make_tag(2, 3);
    Tag t_lift_off = make_tag(0, 7);

    char lib_name[] = "library";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};

    // CONTACT

    char contact_cell_name[] = "CONTACT";
    Cell contact_cell = {.name = contact_cell_name};
    lib.cell_array.append(&contact_cell);

    Polygon contact_poly[4];
    contact_poly[0] = rectangle(Vec2{-3, -3}, Vec2{3, 3}, t_full_etch);
    contact_poly[1] = rectangle(Vec2{-5, -3}, Vec2{-3, 3}, t_partial_etch);
    contact_poly[2] = rectangle(Vec2{5, -3}, Vec2{3, 3}, t_partial_etch);
    contact_poly[3] = regular_polygon(Vec2{0, 0}, 2, 6, 0, t_lift_off);

    Polygon* p[] = {contact_poly, contact_poly + 1, contact_poly + 2, contact_poly + 3};
    contact_cell.polygon_array.extend({.capacity = 0, .count = COUNT(p), .items = p});

    // DEVICE

    char device_cell_name[] = "DEVICE";
    Cell device_cell = {.name = device_cell_name};
    lib.cell_array.append(&device_cell);

    Vec2 cutout_points[] = {{0, 0}, {5, 0}, {5, 5}, {0, 5}, {0, 0},
                            {2, 2}, {2, 3}, {3, 3}, {3, 2}, {2, 2}};
    Polygon cutout_poly = {};
    cutout_poly.point_array.extend(
        {.capacity = 0, .count = COUNT(cutout_points), .items = cutout_points});
    device_cell.polygon_array.append(&cutout_poly);

    Reference contact_ref1 = {
        .type = ReferenceType::Cell,
        .cell = &contact_cell,
        .origin = Vec2{3.5, 1},
        .magnification = 0.25,
    };
    device_cell.reference_array.append(&contact_ref1);

    Reference contact_ref2 = {
        .type = ReferenceType::Cell,
        .cell = &contact_cell,
        .origin = Vec2{1, 3.5},
        .rotation = M_PI / 2,
        .magnification = 0.25,
    };
    device_cell.reference_array.append(&contact_ref2);

    // MAIN

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

    Reference device_ref = {
        .type = ReferenceType::Cell,
        .cell = &device_cell,
        .magnification = 1,
        .repetition = {RepetitionType::Rectangular, 3, 2, Vec2{6, 7}},
    };
    main_cell.reference_array.append(&device_ref);

    // Output

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

    for (uint64_t i = 0; i < COUNT(contact_poly); i++) contact_poly[i].clear();
    cutout_poly.clear();
    contact_cell.polygon_array.clear();
    device_cell.polygon_array.clear();
    device_cell.reference_array.clear();
    main_cell.reference_array.clear();
    lib.cell_array.clear();
    return 0;
}
_images/references.svg

Paths

Besides polygons, the GDSII and OASIS formats define paths, witch are polygonal chains with associated width and end caps. The width is a single number, constant throughout the path, and the end caps can be flush, round (GDSII only), or extended by a custom distance.

There is no specification for the joins between adjacent segments, so it is up to the system using the file to specify those. Usually the joins are straight extensions of the path boundaries up to some beveling limit. Gdstk also uses this specification for the joins.

It is possible to circumvent all of the above limitations within Gdstk by storing paths as polygons in the GDSII or OASIS file. The disadvantage of this solution is that other software will not be able to edit the geometry as paths, since that information is lost.

The construction of paths (either GDSII/OASIS paths or polygonal paths) in Gdstk is based on gdstk.FlexPath and gdstk.RobustPath.

Flexible Paths

The class gdstk.FlexPath is a mirror of gdstk.Curve before, with additional features to facilitate path creation:

  • all curve construction methods are available;

  • path width can be easily controlled throughout the path;

  • end caps and joins can be specified by the user;

  • straight segments can be automatically joined by circular arcs;

  • multiple parallel paths can be designed simultaneously;

  • spacing between parallel paths is arbitrary - the user specifies the offset of each path individually.

# Path defined by a sequence of points and stored as a GDSII path
fp1 = gdstk.FlexPath(
    [(0, 0), (3, 0), (3, 2), (5, 3), (3, 4), (0, 4)], 1, simple_path=True
)

# Other construction methods can still be used
fp1.interpolation([(0, 2), (2, 2), (4, 3), (5, 1)], relative=True)

# Multiple parallel paths separated by 0.5 with different widths,
# end caps, and joins.  Because of the join specification, they
# cannot be stared as GDSII paths, only as polygons.
fp2 = gdstk.FlexPath(
    [(12, 0), (8, 0), (8, 3), (10, 2)],
    [0.3, 0.2, 0.4],
    0.5,
    ends=["extended", "flush", "round"],
    joins=["bevel", "miter", "round"],
)
fp2.arc(2, -0.5 * numpy.pi, 0.5 * numpy.pi)
fp2.arc(1, 0.5 * numpy.pi, 1.5 * numpy.pi)
Cell* example_flexpath1(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    FlexPath* fp = (FlexPath*)allocate_clear(sizeof(FlexPath));
    fp->init(Vec2{0, 0}, 1, 0.5, 0, 0.01, 0);
    fp->simple_path = true;

    Vec2 points1[] = {{3, 0}, {3, 2}, {5, 3}, {3, 4}, {0, 4}};
    fp->segment({.capacity = 0, .count = COUNT(points1), .items = points1}, NULL, NULL, false);

    out_cell->flexpath_array.append(fp);

    fp = (FlexPath*)allocate_clear(sizeof(FlexPath));
    const double widths[] = {0.3, 0.2, 0.4};
    const double offsets[] = {-0.5, 0, 0.5};
    const Tag tags[] = {0, 0, 0};
    fp->init(Vec2{12, 0}, 3, widths, offsets, 0.01, tags);

    fp->elements[0].end_type = EndType::HalfWidth;
    fp->elements[0].join_type = JoinType::Bevel;

    fp->elements[1].end_type = EndType::Flush;
    fp->elements[1].join_type = JoinType::Miter;

    fp->elements[2].end_type = EndType::Round;
    fp->elements[2].join_type = JoinType::Round;

    Vec2 points2[] = {{8, 0}, {8, 3}, {10, 2}};
    fp->segment({.capacity = 0, .count = COUNT(points2), .items = points2}, NULL, NULL, false);

    fp->arc(2, 2, -M_PI / 2, M_PI / 2, 0, NULL, NULL);
    fp->arc(1, 1, M_PI / 2, 1.5 * M_PI, 0, NULL, NULL);

    out_cell->flexpath_array.append(fp);
    return out_cell;
}
_images/flexible_paths.svg

The corner type “circular bend” (together with the bend_radius argument) can be used to automatically curve the path.

# Path created with automatic bends of radius 5
points = [(0, 0), (0, 10), (20, 0), (18, 15), (8, 15)]
fp3 = gdstk.FlexPath(points, 0.5, bend_radius=5, simple_path=True)

# Same path, generated with natural joins, for comparison
fp4 = gdstk.FlexPath(points, 0.5, layer=1, simple_path=True)
Cell* example_flexpath2(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    Vec2 points[] = {{0, 10}, {20, 0}, {18, 15}, {8, 15}};

    for (uint64_t i = 0; i < 2; i++) {
        FlexPath* fp = (FlexPath*)allocate_clear(sizeof(FlexPath));
        fp->init(Vec2{0, 0}, 1, 0.5, 0, 0.01, 0);
        fp->simple_path = true;
        if (i == 0) {
            fp->elements[0].bend_type = BendType::Circular;
            fp->elements[0].bend_radius = 5;
        } else {
            fp->elements[0].tag = make_tag(1, 0);
        }
        fp->segment({.capacity = 0, .count = COUNT(points), .items = points}, NULL, NULL, false);
        out_cell->flexpath_array.append(fp);
    }

    return out_cell;
}
_images/flexible_paths_2.svg

Width and offset variations are possible throughout the path. Changes are linearly tapered in the path section they are defined. Note that, because width changes are not possible for GDSII/OASIS paths, they will be stored as polygonal objects.

# Straight segment showing the possibility of width and offset changes
fp5 = gdstk.FlexPath((0, 0), [0.5, 0.5], 1)
fp5.horizontal(2)
fp5.horizontal(4, width=0.8, offset=1.8)
fp5.horizontal(6)
Cell* example_flexpath3(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    double widths[] = {0.5, 0.5};
    double offsets[] = {-0.5, 0.5};
    Tag tags[] = {0, 0};
    FlexPath* fp = (FlexPath*)allocate_clear(sizeof(FlexPath));
    fp->init(Vec2{0, 0}, 2, widths, offsets, 0.01, tags);

    fp->horizontal(2, NULL, NULL, false);

    widths[0] = 0.8;
    widths[1] = 0.8;
    offsets[0] = -0.9;
    offsets[1] = 0.9;
    fp->horizontal(4, widths, offsets, false);

    fp->horizontal(6, NULL, NULL, false);

    out_cell->flexpath_array.append(fp);

    return out_cell;
}
_images/flexible_paths_3.svg

Robust Paths

In some situations, gdstk.FlexPath is unable to properly calculate all the joins. This often happens when the width or offset of the path is relatively large with respect to the length of the segments being joined. Curves that meet other curves or segments at sharp angles are a typical example where this often happens.

The class gdstk.RobustPath can be used in such scenarios where curved sections are expected to meet at sharp angles. The drawbacks of using gdstk.RobustPath are the extra computational resources required to calculate all joins and the impossibility of specifying joins. The advantages are, as mentioned earlier, more robustness when generating the final geometry, and freedom to use custom functions to parameterize the widths or offsets of the paths.

# Create 4 parallel paths in different layers
rp = gdstk.RobustPath(
    (0, 50),
    [2, 0.5, 1, 1],
    [0, 0, -1, 1],
    ends=["extended", "round", "flush", "flush"],
    layer=[1, 0, 2, 2],
)
rp.segment((0, 45))
rp.segment(
    (0, 5),
    width=[lambda u: 2 + 16 * u * (1 - u), 0.5, 1, 1],
    offset=[
        0,
        lambda u: 8 * u * (1 - u) * numpy.cos(12 * numpy.pi * u),
        lambda u: -1 - 8 * u * (1 - u),
        lambda u: 1 + 8 * u * (1 - u),
    ],
)
rp.segment((0, 0))
rp.interpolation(
    [(15, 5)],
    angles=[0, 0.5 * numpy.pi],
    width=0.5,
    offset=[-0.25, 0.25, -0.75, 0.75],
)
rp.parametric(
    lambda u: numpy.array((4 * numpy.sin(6 * numpy.pi * u), 45 * u)),
    offset=[
        lambda u: -0.25 * numpy.cos(24 * numpy.pi * u),
        lambda u: 0.25 * numpy.cos(24 * numpy.pi * u),
        -0.75,
        0.75,
    ],
)
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

double parametric1(double u, void*) { return 2 + 16 * u * (1 - u); }

double parametric2(double u, void*) { return 8 * u * (1 - u) * cos(12 * M_PI * u); }

double parametric3(double u, void*) { return -1 - 8 * u * (1 - u); }

double parametric4(double u, void*) { return 1 + 8 * u * (1 - u); }

double parametric5(double u, void*) { return -0.25 * cos(24 * M_PI * u); }

double parametric6(double u, void*) { return 0.25 * cos(24 * M_PI * u); }

Vec2 parametric7(double u, void*) { return Vec2{4 * sin(6 * M_PI * u), 45 * u}; }

int main(int argc, char* argv[]) {
    double widths[] = {2, 0.5, 1, 1};
    double offsets[] = {0, 0, -1, 1};
    Tag tags[] = {make_tag(1, 0), make_tag(0, 0), make_tag(2, 0), make_tag(2, 0)};
    RobustPath rp = {};
    rp.init(Vec2{0, 50}, 4, widths, offsets, 0.01, 1000, tags);
    rp.scale_width = true;
    rp.elements[0].end_type = EndType::HalfWidth;
    rp.elements[1].end_type = EndType::Round;
    rp.elements[2].end_type = EndType::Flush;
    rp.elements[3].end_type = EndType::Flush;

    rp.segment(Vec2{0, 45}, NULL, NULL, false);

    Interpolation width1[] = {
        {.type = InterpolationType::Parametric},
        {.type = InterpolationType::Constant},
        {.type = InterpolationType::Constant},
        {.type = InterpolationType::Constant},
    };
    width1[0].function = parametric1;
    for (int i = 1; i < COUNT(width1); i++) {
        width1[i].value = rp.elements[i].end_width;
    }
    Interpolation offset1[] = {
        {.type = InterpolationType::Constant},
        {.type = InterpolationType::Parametric},
        {.type = InterpolationType::Parametric},
        {.type = InterpolationType::Parametric},
    };
    offset1[0].value = 0;
    offset1[1].function = parametric2;
    offset1[2].function = parametric3;
    offset1[3].function = parametric4;
    rp.segment(Vec2{0, 5}, width1, offset1, false);

    rp.segment(Vec2{0, 0}, NULL, NULL, false);

    Vec2 point = {15, 5};
    double angles[] = {0, M_PI / 2};
    bool angle_constraints[] = {true, true};
    Vec2 tension[] = {{1, 1}, {1, 1}};
    Interpolation width2[] = {
        {.type = InterpolationType::Linear},
        {.type = InterpolationType::Linear},
        {.type = InterpolationType::Linear},
        {.type = InterpolationType::Linear},
    };
    Interpolation offset2[] = {
        {.type = InterpolationType::Linear},
        {.type = InterpolationType::Linear},
        {.type = InterpolationType::Linear},
        {.type = InterpolationType::Linear},
    };
    for (int i = 0; i < COUNT(width2); i++) {
        width2[i].initial_value = rp.elements[i].end_width;
        width2[i].final_value = 0.5;
        offset2[i].initial_value = rp.elements[i].end_offset;
    }
    offset2[0].final_value = -0.25;
    offset2[1].final_value = 0.25;
    offset2[2].final_value = -0.75;
    offset2[3].final_value = 0.75;
    rp.interpolation({.capacity = 0, .count = 1, .items = &point}, angles, angle_constraints,
                     tension, 1, 1, false, width2, offset2, false);

    Interpolation offset3[] = {
        {.type = InterpolationType::Parametric},
        {.type = InterpolationType::Parametric},
        {.type = InterpolationType::Constant},
        {.type = InterpolationType::Constant},
    };
    offset3[0].function = parametric5;
    offset3[1].function = parametric6;
    offset3[2].value = -0.75;
    offset3[3].value = 0.75;
    rp.parametric(parametric7, NULL, NULL, NULL, NULL, offset3, true);

    char robustpath_cell_name[] = "RobustPath";
    Cell robustpath_cell = {.name = robustpath_cell_name};
    robustpath_cell.robustpath_array.append(&rp);

    char lib_name[] = "Paths";
    Library lib = {.name = lib_name, .unit = 1e-6, .precision = 1e-9};
    lib.cell_array.append(&robustpath_cell);

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

    rp.clear();
    robustpath_cell.robustpath_array.clear();
    lib.cell_array.clear();
    return 0;
}
_images/robust_paths.svg

Note that, analogously to gdstk.FlexPath, gdstk.RobustPath can be stored as a GDSII/OASIS path as long as its width is kept constant.

Text

In the context of a GDSII/OASIS file, text is supported in the form of labels, which are ASCII annotations placed somewhere in the geometry of a given cell. Similar to polygons, labels are tagged with layer and text type values (text type is the label equivalent of the polygon data type). They are supported by the class gdstk.Label.

Additionally, Gdstk offers the possibility of creating text as polygons to be included with the geometry. The function gdstk.text() creates polygonal text that can be used in the same way as any other polygons in Gdstk. The font used to render the characters contains only horizontal and vertical edges, which is important for some laser writing systems.

# Label centered at (1, 3)
label = gdstk.Label("Sample label", (5, 3), texttype=2)

# Horizontal text with height 2.25
htext = gdstk.text("12345", 2.25, (0.25, 6))

# Vertical text with height 1.5
vtext = gdstk.text("ABC", 1.5, (10.5, 4), vertical=True)

rect = gdstk.rectangle((0, 0), (10, 6), layer=10)
#include <stdio.h>

#include <gdstk/gdstk.hpp>

using namespace gdstk;

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

    char text_cell_name[] = "Text";
    Cell text_cell = {.name = text_cell_name};
    lib.cell_array.append(&text_cell);

    char label_text[] = "Sample label";
    Label label = {
        .tag = make_tag(0, 2),
        .text = label_text,
        .origin = Vec2{5, 3},
        .magnification = 1,
    };
    text_cell.label_array.append(&label);

    Array<Polygon*> all_text = {};
    text("12345", 2.25, Vec2{0.25, 6}, false, 0, all_text);
    text("ABC", 1.5, Vec2{10.5, 4}, true, 0, all_text);
    text_cell.polygon_array.extend(all_text);

    Polygon rect = rectangle(Vec2{0, 0}, Vec2{10, 6}, make_tag(10, 0));
    text_cell.polygon_array.append(&rect);

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

    for (uint64_t i = 0; i < all_text.count; i++) {
        all_text[i]->clear();
        free_allocation(all_text[i]);
    }
    all_text.clear();
    rect.clear();
    text_cell.label_array.clear();
    text_cell.polygon_array.clear();
    lib.cell_array.clear();
    return 0;
}
_images/text1.svg

Geometry Operations

Gdstk offers a number of functions and methods to modify existing geometry. The most useful operations include gdstk.boolean(), gdstk.slice(), gdstk.offset(), and gdstk.Polygon.fillet().

Boolean Operations

Boolean operations (gdstk.boolean()) can be performed on polygons, paths and whole cells. Four operations are defined: union (“or”), intersection (“and”), subtraction (“not”), and symmetric difference (“xor”). They can be computationally expensive, so it is usually advisable to avoid using boolean operations whenever possible. If they are necessary, keeping the number of vertices is all polygons as low as possible also helps.

# Create some text
text = gdstk.text("GDSTK", 4, (0, 0))
# Create a rectangle extending the text's bounding box by 1
rect = gdstk.rectangle((-1, -1), (5 * 4 * 9 / 16 + 1, 4 + 1))

# Subtract the text from the rectangle
inv = gdstk.boolean(rect, text, "not")
Cell* example_boolean(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    Array<Polygon*> txt = {};
    text("GDSTK", 4, Vec2{0, 0}, false, 0, txt);

    Polygon rect = rectangle(Vec2{-1, -1}, Vec2{5 * 4 * 9 / 16 + 1, 4 + 1}, 0);

    boolean(rect, txt, Operation::Not, 1000, out_cell->polygon_array);

    for (int i = 0; i < txt.count; i++) {
        txt[i]->clear();
        free_allocation(txt[i]);
    }
    txt.clear();
    rect.clear();

    return out_cell;
}
_images/boolean_operations.svg

Slice Operation

As the name indicates, a slice operation subdivides a set of polygons along horizontal or vertical cut lines.

ring1 = gdstk.ellipse((-6, 0), 6, inner_radius=4)
ring2 = gdstk.ellipse((0, 0), 6, inner_radius=4)
ring3 = gdstk.ellipse((6, 0), 6, inner_radius=4)

# Slice the first ring across x=-3, the second ring across x=-3
# and x=3, and the third ring across x=3
slices1 = gdstk.slice(ring1, -3, "x")
slices2 = gdstk.slice(ring2, [-3, 3], "x")
slices3 = gdstk.slice(ring3, 3, "x")

slices = gdstk.Cell("SLICES")

# Keep only the left side of slices1, the center part of slices2
# and the right side of slices3
slices.add(*slices1[0])
slices.add(*slices2[1])
slices.add(*slices3[1])
Cell* example_slice(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    Polygon ring[3];
    ring[0] = ellipse(Vec2{-6, 0}, 6, 6, 4, 4, 0, 0, 0.01, 0);
    ring[1] = ellipse(Vec2{0, 0}, 6, 6, 4, 4, 0, 0, 0.01, 0);
    ring[2] = ellipse(Vec2{6, 0}, 6, 6, 4, 4, 0, 0, 0.01, 0);

    double x[] = {-3, 3};
    Array<double> cuts = {.capacity = 0, .count = 1, .items = x};
    Array<Polygon*> result[3] = {};
    slice(ring[0], cuts, true, 1000, result);
    out_cell->polygon_array.extend(result[0]);
    for (uint64_t i = 0; i < result[1].count; i++) {
        result[1][i]->clear();
        free_allocation(result[1][i]);
    }
    result[0].clear();
    result[1].clear();

    cuts.count = 2;
    slice(ring[1], cuts, true, 1000, result);
    out_cell->polygon_array.extend(result[1]);
    for (uint64_t i = 0; i < result[0].count; i++) {
        result[0][i]->clear();
        free_allocation(result[0][i]);
    }
    for (uint64_t i = 0; i < result[2].count; i++) {
        result[2][i]->clear();
        free_allocation(result[2][i]);
    }
    result[0].clear();
    result[1].clear();
    result[2].clear();

    cuts.count = 1;
    cuts.items = x + 1;
    slice(ring[2], cuts, true, 1000, result);
    out_cell->polygon_array.extend(result[1]);
    for (uint64_t i = 0; i < result[0].count; i++) {
        result[0][i]->clear();
        free_allocation(result[0][i]);
    }
    result[0].clear();
    result[1].clear();

    ring[0].clear();
    ring[1].clear();
    ring[2].clear();

    return out_cell;
}
_images/slice_operation.svg

Offset Operation

The function gdstk.offset() dilates or erodes polygons by a fixed amount. It can operate on individual polygons or sets of them, in which case it may be necessary to set use_union = True to remove the impact of inner edges. The same is valid for polygons with holes.

rect1 = gdstk.rectangle((-4, -4), (1, 1))
rect2 = gdstk.rectangle((-1, -1), (4, 4))

# Erosion: because we set `use_union=True`, the inner boundaries have no effect
outer = gdstk.offset([rect1, rect2], -0.5, use_union=True, layer=1)
Cell* example_offset(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    Polygon* rect = (Polygon*)allocate(sizeof(Polygon));
    *rect = rectangle(Vec2{-4, -4}, Vec2{1, 1}, 0);
    out_cell->polygon_array.append(rect);

    rect = (Polygon*)allocate(sizeof(Polygon));
    *rect = rectangle(Vec2{-1, -1}, Vec2{4, 4}, 0);
    out_cell->polygon_array.append(rect);

    uint64_t start = out_cell->polygon_array.count;
    offset(out_cell->polygon_array, -0.5, OffsetJoin::Miter, 2, 1000, true,
           out_cell->polygon_array);
    for (uint64_t i = start; i < out_cell->polygon_array.count; i++) {
        out_cell->polygon_array[i]->tag = make_tag(1, 0);
    }

    return out_cell;
}
_images/offset_operation.svg

Fillet Operation

The method gdstk.Polygon.fillet() can be used to round polygon corners.

flexpath = gdstk.FlexPath([(-8, -4), (0, -4), (0, 4), (8, 4)], 4)
filleted_path = flexpath.to_polygons()[0]
filleted_path.fillet(1.5)
Cell* example_fillet(const char* name) {
    Cell* out_cell = (Cell*)allocate_clear(sizeof(Cell));
    out_cell->name = copy_string(name, NULL);

    FlexPath flexpath = {};
    flexpath.init(Vec2{-8, -4}, 1, 4, 0, 0.01, 0);
    Vec2 points[] = {{0, -4}, {0, 4}, {8, 4}};
    flexpath.segment({.capacity = 0, .count = COUNT(points), .items = points}, NULL, NULL, false);

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

    double r = 1.5;
    for (int i = 0; i < poly_array.count; i++)
        poly_array[i]->fillet({.capacity = 0, .count = 1, .items = &r}, 0.01);

    out_cell->polygon_array.extend(poly_array);
    poly_array.clear();

    return out_cell;
}
_images/fillet_operation.svg

GDSII/OASIS Library

All the information used to create a GDSII/OASIS file is kept within an instance of gdstk.Library. Besides all the geometric and hierarchical information, this class also holds a name and the units for all entities. The name can be any ASCII string — it is simply stored in the file and has no other purpose in Gdstk. The units require some attention because they can impact the resolution of the polygons in the library when written to a file.

A Note About Units

Two values are defined when creating a gdstk.Library: unit and precision. The value of unit defines the unit size—in meters—for all entities in the library. For example, if unit = 1e-6 (10⁻⁶ m, the default value), a vertex at (1, 2) should be interpreted as a vertex in real world position (1 × 10⁻⁶ m, 2 × 10⁻⁶ m). If unit changes to 0.001, then that same vertex would be located (in real world coordinates) at (0.001 m, 0.002 m), or (1 mm, 2 mm).

The value of precision has to do with the type used to store coordinates in the GDSII file: signed 4-byte integers. Because of that, a finer coordinate grid than 1 unit is usually desired to define coordinates. That grid is defined, in meters, by precision, which defaults to 1e-9 (10⁻⁹ m). When the GDSII file is written, all vertices are snapped to the grid defined by precision. For example, for the default values of unit and precision, a vertex at (1.0512, 0.0001) represents real world coordinates (1.0512 × 10⁻⁶ m, 0.0001 × 10⁻⁶ m), or (1051.2 × 10⁻⁹ m, 0.1 × 10⁻⁹ m), which will be rounded to integers: (1051 × 10⁻⁹ m, 0 × 10⁻⁹ m), or (1.051 × 10⁻⁶ m, 0 × 10⁻⁶ m). The actual coordinate values written in the GDSII file will be the integers (1051, 0). By reducing the value of precision from 10⁻⁹ m to 10⁻¹² m, for example, the coordinates will have 3 additional decimal places of precision, so the stored values would be (1051200, 100).

The downside of increasing the number of decimal places in the file is reducing the range of coordinates that can be stored (in real world units). That is because the range of coordinate values that can be written in the file are [-(2³²); 2³¹ - 1] = [-2,147,483,648; 2,147,483,647]. For the default precsision, this range is [-2.147483648 m; 2.147483647 m]. If precision is set to 10⁻¹² m, the same range is reduced by 1000 times: [-2.147483648 mm; 2.147483647 mm].

GDSII files keep a record of both unit and precision, which means that some care must be taken when mixing geometry from different files to ensure they have the same unit. For that reason, the use of the industry standard (unit = 1e-6) is recommended. OASIS files always use this standard for units, whereas precision can be freely chosen. For that reason, when saving or loading an OASIS file with Gdstk, the units can be automatic converted.

Saving a Layout File

To save a GDSII file, simply use the gdstk.Library.write_gds() method, as in the First Layout. An OASIS file can be similarly created with gdstk.Library.write_oas().

An SVG image from a specific cell can also be exported through gdstk.Cell.write_svg(), which was also demonstrated in First Layout.

Loading a Layout File

The functions gdstk.read_gds() and gdstk.read_oas() load an existing GDSII or OASIS file into a new instance of gdstk.Library.

# Load a GDSII file into a new library.
lib1 = gdstk.read_gds("filename.gds")
# Verify the unit used in the library.
print(lib1.unit)

# Load the same file, but convert all units to nm.
lib2 = gdstk.read_gds("filename.gds", 1e-9)

# Load an OASIS file into a third library.
# The library will use unit=1e-6, the default for OASIS.
lib3 = gdstk.read_oas("filename.oas")
// Use units from infile
Library lib1 = read_gds("filename.gds", 0);

// Convert to new unit
Library lib2 = read_gds("filename.gds", 1e-9);

// Use default OASIS unit (1e-6)
Library lib3 = read_oas("filename.oas");

Access to the cells in the loaded library is primarily provided through the list gdstk.Library.cells. As a shorthand, if the desired cell name is know, the idiom lib1["CELL_NAME"] can be used, although it is not as efficient as building a cell dictionary if a large number of queries is expected.

Additionally, the method gdstk.Library.top_level() can be used to find the top-level cells in the library (cells on the top of the hierarchy, i.e., cell that are not referenced by any other cells).

Raw Cells

Library loaded using the previous method have all their elements interpreted and re-created by Gdstk. This can be time-consuming for large layouts. If the reason for loading a file is simply to re-use it’s cells without any modifications, the function gdstk.read_rawcells() is much more efficient.

# Load all cells from a GDSII file without creating the actual geometry
cells = gdstk.read_rawcells("filename.gds")

# Use some loaded cell in the current design
my_ref = gdstk.Reference(cells["SOME_CELL"], (0, 0))
Map<RawCell*> cells = read_rawcells("filename.gds");

Reference my_ref = {};
my_ref.init(cells.get("SOME_CELL"), 1);

Note

This method only works when using the GDSII format; OASIS does not support gdstk.RawCell. Units are not changed in this process, so the current design must use the same unit and precision as the loaded cells.