GDSII import

See on github, run on colab, or just follow along with the output below.


In Tidy3D, complex structures can be defined or imported from GDSII files via the third-party gdspy package. In this tutorial, we will first illustrate how to use the package to define a structure, then we will save this to file, and then we will read that file and import the structures in a simulation.

# get the most recent version of tidy3d
!pip install -q --upgrade tidy3d

# gdspy is also needed for gds import
!pip install -q gdspy

# make sure notebook plots inline
%matplotlib inline

# standard python imports
import numpy as np
import matplotlib.pyplot as plt
import gdspy
import os

# tidy3D import
import tidy3d as td
from tidy3d import web
Using Tidy3D credentials from stored file

Creating a beam splitter with gdspy

First, we will construct an integrated beam splitter as in the title image in this notebook using gdspy. If you are only interested in importing an already existing GDSII file, see the next section.

We first define some structural parameters. The two arms of the device start at a distance wg_spacing_in apart, then come together at a coupling distance wg_spacing_coup for a certain length coup_length, and then split again into separate ports. In the coupling region, the field overlap results in energy exchange between the two waveguides. Here, we will only see how to define, export, and import such a device using gdspy, while in a later example we will simulate the device and study the frequency dependence of the transmission into each of the ports.

### Length scale in micron.

# Waveguide width
wg_width = 0.45
# Waveguide separation in the beginning/end
wg_spacing_in = 8
# Length of the coupling region
coup_length = 10
# Length of the bend region
bend_length = 16
# Waveguide separation in the coupling region
wg_spacing_coup = 0.10
# Total device length along propagation direction
device_length = 100

To create the device, we will define each waveguide as a GDSII path object with the given waveguide width. To do that, we just need to define a series of points along the center of each waveguide that follows the curvature we desire. First, we define a convenience function to create the central points along one of the waveguides, using a hyperbolic tangent curvature between the input and coupling regions. The second waveguide is just a reflected version of the first one.

def bend_pts(bend_length, width, npts=10):
    """ Set of points describing a tanh bend from (0, 0) to (length, width)"""
    x = np.linspace(0, bend_length, npts)
    y = width*(1 + np.tanh(6*(x/bend_length - 0.5)))/2
    return np.stack((x, y), axis=1)

def arm_pts(length, width, coup_length, bend_length, npts_bend=30):
    """ Set of points defining one arm of an integrated coupler """
    ### Make the right half of the coupler arm first
    # Make bend and offset by coup_length/2
    bend = bend_pts(bend_length, width, npts_bend)
    bend[:, 0] += coup_length / 2
    # Add starting point as (0, 0)
    right_half = np.concatenate(([[0, 0]], bend))
    # Add an extra point to make sure waveguide is straight past the bend
    right_half = np.concatenate((right_half, [[right_half[-1, 0] + 0.1, width]]))
    # Add end point as (length/2, width)
    right_half = np.concatenate((right_half, [[length/2, width]]))

    ### Make the left half by reflecting and omitting the (0, 0) point
    left_half = np.copy(right_half)[1:, :]
    left_half[:, 0] = -left_half[::-1, 0]
    left_half[:, 1] = left_half[::-1, 1]

    return np.concatenate((left_half, right_half), axis=0)

