Getting started

Get started with Nazca by trying the examples below. Also be aware of the Quick-tour video and instructions on how to install Nazca.


Nazca places elements in a mask layout using a standard Cartesian coordinate system with coordinates (x, y). Nazca adds a direction with angle a to the coordinate, hence we have (x, y, a), which we also refer to as a ‘pin’. Angle a=0 points in the direction of the positive x-axis and positive angles rotate in the counter-clockwise direction. The angle provides the direction in which mask elements connect to the coordinate.

coordinate system

The Nazca concept is to first create a mask element and subsequently put it any number of times into a mask layout. Note that it is optional to actually put an element in a layout. You can also query the element’s properties.

Mask elements contain pins for providing connectivity. Two elements are connected to eachother with pins, e.g. to create a circuit. Pins in a mask element are by definition pointing outward of the element. When connecting two elements, ultilizing their pins, the pin directions (angles a) of the connected pins point towards each other by default. This is called a “chain connection” in Nazca. If you do not specify where to put an element it will conveniently connect to the last element put by means of the default in and output pin in each element.

Below are some examples to start creating layouts with Nazca in GDS. If you like to see the layout inside a Jupyter notebook you can use nd.export_plt() instead of nd.export_gds().

1. Import nazca, draw waveguides and export to gds file

Using Nazca in a .py file starts with importing the nazca module with import nazca as nd in. Next, the example creates straight waveguide elements with nd.strt() and bent waveguides with nd.bend(). Each element is placed in the layout via its put() method. An empty put() statement defaults to connecting the default input pin of the element to the default output pin of the last element placed. If no previous element was placed, put() defaults to layout coordinate (0, 0 ,0). Note all elements in the example are placed in inside a cell that is automatically created. In order to create the layout and save it as GDS use nd.export_gds(). It saves to filename ‘nazca_export.gds’ in your work directory, but you may specify a custom path and filename via the ‘filename’ keyword.

import nazca as nd

nd.strt(length=20).put()
nd.bend(angle=90).put()
nd.bend(angle=-180).put()
nd.strt(length=10).put()

nd.export_gds()

The resulting GDS image:

basic waveguides

2. Put waveguides at absolute positions

The put method can also indicate an absolute position for placement of an element:

import nazca as nd

nd.strt(length=5).put(0)
nd.strt(length=10, width=4).put(0, 10)
nd.bend(angle=90, radius=10).put(15, 10, -90)

nd.export_gds()

The resulting GDS image:

basic waveguides

3 Using a PDK (process design kit)

In this example we import the demonstration foundry “demofab” that comes with Nazca. Demofab has predefined building blocks (mask elements) and interconnects. Demofab is for playing around and getting you ready for designing in Nazca in any technology. Indeed, the exact same demofab concepts apply to any PDK in Nazca, be it for Silicon, InP, SiN, glass, polymer or others.

This example creates a 1x2 MMI and connects waveguides using chain connections that use expiclit pins names in explicit element references. Interconnects have been predefined in the PDK and here we use the demo.shallow waveguide interconnect object.

By assigning name mmi1 to the MMI, we can refer to any of mmi1 its ports (‘a0’, ‘b0’, ‘b1’) via the Python dictionary mmi1.pin. This comes in very useful when building a circuit:

import nazca as nd
import nazca.demofab as demo

mmi1 = demo.mmi1x2_sh().put()
demo.shallow.strt(length=50).put(mmi1.pin['a0'])
demo.shallow.sbend(offset=20).put(mmi1.pin['b0'])
demo.shallow.sbend(offset=-20).put(mmi1.pin['b1'])

nd.export_gds()

The resulting GDS image:

1x2 MMI with waveguides

4 Creating a new building block and reuse it

In Nazca it is straight forward to create new building blocks and build hierarchical designs with them. First some more explanation on naming conventions: The ‘building block’ in Nazca is referred to as a ‘Cell’ object in the Python code, because it is typically exported as a cell in GDS. Note also that a building block is essentially a more generic name for ‘mask element’ to emphasize it may contain more information than for layout only. A hierarchal design is created by placing building blocks inside other building blocks. The hierarchy in Nazca is directly transferred to the exported GDS file.

This example creates a building block from a 1x2 MMI and three in-out waveguides. Next, the new block is put several times in the mask.

import nazca as nd
import nazca.demofab as demo

with nd.Cell(name='myMMI') as mmi:
    mmi1 = demo.mmi1x2_sh().put()
    demo.shallow.strt(length=50).put(mmi1.pin['a0'])
    demo.shallow.sbend(offset=20).put(mmi1.pin['b0'])
    demo.shallow.sbend(offset=-20).put(mmi1.pin['b1'])

