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);
}
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);
}
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);
}
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();
}
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();
}
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();
}
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);
}
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});
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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.