GDSPY(1) | gdspy | GDSPY(1) |
gdspy - gdspy Documentation
gdspy is a Python module that allows the creation of GDSII stream files.
Most features of the GDSII format are implemented, including support for polygons with any number of vertices.
GDSII format references:
For installation instructions and other information, please check out the GitHub repository or the README file included with the source.
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. Most modern software disregards this limit and allows an arbitrary number of points per polygon. Gdspy 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 gdspy is 1 µm (10⁻⁶ m), but that can be easily changed by the user.
Let's create our first GDSII file:
import gdspy # Create the geometry: a single rectangle. rect = gdspy.Rectangle((0, 0), (2, 1)) cell = gdspy.Cell('FIRST') cell.add(rect) # Save all created cells in file 'first.gds'. gdspy.write_gds('first.gds') # Optionally, display all cells using the internal viewer. gdspy.LayoutViewer()
After importing the gdspy module, we create a gdspy.Rectangle with opposing corners at positions (0, 0) and (2, 1).
Then a gdspy.Cell is created and the rectangle is added to the cell. All shapes in the GDSII format exist inside cells. A cell can be imagined as a piece of paper where the layout will be defined. Later, the cells can be used to create a hierarchy of geometries, ass we'll see in References.
Finally, the whole structure is saved in a file called "first.gds" in the current directory. By default, all created cells are included in this operation.
The GDSII file can be opened in a number of viewers and editors, such as KLayout. Alternatively, gdspy includes a simple viewer that can also be used: gdspy.LayoutViewer.
General polygons can be defined by an ordered list of vertices. The orientation of the vertices (clockwise/counter-clockwise) is not important.
# 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 = gdspy.Polygon(points)
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 = gdspy.Polygon(
[(0, 0), (5, 0), (5, 5), (0, 5), (0, 0), (2, 2), (2, 3), (3, 3), (3, 2), (2, 2)] )
The gdspy.Round class creates circles, ellipses, doughnuts, arcs and slices. In all cases, the arguments tolerance or number_of_points will control the number of vertices used to approximate the curved shapes.
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.
# Circle centered at (0, 0), with radius 2 and tolerance 0.1 circle = gdspy.Round((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 = gdspy.Round((4, 0), [1, 2], tolerance=1e-4) # Circular arc example arc = gdspy.Round(
(2, 4),
2,
inner_radius=1,
initial_angle=-0.2 * numpy.pi,
final_angle=1.2 * numpy.pi,
tolerance=0.01, )
Constructing complex polygons by manually listing all vertices in gdspy.Polygon can be challenging. The class gdspy.Curve can be used to facilitate the creation of polygons by drawing their shapes step-by-step. It uses a syntax similar to the SVG path specification.
A short summary of the available methods is presented below:
Method | Primitive |
L/l | Line segments |
H/h | Horizontal line segments |
V/v | Vertical line segments |
C/c | Cubic Bezier curve |
S/s | Smooth cubic Bezier curve |
Q/q | Quadratic Bezier curve |
T/t | Smooth quadratic Bezier curve |
B/b | General degree Bezier curve |
I/i | Smooth interpolating curve |
arc | Elliptical arc |
The uppercase version of the methods considers that all coordinates are absolute, whereas the lowercase considers that they are relative to the current end point of the curve. Except for gdspy.Curve.I(), gdspy.Curve.i() and gdspy.Curve.arc(), they accept variable numbers of arguments that are used as coordinates to construct the primitive.
# Construct a curve made of a sequence of line segments c1 = gdspy.Curve(0, 0).L(1, 0, 2, 1, 2, 2, 0, 2) p1 = gdspy.Polygon(c1.get_points()) # Construct another curve using relative coordinates c2 = gdspy.Curve(3, 1).l(1, 0, 2, 1, 2, 2, 0, 2) p2 = gdspy.Polygon(c2.get_points())
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 gdspy.Round, but they allow for an extra rotation of the major axis of the ellipse.
# Use complex numbers to facilitate writing polar coordinates c3 = gdspy.Curve(0, 2).l(4 * numpy.exp(1j * numpy.pi / 6)) # Elliptical arcs have syntax similar to gdspy.Round c3.arc((4, 2), 0.5 * numpy.pi, -0.5 * numpy.pi) p3 = gdspy.Polygon(c3.get_points())
Other curves can be constructed as cubic, quadratic and general-degree Bezier curves. Additionally, a smooth interpolating curve can be calculated with the methods gdspy.Curve.I() and gdspy.Curve.i(), which have a number of arguments to control the shape of the curve.
# Cubic Bezier curves can be easily created with C and c c4 = gdspy.Curve(0, 0).c(1, 0, 1, 1, 2, 1) # Smooth continuation with S or s c4.s(1, 1, 0, 1).S(numpy.exp(1j * numpy.pi / 6), 0, 0) p4 = gdspy.Polygon(c4.get_points()) # Similarly for quadratic Bezier curves c5 = gdspy.Curve(5, 3).Q(3, 2, 3, 0, 5, 0, 4.5, 1).T(5, 3) p5 = gdspy.Polygon(c5.get_points()) # Smooth interpolating curves can be built using I or i, including # closed shapes c6 = gdspy.Curve(0, 3).i([(1, 0), (2, 0), (1, -1)], cycle=True) p6 = gdspy.Polygon(c6.get_points())
All polygons can be transformed trough gdspy.PolygonSet.translate(), gdspy.PolygonSet.rotate(), gdspy.PolygonSet.scale(), and gdspy.PolygonSet.mirror(). The transformations are applied in-place, i.e., no polygons are created.
poly = gdspy.Rectangle((-2, -2), (2, 2)) poly.rotate(numpy.pi / 4) poly.scale(1, 0.5)
All shapes in the GDSII format are tagged with 2 properties: layer and datatype (or texttype in the case of gdspy.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 GDSII 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 datatype configurations.
# Layer/datatype definitions for each step in the fabrication ld_fulletch = {"layer": 1, "datatype": 3} ld_partetch = {"layer": 2, "datatype": 3} ld_liftoff = {"layer": 0, "datatype": 7} p1 = gdspy.Rectangle((-3, -3), (3, 3), **ld_fulletch) p2 = gdspy.Rectangle((-5, -3), (-3, 3), **ld_partetch) p3 = gdspy.Rectangle((5, -3), (3, 3), **ld_partetch) p4 = gdspy.Round((0, 0), 2.5, number_of_points=6, **ld_liftoff)
References give the GDSII format its hierarchical features. They work by reusing a cell content in another cell (without actually copying the whole geometry). As a simplistic example, imagine the we are designing a simple electronic circuit that uses hundreds of transistors, but they all have 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 with gdspy.CellReference, it is possible to create full 2D arrays with a single entity using gdspy.CellArray. Both are exemplified below.
# Create a cell with a component that is used repeatedly contact = gdspy.Cell("CONTACT") contact.add([p1, p2, p3, p4]) # Create a cell with the complete device device = gdspy.Cell("DEVICE") device.add(cutout) # Add 2 references to the component changing size and orientation ref1 = gdspy.CellReference(contact, (3.5, 1), magnification=0.25) ref2 = gdspy.CellReference(contact, (1, 3.5), magnification=0.25, rotation=90) device.add([ref1, ref2]) # The final layout has several repetitions of the complete device main = gdspy.Cell("MAIN") main.add(gdspy.CellArray(device, 3, 2, (6, 7)))
Besides polygons, the GDSII format defines 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, 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 GDSII file to specify those. Usually the joins are straight extensions of the path boundaries up to some beveling limit. Gdspy also uses this specification for the joins.
It is possible to circumvent all of the above limitations within gdspy by storing paths as polygons in the GDSII 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 paths or polygonal paths) in gdspy is quite rich. There are 3 classes that can be used depending on the requirements of the desired path.
The class gdspy.Path is designed to allow the creation of path-like polygons in a piece-wise manner. It is the most computationally efficient class between the three because it does not calculate joins. That means the user is responsible for designing the joins. The paths can end up with discontinuities if care is not taken when creating them.
# Start a path at (0, 0) with width 1 path1 = gdspy.Path(1, (0, 0)) # Add a segment to the path goin in the '+y' direction path1.segment(4, "+y") # Further segments or turns will folow the current path direction # to ensure continuity path1.turn(2, "r") path1.segment(1) path1.turn(3, "rr")
Just as with Circles, all curved geometry is approximated by line segments. The number of segments is similarly controlled by a tolerance or a number_of_points argument. Curves also include fracturing to limit the number of points in each polygon.
More complex paths can be constructed with the methods gdspy.Path.bezier(), gdspy.Path.smooth(), and gdspy.Path.parametric(). The example below demonstrates a couple of possibilities.
path2 = gdspy.Path(0.5, (0, 0)) # Start the path with a smooth Bezier S-curve path2.bezier([(0, 5), (5, 5), (5, 10)]) # We want to add a spiral curve to the path. The spiral is defined # as a parametric curve. We make sure spiral(0) = (0, 0) so that # the path is continuous. def spiral(u):
r = 4 - 3 * u
theta = 5 * u * numpy.pi
x = r * numpy.cos(theta) - 4
y = r * numpy.sin(theta)
return (x, y) # It is recommended to also define the derivative of the parametric # curve, otherwise this derivative must be calculated nummerically. # The derivative is used to define the side boundaries of the path, # so, in this case, to ensure continuity with the existing S-curve, # we make sure the the direction at the start of the spiral is # pointing exactly upwards, as if is radius were constant. # Additionally, the exact magnitude of the derivative is not # important; gdspy only uses its direction. def dspiral_dt(u):
theta = 5 * u * numpy.pi
dx_dt = -numpy.sin(theta)
dy_dt = numpy.cos(theta)
return (dx_dt, dy_dt) # Add the parametric spiral to the path path2.parametric(spiral, dspiral_dt)
The width of the path does not have to be constant. Each path component can linearly taper the width of the path by using the final_width argument. In the case of a parametric curve, more complex width changes can be created by setting final_width to a function.
Finally, parallel paths can be created simultaneously with the help of arguments number_of_paths, distance, and final_distance.
# Start 3 parallel paths with center-to-center distance of 1.5 path3 = gdspy.Path(0.1, (-5.5, 3), number_of_paths=3, distance=1.5) # Add a segment tapering the widths up to 0.5 path3.segment(2, "-y", final_width=0.5) # Add a bezier curve decreasing the distance between paths to 0.75 path3.bezier([(0, -2), (1, -3), (3, -3)], final_distance=0.75) # Add a parametric section to modulate the width with a sinusoidal # shape. Note that the algorithm that determines the number of # evaluations of the parametric curve does not take the width into # consideration, so we have to manually increase this parameter. path3.parametric(
lambda u: (5 * u, 0),
lambda u: (1, 0),
final_width=lambda u: 0.4 + 0.1 * numpy.cos(10 * numpy.pi * u),
number_of_evaluations=256, ) # Add a circular turn and a final tapering segment. path3.turn(3, "l") path3.segment(2, final_width=1, final_distance=1.5)
Although very efficient, gdspy.Path is limited in the type of path it can provide. For example, if we simply want a path going through a sequence of points, we need a class that can correctly compute the joins between segments. That's one of the advantages of class gdspy.FlexPath. Other path construction methods are similar to those in gdspy.Path.
A few features of gdspy.FlexPath are:
# Path defined by a sequence of points and stored as a GDSII path sp1 = gdspy.FlexPath(
[(0, 0), (3, 0), (3, 2), (5, 3), (3, 4), (0, 4)], 1, gdsii_path=True ) # Other construction methods can still be used sp1.smooth([(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. sp2 = gdspy.FlexPath(
[(12, 0), (8, 0), (8, 3), (10, 2)],
[0.3, 0.2, 0.4],
0.5,
ends=["extended", "flush", "round"],
corners=["bevel", "miter", "round"], ) sp2.arc(2, -0.5 * numpy.pi, 0.5 * numpy.pi) sp2.arc(1, 0.5 * numpy.pi, 1.5 * numpy.pi)
The following example shows other features, such as width tapering, arbitrary offsets, and custom joins and end caps.
# Path corners and end caps can be custom functions. # This corner function creates 'broken' joins. def broken(p0, v0, p1, v1, p2, w):
# Calculate intersection point p between lines defined by
# p0 + u0 * v0 (for all u0) and p1 + u1 * v1 (for all u1)
den = v1[1] * v0[0] - v1[0] * v0[1]
lim = 1e-12 * (v0[0] ** 2 + v0[1] ** 2) * (v1[0] ** 2 + v1[1] ** 2)
if den ** 2 < lim:
# Lines are parallel: use mid-point
u0 = u1 = 0
p = 0.5 * (p0 + p1)
else:
dx = p1[0] - p0[0]
dy = p1[1] - p0[1]
u0 = (v1[1] * dx - v1[0] * dy) / den
u1 = (v0[1] * dx - v0[0] * dy) / den
p = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1)
if u0 <= 0 and u1 >= 0:
# Inner corner
return [p]
# Outer corner
return [p0, p2, p1] # This end cap function creates pointy caps. def pointy(p0, v0, p1, v1):
r = 0.5 * numpy.sqrt(numpy.sum((p0 - p1) ** 2))
v0 /= numpy.sqrt(numpy.sum(v0 ** 2))
v1 /= numpy.sqrt(numpy.sum(v1 ** 2))
return [p0, 0.5 * (p0 + p1) + 0.5 * (v0 - v1) * r, p1] # Paths with arbitrary offsets from the center and multiple layers. sp3 = gdspy.FlexPath(
[(0, 0), (0, 1)],
[0.1, 0.3, 0.5],
offset=[-0.2, 0, 0.4],
layer=[0, 1, 2],
corners=broken,
ends=pointy, ) sp3.segment((3, 3), offset=[-0.5, -0.1, 0.5]) sp3.segment((4, 1), width=[0.2, 0.2, 0.2], offset=[-0.2, 0, 0.2]) sp3.segment((0, -1), relative=True)
The corner type 'circular bend' (together with the bend_radius argument) can be used to automatically curve the path. This feature is used in Example: Integrated Photonics.
# Path created with automatic bends of radius 5 points = [(0, 0), (0, 10), (20, 0), (18, 15), (8, 15)] sp4 = gdspy.FlexPath(
points, 0.5, corners="circular bend", bend_radius=5, gdsii_path=True ) # Same path, generated with natural corners, for comparison sp5 = gdspy.FlexPath(points, 0.5, layer=1, gdsii_path=True)
In some situations, gdspy.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 join other curves or segments at sharp angles are an example of such situation.
The class gdspy.RobustPath can be used in such scenarios where robustness is more important than efficiency due to sharp corners or large offsets in the paths. The drawbacks of using gdspy.RobustPath are the loss in computation efficiency (compared to the other 2 classes) and the impossibility of specifying corner shapes. 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 in any construction method.
# Create 4 parallel paths in different layers lp = gdspy.RobustPath(
(50, 0),
[2, 0.5, 1, 1],
[0, 0, -1, 1],
ends=["extended", "round", "flush", "flush"],
layer=[0, 2, 1, 1], ) lp.segment((45, 0)) lp.segment(
(5, 0),
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),
], ) lp.segment((0, 0)) lp.smooth(
[(5, 10)],
angles=[0.5 * numpy.pi, 0],
width=0.5,
offset=[-0.25, 0.25, -0.75, 0.75], ) lp.parametric(
lambda u: numpy.array((45 * u, 4 * numpy.sin(6 * numpy.pi * 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,
], )
Note that, analogously to gdspy.FlexPath, gdspy.RobustPath can be stored as a GDSII path as long as its width is kept constant.
In the context of a GDSII 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 texttype values (texttype is the label equivalent of the polygon datatype). They are supported by the class gdspy.Label.
Additionally, gdspy offers the possibility of creating text as polygons to be included with the geometry. The class gdspy.Text creates polygonal text that can be used in the same way as any other polygons in gdspy. The font used to render the characters contains only horizontal and vertical edges, which is important for some laser writing systems.
# Label anchored at (1, 3) by its north-west corner label = gdspy.Label("Sample label", (1, 3), "nw") # Horizontal text with height 2.25 htext = gdspy.Text("12345", 2.25, (0.25, 6)) # Vertical text with height 1.5 vtext = gdspy.Text("ABC", 1.5, (10.5, 4), horizontal=False) rect = gdspy.Rectangle((0, 0), (10, 6), layer=10)
Gdspy offers a number of functions and methods to modify existing geometry. The most useful operations include gdspy.boolean(), gdspy.slice(), gdspy.offset(), and gdspy.PolygonSet.fillet().
Boolean operations (gdspy.boolean()) can be performed on polygons, paths and whole cells. Four operations are defined: union ('or'), intersection ('and'), subtraction ('not'), and symmetric subtraction ('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 = gdspy.Text("GDSPY", 4, (0, 0)) # Create a rectangle extending the text's bounding box by 1 bb = numpy.array(text.get_bounding_box()) rect = gdspy.Rectangle(bb[0] - 1, bb[1] + 1) # Subtract the text from the rectangle inv = gdspy.boolean(rect, text, "not")
As the name indicates, a slice operation subdivides a set of polygons along horizontal or vertical cut lines.
In a few cases, a boolean operation can be substituted by one or more slice operations. Because gdspy.slice() is ususally much simpler than gdspy.boolean(), it is a good idea to use the former if possible.
ring1 = gdspy.Round((-6, 0), 6, inner_radius=4) ring2 = gdspy.Round((0, 0), 6, inner_radius=4) ring3 = gdspy.Round((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 = gdspy.slice(ring1, -3, axis=0) slices2 = gdspy.slice(ring2, [-3, 3], axis=0) slices3 = gdspy.slice(ring3, 3, axis=0) slices = gdspy.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])
The function gdspy.offset() expands or contracts polygons by a fixed amount. It can operate on individual polygons or sets of them, in which case it may make sense to use the argument join_first to operate on the whole geometry as if a boolean 'or' was executed beforehand.
rect1 = gdspy.Rectangle((-4, -4), (1, 1)) rect2 = gdspy.Rectangle((-1, -1), (4, 4)) # Offset both polygons # Because we join them first, a single polygon is created. outer = gdspy.offset([rect1, rect2], 0.5, join_first=True, layer=1)
The method gdspy.PolygonSet.fillet() can be used to round polygon corners. It doesn't have a join_first argument as gdspy.offset(), so if it will be used on a polygon, that polygon should probably not be fractured.
multi_path = gdspy.Path(2, (-3, -2)) multi_path.segment(4, "+x") multi_path.turn(2, "l").turn(2, "r") multi_path.segment(4) # Create a copy with joined polygons and no fracturing joined = gdspy.boolean(multi_path, None, "or", max_points=0) joined.translate(0, -5) # Fillet applied to each polygon in the path multi_path.fillet(0.5) # Fillet applied to the joined copy joined.fillet(0.5)
All the information used to create a GDSII file is kept within an instance of GdsLibrary. 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 GDSII file and has no other purpose in gdspy. The units require some attention because they can impact the resolution of the polygons in the library when written to a file.
Two values are defined: 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].
To save a GDSII file, the easiest way is to use gdspy.write_gds(), as in the First GDSII. That function accepts arguments unit and precision to change the default values, as explained in the section above.
In reality, it calls the gdspy.GdsLibrary.write_gds() method from a global gdspy.GdsLibrary instance: gdspy.current_library. This instance automatically holds all cells created by gdspy unless specifically told not to with the argument exclude_from_current set to True in gdspy.Cell.
That means that after saving a file, if a new GDSII library is to be started from scratch using the global instance, it is important to reinitialize it with:
gdspy.current_library = gdspy.GdsLibrary()
To load an existing GDSII file (or to work simultaneously with multiple libraries), a new instance of GdsLibrary can be created or an existing one can be used:
# Load a GDSII file into a new library gdsii = gdspy.GdsLibrary(infile='filename.gds') # Use the current global library to load the file gdspy.current_library.read_gds('filename.gds')
In either case, care must be taken to merge the units from the library and the file, which is controlled by the argument units in gdspy.GdsLibrary.read_gds() (keyword argument in gdspy.GdsLibrary).
Access to the cells in the loaded library is provided through the dictionary gdspy.GdsLibrary.cell_dict (cells indexed by name). The method gdspy.GdsLibrary.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) and gdspy.GdsLibrary.extract() can be used to import a given cell and all of its dependencies into gdspy.current_library.
This example demonstrates the use of gdspy primitives to create more complex structures.
These structures are commonly used in the field of integrated photonics.
photonics.py
photonics.gds
###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or <http://www.boost.org/LICENSE_1_0.txt> # # # ###################################################################### import numpy import gdspy def grating(period, number_of_teeth, fill_frac, width, position, direction, lda=1, sin_theta=0,
focus_distance=-1, focus_width=-1, tolerance=0.001, layer=0, datatype=0):
'''
Straight or focusing grating.
period : grating period
number_of_teeth : number of teeth in the grating
fill_frac : filling fraction of the teeth (w.r.t. the period)
width : width of the grating
position : grating position (feed point)
direction : one of {'+x', '-x', '+y', '-y'}
lda : free-space wavelength
sin_theta : sine of incidence angle
focus_distance : focus distance (negative for straight grating)
focus_width : if non-negative, the focusing area is included in
the result (usually for negative resists) and this
is the width of the waveguide connecting to the
grating
tolerance : same as in `path.parametric`
layer : GDSII layer number
datatype : GDSII datatype number
Return `PolygonSet`
'''
if focus_distance < 0:
p = gdspy.L1Path((position[0] - 0.5 * width,
position[1] + 0.5 * (number_of_teeth - 1 + fill_frac) * period),
'+x', period * fill_frac, [width], [], number_of_teeth, period,
layer=layer, datatype=datatype)
else:
neff = lda / float(period) + sin_theta
qmin = int(focus_distance / float(period) + 0.5)
p = gdspy.Path(period * fill_frac, position)
c3 = neff**2 - sin_theta**2
w = 0.5 * width
for q in range(qmin, qmin + number_of_teeth):
c1 = q * lda * sin_theta
c2 = (q * lda)**2
p.parametric(lambda t: (width * t - w,
(c1 + neff * numpy.sqrt(c2 - c3 * (width * t - w)**2)) / c3),
tolerance=tolerance, max_points=0, layer=layer, datatype=datatype)
p.x = position[0]
p.y = position[1]
sz = p.polygons[0].shape[0] // 2
if focus_width == 0:
p.polygons[0] = numpy.vstack((p.polygons[0][:sz, :], [position]))
elif focus_width > 0:
p.polygons[0] = numpy.vstack((p.polygons[0][:sz, :],
[(position[0] + 0.5 * focus_width, position[1]),
(position[0] - 0.5 * focus_width, position[1])]))
p.fracture()
if direction == '-x':
return p.rotate(0.5 * numpy.pi, position)
elif direction == '+x':
return p.rotate(-0.5 * numpy.pi, position)
elif direction == '-y':
return p.rotate(numpy.pi, position)
else:
return p if __name__ == '__main__':
# Examples
# Negative resist example
width = 0.45
bend_radius = 50.0
ring_radius = 20.0
taper_len = 50.0
input_gap = 150.0
io_gap = 500.0
wg_gap = 20.0
ring_gaps = [0.06 + 0.02 * i for i in range(8)]
ring = gdspy.Cell('NRing')
ring.add(gdspy.Round((ring_radius, 0), ring_radius, ring_radius - width, tolerance=0.001))
grat = gdspy.Cell('NGrat')
grat.add(grating(0.626, 28, 0.5, 19, (0, 0), '+y', 1.55,
numpy.sin(numpy.pi * 8 / 180), 21.5, width,
tolerance=0.001))
taper = gdspy.Cell('NTaper')
taper.add(gdspy.Path(0.12, (0, 0)).segment(taper_len, '+y', final_width=width))
c = gdspy.Cell('Negative')
for i, gap in enumerate(ring_gaps):
path = gdspy.FlexPath([(input_gap * i, taper_len)], width=width,
corners='circular bend', bend_radius=bend_radius,
gdsii_path=True)
path.segment((0, 600 - wg_gap * i), relative=True)
path.segment((io_gap, 0), relative=True)
path.segment((0, 300 + wg_gap * i), relative=True)
c.add(path)
c.add(gdspy.CellReference(ring, (input_gap * i + width / 2 + gap, 300)))
c.add(gdspy.CellArray(taper, len(ring_gaps), 1, (input_gap, 0), (0, 0)))
c.add(gdspy.CellArray(grat, len(ring_gaps), 1, (input_gap, 0), (io_gap, 900 + taper_len)))
# Positive resist example
width = 0.45
ring_radius = 20.0
big_margin = 10.0
small_margin = 5.0
taper_len = 50.0
bus_len = 400.0
input_gap = 150.0
io_gap = 500.0
wg_gap = 20.0
ring_gaps = [0.06 + 0.02 * i for i in range(8)]
ring_margin = gdspy.Rectangle((0, -ring_radius - big_margin),
(2 * ring_radius + big_margin, ring_radius + big_margin))
ring_hole = gdspy.Round((ring_radius, 0), ring_radius, ring_radius - width, tolerance=0.001)
ring_bus = gdspy.Path(small_margin, (0, taper_len), number_of_paths=2,
distance=small_margin + width)
ring_bus.segment(bus_len, '+y')
p = gdspy.Path(small_margin, (0, 0), number_of_paths=2, distance=small_margin + width)
p.segment(21.5, '+y', final_distance=small_margin + 19)
grat = gdspy.Cell('PGrat').add(p)
grat.add(grating(0.626, 28, 0.5, 19, (0, 0), '+y', 1.55,
numpy.sin(numpy.pi * 8 / 180), 21.5, tolerance=0.001))
p = gdspy.Path(big_margin, (0, 0), number_of_paths=2, distance=big_margin + 0.12)
p.segment(taper_len, '+y', final_width=small_margin, final_distance=small_margin + width)
taper = gdspy.Cell('PTaper').add(p)
c = gdspy.Cell('Positive')
for i, gap in enumerate(ring_gaps):
path = gdspy.FlexPath([(input_gap * i, taper_len + bus_len)],
width=[small_margin, small_margin],
offset=small_margin + width, gdsii_path=True)
path.segment((0, 600 - bus_len - bend_radius - wg_gap * i), relative=True)
path.turn(bend_radius, 'r')
path.segment((io_gap - 2 * bend_radius, 0), relative=True)
path.turn(bend_radius, 'l')
path.segment((0, 300 - bend_radius + wg_gap * i), relative=True)
c.add(path)
dx = width / 2 + gap
c.add(gdspy.boolean(
gdspy.boolean(ring_bus, gdspy.copy(ring_margin, dx, 300), 'or', precision=1e-4),
gdspy.copy(ring_hole, dx, 300), 'not', precision=1e-4).translate(input_gap * i, 0))
c.add(gdspy.CellArray(taper, len(ring_gaps), 1, (input_gap, 0), (0, 0)))
c.add(gdspy.CellArray(grat, len(ring_gaps), 1, (input_gap, 0), (io_gap, 900 + taper_len)))
# Save to a gds file and check out the output
gdspy.write_gds('photonics.gds')
gdspy.LayoutViewer()
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 gdspy.PolygonSet objects.
fonts.py
fonts.gds
###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or <http://www.boost.org/LICENSE_1_0.txt> # # # ###################################################################### from matplotlib.font_manager import FontProperties from matplotlib.textpath import TextPath import gdspy def render_text(text, size=None, position=(0, 0), font_prop=None, tolerance=0.1):
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 = gdspy.Curve(*points, tolerance=tolerance)
elif code == path.LINETO:
c.L(*points)
elif code == path.CURVE3:
c.Q(*points)
elif code == path.CURVE4:
c.C(*points)
elif code == path.CLOSEPOLY:
poly = c.get_points()
if poly.size > 0:
if poly[:, 0].min() < xmax:
i = len(polys) - 1
while i >= 0:
if gdspy.inside(poly[:1], [polys[i]], precision=0.1 * tolerance)[0]:
p = polys.pop(i)
poly = gdspy.boolean([p], [poly], 'xor', precision=0.1 * tolerance,
max_points=0).polygons[0]
break
elif gdspy.inside(polys[i][:1], [poly], precision=0.1 * tolerance)[0]:
p = polys.pop(i)
poly = gdspy.boolean([p], [poly], 'xor', precision=0.1 * tolerance,
max_points=0).polygons[0]
i -= 1
xmax = max(xmax, poly[:, 0].max())
polys.append(poly)
return polys if __name__ == "__main__":
fp = FontProperties(family='serif', style='italic')
text = gdspy.PolygonSet(render_text('Text rendering', 10, font_prop=fp), layer=1)
gdspy.Cell('TXT').add(text)
gdspy.write_gds('fonts.gds')
gdspy.LayoutViewer()
Set of polygonal objects.
The last point should not be equal to the first (polygons are automatically closed).
The original GDSII specification supports only a maximum of 199 vertices per polygon.
Polygonal geometric object.
The last point should not be equal to the first (polygons are automatically closed).
The original GDSII specification supports only a maximum of 199 vertices per polygon.
>>> triangle_pts = [(0, 40), (15, 40), (10, 50)] >>> triangle = gdspy.Polygon(triangle_pts) >>> myCell.add(triangle)
Rectangular geometric object.
>>> rectangle = gdspy.Rectangle((0, 0), (10, 20)) >>> myCell.add(rectangle)
Circular geometric object.
Represent a circle, ellipse, ring or their sections.
The original GDSII specification supports only a maximum of 199 vertices per polygon.
>>> circle = gdspy.Round((30, 5), 8) >>> ell_ring = gdspy.Round((50, 5), (8, 7), inner_radius=(5, 4)) >>> pie_slice = gdspy.Round((30, 25), 8, initial_angle=0, ... final_angle=-5.0*numpy.pi/6.0) >>> arc = gdspy.Round((50, 25), 8, inner_radius=5, ... initial_angle=-5.0*numpy.pi/6.0, ... final_angle=0)
Polygonal text object.
Each letter is formed by a series of polygons.
>>> text = gdspy.Text('Sample text', 20, (-10, -100)) >>> myCell.add(text)
Series of geometric objects that form a path or a collection of parallel paths.
The direction of the path is not modified by this method and its width is scaled only by scalex.
The original GDSII specification supports only a maximum of 199 vertices per polygon.
The original GDSII specification supports only a maximum of 199 vertices per polygon.
curve_function will be evaluated uniformly in the interval [0, 1] at least number_of_points times. More points will be added to the curve at the midpoint between evaluations if that points presents error larger than tolerance.
The norm of the vector returned by curve_derivative is not important. Only the direction is used.
The original GDSII specification supports only a maximum of 199 vertices per polygon.
>>> def my_parametric_curve(t): ... return (2**t, t**2) >>> def my_parametric_curve_derivative(t): ... return (0.69315 * 2**t, 2 * t) >>> my_path.parametric(my_parametric_curve, ... my_parametric_curve_derivative)
A Bezier curve is added to the path starting from its current position and finishing at the last point in the points array.
The original GDSII specification supports only a maximum of 199 vertices per polygon.
Uses the Hobby algorithm
[1]_
The original GDSII specification supports only a maximum of 199 vertices per polygon.
Series of geometric objects that form a polygonal path or a collection of parallel polygonal paths.
Deprecated since version 1.4: PolyPath is deprecated in favor of FlexPath and will be removed in a future version of Gdspy.
The bevel join will give strange results if the number of paths is greater than 1.
Series of geometric objects that form a path or a collection of parallel paths with Manhattan geometry.
Deprecated since version 1.4: L1Path is deprecated in favor of FlexPath and will be removed in a future version of Gdspy.
>>> length = [10, 30, 15, 15, 15, 15, 10] >>> turn = [1, -1, -1, 3, -1, 1] >>> l1path = gdspy.L1Path((0, 0), '+x', 2, length, turn) >>> myCell.add(l1path)
Path object.
This class keeps information about the constructive parameters of the path and calculates its boundaries only upon request.
It can be stored as a proper path element in the GDSII format, unlike Path. In this case, the width must be constant along the whole path.
The value of tolerance should not be smaller than precision, otherwise there would be wasted computational effort in calculating the paths.
The resulting object will be fractured according to the parameter max_points used when instantiating this object.
If FlexPath.gdsii_path is True, GDSII path elements are created instead of boundaries. Such paths do not support variable widths, but their memeory footprint is smaller than full polygonal boundaries.
This functions creates a PolgonSet from this object and calculates its area, which means it is computationally expensive.
Applies the transformations in the same order as a CellReference or a CellArray. If width_transform is False, the widths are not scaled.
The initial angle of the arc is calculated from the last path segment.
A Bezier curve is added to the path starting from its current position and finishing at the last point in the points array.
Uses the Hobby algorithm
[1]_
Arguments width and offset are repeated for each cubic Bezier that composes this path element.
Path object with lazy evaluation.
This class keeps information about the constructive parameters of the path and calculates its boundaries only upon request. The benefits are that joins and path components can be calculated automatically to ensure continuity (except in extreme cases).
It can be stored as a proper path element in the GDSII format, unlike Path. In this case, the width must be constant along the whole path.
The downside of RobustPath is that it is more computationally expensive than the other path classes.
The value of tolerance should not be smaller than precision, otherwise there would be wasted computational effort in calculating the paths.
The resulting object will be fractured according to the parameter max_points used when instantiating this object.
If RobustPath.gdsii_path is True, GDSII path elements are created instead of boundaries. Such paths do not support variable widths, but their memeory footprint is smaller than full polygonal boundaries.
This functions creates a PolgonSet from this object and calculates its area, which means it is computationally expensive.
Applies the transformations in the same order as a CellReference or a CellArray. If width_transform is False, the widths are not scaled.
The initial angle of the arc is calculated from an average of the current directions of all parallel paths in this object.
A Bezier curve is added to the path starting from its current position and finishing at the last point in the points array.
Uses the Hobby algorithm
[1]_
Arguments width and offset are repeated for each cubic Bezier that composes this path element.
Generation of curves loosely based on SVG paths.
Short summary of available methods:
Method | Primitive |
L/l | Line segments |
H/h | Horizontal line segments |
V/v | Vertical line segments |
C/c | Cubic Bezier curve |
S/s | Smooth cubic Bezier curve |
Q/q | Quadratic Bezier curve |
T/t | Smooth quadratic Bezier curve |
B/b | General degree Bezier curve |
I/i | Smooth interpolating curve |
arc | Elliptical arc |
The uppercase version of the methods considers that all coordinates are absolute, whereas the lowercase considers that they are relative to the current end point of the curve.
In all methods of this class that accept coordinate pairs, a single complex number can be passed to be split into its real and imaginary parts. This feature can be useful in expressing coordinates in polar form.
All commands follow the SVG 2 specification, except for elliptical arcs and smooth interpolating curves, which are inspired by the Metapost syntax.
>>> curve = gdspy.Curve(3, 4).H(1).q(0.5, 1, 2j).L(2 + 3j, 2, 2) >>> pol = gdspy.Polygon(curve.get_points())
Uses the Hobby algorithm
[1]_
>>> c1 = gdspy.Curve(0, 1).I([(1, 1), (2, 1), (1, 0)]) >>> c2 = gdspy.Curve(0, 2).I([(1, 2), (2, 2), (1, 1)], ... cycle=True) >>> ps = gdspy.PolygonSet([c1.get_points(), c2.get_points()])References
Uses the Hobby algorithm
[1]_
>>> c1 = gdspy.Curve(0, 1).i([(1, 0), (2, 0), (1, -1)]) >>> c2 = gdspy.Curve(0, 2).i([(1, 0), (2, 0), (1, -1)], ... cycle=True) >>> ps = gdspy.PolygonSet([c1.get_points(), c2.get_points()])References
Text that can be used to label parts of the geometry or display messages. The text does not create additional geometry, it's meant for display and labeling purposes only.
>>> label = gdspy.Label('Sample label', (10, 0), 'sw') >>> myCell.add(label)
>>> text = gdspy.Label((0, 0), (10, 20)) >>> text = text.translate(2, 0) >>> myCell.add(text)
>>> ring = gdspy.Round((0, 0), 10, inner_radius = 5) >>> result = gdspy.slice(ring, [-7, 7], 0) >>> cell.add(result[1])
>>> rectangle = gdspy.Rectangle((0, 0), (10, 20)) >>> rectangle2 = gdspy.copy(rectangle, 2,0) >>> myCell.add(rectangle) >>> myCell.add(rectangle2)
Collection of polygons, paths, labels and raferences to other cells.
The function or callable test is called for each polygon in the cell. If its return value evaluates to True, the corresponding polygon is removed from the cell.
Remove polygons in layer 1:
>>> cell.remove_polygons(lambda pts, layer, datatype: ... layer == 1)
Remove polygons with negative x coordinates:
>>> cell.remove_polygons(lambda pts, layer, datatype: ... any(pts[:, 0] < 0))
The function or callable test is called for each FlexPath or RobustPath in the cell. If its return value evaluates to True, the corresponding label is removed from the cell.
The function or callable test is called for each label in the cell. If its return value evaluates to True, the corresponding label is removed from the cell.
Remove labels in layer 1:
>>> cell.remove_labels(lambda lbl: lbl.layer == 1)
NOTE:
Simple reference to an existing cell.
NOTE:
Multiple references to an existing cell in an array format.
NOTE:
GDSII library (file).
Represent a GDSII library containing a dictionary of cells.
CellReference or CellArray instances that referred to an overwritten cell are not automatically updated.
The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio unit/precision. For example, if a circle with radius 1.5 is created and we set GdsLibrary.unit to 1.0e-6 (1 um) and GdsLibrary.precision to 1.0e-9` (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm.
Only the specified cells are written. The user is responsible for ensuring all cell dependencies are satisfied.
Not all features from the GDSII specification are currently supported. A warning will be produced if any unsupported features are found in the imported file.
CellReference or CellArray instances that referred to an overwritten cell are not automatically updated.
Top level cells are those that are not referenced by any other cells.
GDSII strem library writer.
The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio unit/precision. For example, if a circle with radius 1.5 is created and we set unit to 1.0e-6 (1 um) and precision to 1.0e-9 (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm.
This class can be used for incremental output of the geometry in case the complete layout is too large to be kept in memory all at once.
>>> writer = gdspy.GdsWriter('out-file.gds', unit=1.0e-6, ... precision=1.0e-9) >>> for i in range(10): ... cell = gdspy.Cell('C{}'.format(i), True) ... # Add the contents of this cell... ... writer.write_cell(cell) ... # Clear the memory: erase Cell objects and any other objects ... # that won't be needed. ... del cell >>> writer.close()
Only the specified cell is written. Dependencies must be manually included.
The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio unit/precision. For example, if a circle with radius 1.5 is created and we set unit to 1.0e-6 (1 um) and precision to 1.0e-9 (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm.
The hash is generated based only on the contents of the cells in the GDSII library, ignoring any timestamp records present in the file structure.
The returned cells inherit the units of the loaded file. If they are used in a new library, the new library must use compatible units.
This variable can be freely overwritten by the user with a new instance of GdsLibrary. Examples.sp
>>> gdspy.Cell('MAIN') >>> gdspy.current_library = GdsLibrary() # Reset current library >>> gdspy.Cell('MAIN') # A new MAIN cell is created without error
Help support the development of gdspy by donating:
Click here
Lucas H. Gabrielli
2009-2020, Lucas H. Gabrielli
March 21, 2020 | 1.4.2 |