mmi.put(0)
mmi.put(0, 100)
mmi.put(300, 50)

nd.export_gds()

The resulting GDS image:

Create an MMI building block

5. Adding pins to a new building block

A building block needs pins to connect it to the rest of a layout, e.g. interconnects or other building blocks. Remember that in the Nazca philosophy of chain connections, the pins always point outwards from a building block; This avoids a lot of confusion and trial and error when building a circuit. If pins are not explicitly defined in a cell, then default pins ‘a0’ and ‘b0’ are automatically added at the cell origin, with ‘a0’ pointing in the negative x, i.e. (0, 0, 180) and ‘b0’ in the positive x-direction, i.e. (0, 0, 0). By default, a building block is placed with its ‘a0’ port at the indicated position in the put method. To choose another port you can simple place the name of the pin to place the block as first argument in put, e.g. put(‘b0’) to place port ‘b0’ at (0, 0 , 0) or put(‘b0’, 0, 100, 90) to place port ‘b0’ at (0, 100, 90).

import nazca as nd
import nazca.demofab as demo

with nd.Cell(name='myMMI') as mmi:
    mmi1 = demo.mmi1x2_sh().put()
    elm1 = demo.shallow.strt(length=50).put(mmi1.pin['a0'])
    elm2 = demo.shallow.sbend(offset=40).put(mmi1.pin['b0'])
    elm3 = demo.shallow.sbend(offset=-40).put(mmi1.pin['b1'])

    nd.Pin('a0', pin=elm1.pin['b0']).put()
    nd.Pin('b0', pin=elm2.pin['b0']).put()
    nd.Pin('b1', pin=elm3.pin['b0']).put()

mmi.put('a0', 0) # same as mmi.put(0), 'a0' is the default.
mmi.put('b0', 0, 100) # connect pin 'b0' of 'mmi' at (0, 100)

nd.export_gds()

The resulting GDS image:

Create an MMI building block

6. Creating a parametrized building block

In this example constructs a parametrized MMI building block by placing a Cell definition inside a Python function definition. We use the building block containing the MMI with in-out waveguides of the previous example and parametrize the output pitch. Doing so provides a nice way to draw a 1x8 splitter. Additionally, a straight guide of the demo.shallow interconnect type is connected to one of the 1x8 splitter outputs.

import nazca as nd
import nazca.demofab as demo

def mmi(offset=40):
    with nd.Cell(name='myMMI') as mymmi:
        mmi1 = demo.mmi1x2_sh().put()
        elm1 = demo.shallow.strt(length=50).put(mmi1.pin['a0'])
        elm2 = demo.shallow.sbend(offset=offset).put(mmi1.pin['b0'])
        elm3 = demo.shallow.sbend(offset=-offset).put(mmi1.pin['b1'])

        nd.Pin('a0', pin=elm1.pin['b0']).put()
        nd.Pin('b0', pin=elm2.pin['b0']).put()
        nd.Pin('b1', pin=elm3.pin['b0']).put()
    return mymmi

mmi1  = mmi(offset=100).put(0)
mmi2a = mmi(offset=50).put(mmi1.pin['b0'])
mmi2b = mmi(offset=50).put(mmi1.pin['b1'])
mmi3a = mmi(offset=25).put(mmi2a.pin['b0'])
mmi3b = mmi(offset=25).put(mmi2a.pin['b1'])
mmi3c = mmi(offset=25).put(mmi2b.pin['b0'])
mmi3d = mmi(offset=25).put(mmi2b.pin['b1'])
demo.shallow.strt(length=200).put(mmi3c.pin['b0'])

nd.export_gds()

The resulting GDS image:

Create an MMI building block

The elegance of using hierarchy in Nazca becomes even more clear by putting the 1x8 splitter in a cell of its own. As a result, all it takes to place a the 1x8 splitter now reduces to splitter_1x8.put().

import nazca as nd
import nazca.demofab as demo

def mmi(offset=40):
    with nd.Cell(name='myMMI') as mymmi:
        mmi1 = demo.mmi1x2_sh().put()
        elm1 = demo.shallow.strt(length=50).put(mmi1.pin['a0'])
        elm2 = demo.shallow.sbend(offset=offset).put(mmi1.pin['b0'])
        elm3 = demo.shallow.sbend(offset=-offset).put(mmi1.pin['b1'])

        nd.Pin('a0', pin=elm1.pin['b0']).put()
        nd.Pin('b0', pin=elm2.pin['b0']).put()
        nd.Pin('b1', pin=elm3.pin['b0']).put()
    return mymmi