# Plot the upper arm for the current configuration
arm_center_coords = arm_pts(

fig, ax = plt.subplots(1, figsize=(8, 3))
ax.plot(arm_center_coords[:, 0], arm_center_coords[:, 1], lw=4)
ax.set_xlim([-30, 30])
ax.set_xlabel("x (um)")
ax.set_ylabel("y (um)")
ax.set_title("Upper beam splitter arm")

Next, we construct the splitter and write it to a GDS cell. We add a rectangle for the substrate to layer 0, and in layer 1 we add two paths, one for the upper and one for the lower splitter arms, and set the path width to be the waveguide width defined above.

# Reset the gdspy library.
# This could be useful if re-running the notebook without restarting the kernel.
gdspy.current_library = gdspy.GdsLibrary()
lib = gdspy.GdsLibrary()

# Geometry must be placed in GDS cells to import into Tidy3D
coup_cell = lib.new_cell('Coupler')

substrate = gdspy.Rectangle(
    (-device_length/2, -wg_spacing_in/2-10),
    (device_length/2, wg_spacing_in/2+10),

def make_coupler(
    """ Make an integrated coupler using the gdspy FlexPath object. """
    # Compute one arm of the coupler
    arm_width = (wg_spacing_in - wg_width - wg_spacing_coup)/2
    arm = arm_pts(length, arm_width, coup_length, bend_length, npts_bend)
    # Reflect and offset bottom arm
    coup_bot = np.copy(arm)
    coup_bot[:, 1] = -coup_bot[::-1, 1] - wg_width/2 - wg_spacing_coup/2
    # Offset top arm
    coup_top = np.copy(arm)
    coup_top[:, 1] += wg_width/2 + wg_spacing_coup/2

    # Create waveguides as GDS paths
    path_bot = gdspy.FlexPath(coup_bot, wg_width, layer=1, datatype=0)
    path_top = gdspy.FlexPath(coup_top, wg_width, layer=1, datatype=1)

    return [path_bot, path_top]

# Add the coupler to a gdspy cell
gds_coup = make_coupler(


# Uncomment to display the cell using the internal gdspy viewer
# gdspy.LayoutViewer(lib)

Finally, we can save what we have built to a GDSII file.

os.makedirs('data', exist_ok=True)

Importing a GDSII file to simulation

We can now add this device to a Tidy3D simulation and use our in-built plotting tools to see what we have created. First, we use gdspy to load the file that we just created, and examine its contents.

lib_loaded = gdspy.GdsLibrary(infile='data/coupler.gds')
{'Coupler': <gdspy.library.Cell object at 0x7f5e2cf16640>}
coup_cell = lib_loaded.cells['Coupler']
print("Layers in cell: ", coup_cell.get_layers())
print("Layer and datatype of each polygon in cell: ")
for (ip, poly) in enumerate(coup_cell.polygons):
    print(f"    Polygon {ip}: ({poly.layers[0]}, {poly.datatypes[0]})")
Layers in cell:  {0, 1}
Layer and datatype of each polygon in cell:
    Polygon 0: (0, 0)
    Polygon 1: (1, 0)
    Polygon 2: (1, 1)

As we know from creating the file above, Polygon 0 defines the substrate which is made of oxide, while Polygons 1 and 2 define the waveguides, made of silicon, and hence they are in a different layer. Below, we add these structures to a Tidy3D simulation, setting a height of 220nm for the waveguides. We will not do a full run here, so we will not add sources and monitors.

# Waveguide height
wg_height = 0.22

# Permittivity of waveguide and substrate
wg_n = 3.48
sub_n = 1.45
mat_wg = td.Medium(n=wg_n)
mat_sub = td.Medium(n=sub_n)

# Substrate
oxide = td.GdsSlab(

# Waveguides (import all datatypes if gds_dtype not specified)
coupler = td.GdsSlab(

# Simulation size along propagation direction
sim_length = 2 + 2*bend_length + coup_length
# Spacing between waveguides and PML
pml_spacing = 1
sim_size = [
    wg_spacing_in + wg_width + 2*pml_spacing,
    wg_height + 2*pml_spacing]

# Mesh step in all directions
mesh_step = 0.020

### Initialize and visualize simulation ###
sim = td.Simulation(
    structures=[oxide, coupler],
    pml_layers=[12, 12, 12])

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))
sim.viz_mat_2D(normal='z', position=wg_height/2, ax=ax1);
sim.viz_mat_2D(normal='x', ax=ax2, source_alpha=1);
ax2.set_xlim([-3, 3])
Initializing simulation...
Mesh step (micron): [2.00e-02, 2.00e-02, 2.00e-02].
Simulation domain in number of grid points: [2224, 546, 135].
Total number of computational grid points: 1.64e+08.
Total number of time steps: 57696.