3. Waveguides and waveguide connectors

Waveguides are the basic building block of every photonics component and circuit. Therefore, it makes sense to start learning IPKISS by introducing waveguides.

In IPKISS, we draw waveguides based on two concepts: the trace template and the trace. The trace template defines all the aspects of the waveguide that could be extracted from its cross section:

  • how the waveguide is drawn along the shape of the waveguide, i.e. the geometrical cross section;
  • the simulation model, e.g. the effective index of the waveguide.

The trace determines where and how the waveguide is drawn. It is needed to specify:

  • the path of the waveguide;
  • how the bending is drawn.

This concept is best illustrated by the image below.


Many parametric cells in IPKISS take a trace_template parameter, which determines the template that will be used to draw all the waveguides that are part of that PCell.

3.1. Draw a waveguide from a trace template

Let’s see how this concept works with a regular simple waveguide. First, we specify the trace template we want to use, together with the properties associated with it, such as the width of the waveguide core and the width of the cladding.

wg_t = pdk.SiWireWaveguideTemplate()  # Predefined trace template from si_fab
            cladding_width=2 * 3.0 + 1.0)

Then, when we instantiate the waveguide layout, we specify the shape (trace) of the waveguide. The shape is specified by a series of coordinates through which we want the waveguide to pass. By specifying the bend radius (of the circular waveguide bend as default), we make sure that the light travels through a smooth bend.