with nd.Cell(name='splitter') as splitter_1x8:
    mmi1  = mmi(offset=100).put(0)
    mmi2a = mmi(offset=50).put(mmi1.pin['b0'])
    mmi2b = mmi(offset=50).put(mmi1.pin['b1'])
    mmi3a = mmi(offset=25).put(mmi2a.pin['b0'])
    mmi3b = mmi(offset=25).put(mmi2a.pin['b1'])
    mmi3c = mmi(offset=25).put(mmi2b.pin['b0'])
    mmi3d = mmi(offset=25).put(mmi2b.pin['b1'])

    nd.Pin('a0', pin=mmi1.pin['a0']).put()
    nd.Pin('b0', pin=mmi3a.pin['b0']).put()
    nd.Pin('b1', pin=mmi3a.pin['b1']).put()
    nd.Pin('b2', pin=mmi3b.pin['b0']).put()
    nd.Pin('b3', pin=mmi3b.pin['b1']).put()
    nd.Pin('b4', pin=mmi3c.pin['b0']).put()
    nd.Pin('b5', pin=mmi3c.pin['b1']).put()
    nd.Pin('b6', pin=mmi3d.pin['b0']).put()
    nd.Pin('b7', pin=mmi3d.pin['b1']).put()

split1x8 = splitter_1x8.put(0)
demo.shallow.strt(length=200).put(split1x8.pin['b4'])

nd.export_gds()

Note that the splitter definition can be put in a separate python file, e.g. splitter.py and the resulting code becomes:

import nazca as nd
import nazca.demofab as demo
import splitter

split1x8 = splitter.splitter_1x8.put(0)
demo.shallow.strt(length=200).put(split1x8.pin['b4'])

Hierarchy

The Nazca hierarchy is extendable as much as you like, e.g. a hierarchy of cells A, B and C like A({B[C, C]}, {B[C, C]}) could look like something in the image below, where each rectangle represents a cell and the bottom-left corner of each rectangle the origin (0, 0) of the cell.

hierarchical design

Using the 1x8 splitter as cell B, the MMI with fanouts as cell C, and realizing that the main mask is cell A, we can do something like the following (assuming you have created the splitter.py module file in the example above):

import nazca as nd
import nazca.demofab as demo
import splitter

split1x8 = splitter_1x8.put(100, 800, -20)
split1x8 = splitter_1x8.put(1400, 400, 20)

nd.export_gds()

and visualize the GDS at different hierarchy levels in KLayout as follows:

hierarchical design 1
hierarchical design 2
hierarchical design 3

7 Creating a parametrized DBR laser

This example creates a DBR laser with 6 pins to connect it to the outside world, i.e. two optical (‘a0’, and ‘b0’) and four electrical pins (‘c0’, ‘c1’, ‘c2’, ‘c3’). There is an example on assigning a parametrized cell to a variable for reuse, i.e. iso = demo.isolation_act(length=20).

import nazca as nd
import nazca.demofab as demo

def dbr_laser(Ldbr1=50, Ldbr2=500, Lsoa=750, Lpm=70):
    """Create a parametrized dbr laser building block."""
    with nd.Cell(name='laser') as laser:
        #create an isolation cell for reuse
        iso = demo.isolation_act(length=20)

        #draw the laser
        s2a   = demo.s2a().put(0)
        dbr1  = demo.dbr(length=Ldbr1).put()
        iso.put()
        soa   = demo.soa(length=Lsoa).put()
        iso.put()
        phase = demo.phase_shifter(length=Lpm).put()
        iso.put()
        dbr2  = demo.dbr(length=Ldbr2).put()
        a2s   = demo.a2s().put()

        # add pins to the laser building block
        nd.Pin('a0', pin=s2a.pin['a0']).put()
        nd.Pin('b0', pin=a2s.pin['b0']).put()
        nd.Pin('c0', pin=dbr1.pin['c0']).put()
        nd.Pin('c1', pin=soa.pin['c0']).put()
        nd.Pin('c2', pin=phase.pin['c0']).put()
        nd.Pin('c3', pin=dbr2.pin['c0']).put()
    return laser

#place several lasers:
laser1 = dbr_laser(Lsoa=750).put(0)
laser2 = dbr_laser(Lsoa=1000).put(0, -300)
laser3 = dbr_laser(Lsoa=500, Ldbr1=20, Ldbr2=800, Lpm=150).put(0, -600)

demo.shallow.bend(angle=-45).put(laser1.pin['b0'])

nd.export_gds()

The resulting GDS image:

Create a laser building block

To make life easy, the DBR laser has been already defined in the demofab PDK, hence, a shorter way to work with lasers is shown below. Here we create a number of laser building blocks, put them in a list and loop over them in a pythonic way to connect electrical bonding pads to each laser. Note that the core idea in Nazca is to create your building blocks, like the laser, verify them and use these blocks to simplify your main layout design.

