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