wg1 = i3.RoundedWaveguide(trace_template=wg_t)
wg1_l = wg1.Layout(shape=[(0.0, 0.0), (10.0, 0), (10.0, 10.0), (20.0, 20.0), (30.0, 0.0), (40., 40.0)],

We can now visualize our waveguide in different ways. We can use visualize() for a top-down view, or cross_section() for a cross-sectional view. We can also export the layout to GDSII with the command write_gdsii().

wg1_l.cross_section(cross_section_path=i3.Shape([(2.0, -5.0), (2.0, 5.0)])).visualize()
../../../_images/waveguide_trace_template_topview.png ../../../_images/waveguide_trace_template_cross_section.png

3.2. Routing functions

Often, you don’t want to specify the exact path of a waveguide yourself, but you just care about the start and end ports. In this case, it’s convenient to use routing functions. For example, a route created by the RouteManhattan function creates a Manhattan-like (orthogonal) waveguide between two defined ports. In our documentation, you may find various types of routing functions (Routing functions).

Let’s see how we can use routing functions to draw a waveguide. First, we define the input and output ports. These inherit from i3.OpticalPort and are characterised by a name, a position, an input/output angle and a trace template.

input_port = i3.OpticalPort(name="in", position=(5.0, 0.0), angle_deg=0.0, trace_template=wg_t)
output_port = i3.OpticalPort(name="out", position=(50.0, 30.0), angle_deg=180.0, trace_template=wg_t)

Afterwards, we connect the two ports with a route using i3.RouteManhattan.

route = i3.RouteManhattan(input_port=input_port,

To conclude, we use this route to draw a waveguide. The waveguide has a specified trace template and its shape follows the route made by i3.RouteManhattan.

wg2 = i3.RoundedWaveguide(trace_template=wg_t)
wg2_l = wg2.Layout(shape=route)

3.3. Waveguide connectors

Waveguide connectors are functions that take the following parameters as an input:

  • start_port
  • end_port
  • name
  • other parameters specific to the connector

and return a waveguide PCell that connects a start_port with an end_port. Waveguide connectors are useful to consolidate the versatility of our waveguides into functions that you can use in your design. In the training material (in ‘additional_utils’), several connectors are implemented. Let’s see some examples on how to use them. We start by importing routing functions from additional_utils:

from circuit.connector_functions import (
    wide_manhattan, manhattan_offset, bezier_bend, bezier_sbend_tapered,
    bezier_sbend, bezier_ubend,

We define some ports and trace templates that we can use to draw waveguide connectors:

# Instantiate trace templates to use for the ports
tt1 = pdk.SiWireWaveguideTemplate()

tt2 = pdk.SiWireWaveguideTemplate()

# Define some ports
port1 = i3.OpticalPort(position=(0.0, 0.0), angle=0.0, trace_template=tt1)
port2 = i3.OpticalPort(position=(100.0, 100.0), angle=180.0, trace_template=tt1)
port3 = i3.OpticalPort(position=(100.0, 100.0), angle=180.0, trace_template=tt2)
port4 = i3.OpticalPort(position=(0.0, 100.0), angle=0.0, trace_template=tt1)
port5 = i3.OpticalPort(position=(100.0, 100.0), angle=-80, trace_template=tt1)
port6 = i3.OpticalPort(position=(100.0, 100.0), angle=180.0, trace_template=tt1)

Now we can draw waveguide connectors:

  • manhattan: it draws a manhattan-type waveguide between two ports
wav = manhattan(start_port=port1, end_port=port2, name="manhattan")
lv = wav.get_default_view(i3.LayoutView)
  • wide_manhattan: it draws a manhattan-type waveguide between two ports with wider multi-mode sections in the straight sections
wav = wide_manhattan(start_port=port1, end_port=port6, name="wide_manhattan")
lv = wav.get_default_view(i3.LayoutView)
  • manhattan_offset: it draws an offset waveguide at every bend to avoid losses
wav = manhattan_offset(start_port=port1, end_port=port6, offset=0.2, name="manhattan_offset")
lv = wav.get_default_view(i3.LayoutView)
  • sbend: it draws a regular S-bend with fixed bend radius
wav = sbend(start_port=port1, end_port=port2, name="sbend")
lv = wav.get_default_view(i3.LayoutView)
  • bezier_sbend: it draws a bezier S-bend with maximal bend radius; you can check for a minimal bend radius and set the adiabatic angle of the transition
wav = bezier_sbend(start_port=port1, end_port=port2, adiabatic_angle=15.0, name="bezier_sbend",
lv = wav.get_default_view(i3.LayoutView)
  • bezier_sbend_tapered: it draw a bezier S-bend with a taper to connect ports with different trace template
wav = bezier_sbend_tapered(start_port=port1, end_port=port3, adiabatic_angle=15.0, name="bezier_sbend_tapered",
lv = wav.get_default_view(i3.LayoutView)
  • bezier_ubend: it draws a bezier U-bend with maximal bend radius
wav = bezier_ubend(start_port=port1, end_port=port4, adiabatic_angle=15.0, name="bezier_ubend",
lv = wav.get_default_view(i3.LayoutView)
  • bezier_bend: it draws a regular bend with maximal bend radius at the starting and ending points of the bend.
wav = bezier_bend(start_port=port1, end_port=port5, adiabatic_angle=15.0, name="bezier_bend",
lv = wav.get_default_view(i3.LayoutView)

3.4. Define a custom waveguide template

In addition to waveguide templates you may find in SiFab, you can also define your own waveguide template using GenericWaveguideTempate .

class MyWgTemplate(GenericWaveguideTemplate):
    class Layout(GenericWaveguideTemplate.Layout):
        core_width = i3.PositiveNumberProperty(default=0.45, doc="Core width of the waveguide")
        cladding_width = i3.PositiveNumberProperty(default=4.0, doc="Cladding width of the waveguide")

        def _default_windows(self):
            windows = []
                                              start_offset=-0.5 * self.core_width,
                                              end_offset=+0.5 * self.core_width))

                                              start_offset=-0.5 * self.cladding_width,
                                              end_offset=+0.5 * self.cladding_width))
            return windows

Now, we use this created waveguide template to draw a simple shape:

# Instantiate the new waveguide template to use it in our waveguide below
wg_tmpl = MyWgTemplate()

# Draw a waveguide
wg = i3.RoundedWaveguide(trace_template=wg_tmpl)
wg_lay = wg.Layout(shape=[(0, 0), (5, 0), (11, 5)])

Congratulations! You have now been introduced to the IPKISS waveguide concept, which is useful for drawing almost any photonic components and routing photonic circuits.