import nazca as nd
import nazca.demofab as demo

laser1 = demo.dbr_laser(Lsoa=750)
laser2 = demo.dbr_laser(Lsoa=1000)
laser3 = demo.dbr_laser(Lsoa=500, Ldbr1=20, Ldbr2=800, Lpm=150)
laserBBs = [laser1, laser2, laser3]

for j, laser in enumerate(laserBBs):
    demo.shallow.strt(length=100).put(0, 800*j)
    las = laser.put()
    demo.shallow.strt(length=200).put()

    for i, pinname in enumerate(['c0', 'c1', 'c2', 'c3']):
        pad = demo.pad_dc().put(las.pin['a0'].move(-i*250-150, -600, -90))
        demo.metaldc.sbend_p2p(las.pin[pinname], pad.pin['c0'], Lstart=(i+1)*75).put()

nd.export_gds()

The resulting GDS image:

Create a laser building block

8 Creating a parametrized Mach-Zehnder interferometer

This example defines a Mach-Zehnder interferometer cell with the pins to connect it in a layout, i.e optical pins ‘a0’, ‘b0’ and metal pins ‘c0’, ‘c1’, ‘d0’, ‘d2’ The example uses the ‘deep’ waveguide type in demofab. The default input pin is ‘a0’ and the default output pin ‘b0’.

import nazca as nd
import nazca.demofab as demo

def mzi(length=1000):
    with nd.Cell(name='mzi') as mziBB:
        eopm = demo.eopm_dc(length=length, pads=True, sep=40)

        #part 1: place foundry blocks:
        mmi_left  = demo.mmi2x2_dp().put()
        eopm_top  = eopm.put(mmi_left.pin['b0'].move(135, 50))
        eopm_bot  = eopm.put(mmi_left.pin['b1'].move(135, -50), flip=True)
        mmi_right = demo.mmi2x2_dp().put(eopm_top.pin['b0'].move(135, -50))

        #part 2: add waveguide interconnects
        demo.deep.sbend_p2p(mmi_left.pin['b0'], eopm_top.pin['a0']).put()
        demo.deep.sbend_p2p(eopm_top.pin['b0'], mmi_right.pin['a0']).put()
        demo.deep.sbend_p2p(mmi_left.pin['b1'], eopm_bot.pin['a0']).put()
        demo.deep.sbend_p2p(eopm_bot.pin['b0'], mmi_right.pin['a1']).put()

        #part 3: add pins
        nd.Pin('a0', pin=mmi_left.pin['a0']).put()
        nd.Pin('a1', pin=mmi_left.pin['a1']).put()
        nd.Pin('b0', pin=mmi_right.pin['b0']).put()
        nd.Pin('b1', pin=mmi_right.pin['b1']).put()
        nd.Pin('c0', pin=eopm_top.pin['c0']).put()
        nd.Pin('c1', pin=eopm_top.pin['c1']).put()
        nd.Pin('d0', pin=eopm_bot.pin['c0']).put()
        nd.Pin('d1', pin=eopm_bot.pin['c1']).put()

    return mziBB

mzi(length=1000).put()
mzi(length=500).put()
nd.export_gds()

The resulting GDS image:

Create an MZI building block

Since the MZI has been already defined in demofab, a shorter way to obtain similar results is the implementation below. Note that in demofab a bounding box has been added to the MZI to easier identify it in the layout.

import nazca as nd
import nazca.demofab as demo

mzi1 = demo.mzi(length=1000, sep=100).put(0)

demo.deep.sbend(offset=500).put(mzi1.pin['b0'])
mzi2a = demo.mzi(length=500, sep=200).put()

demo.deep.sbend(offset=-500).put(mzi1.pin['b1'])
mzi2b = demo.mzi(length=500, sep=200).put()

nd.export_gds()

The resulting GDS image:

Use an MZI building block

9. Create text

Your PIC design will make more sense with text to identify different parts of your chip. Below is an example of placing text in Nazca. Text is flexible in size, placement, alignment and font. The font in this example is foundry friendly.

import nazca as nd

message = "Nazca, open source Photonic IC design in Python 3!"
for i in range(7):
    T1 = nd.text(text=message, height=70*0.85**i)
    T1.put(0, -i*100)
    T1.put(0, -1200+i*100)

    T2 = nd.text(text=message, height=70*0.85**i, align='rb')
    T2.put(4000, -i*100)
    T2.put(4000, -1200+i*100)

nd.text('NAZCA', height=500, align='cc', layer=2).put(0.5*4000, -600)
nd.export_gds()

The resulting GDS image:

Using text in a design

For more examples, go to the Tutorials.