Welcome to Nazca Design!

Nazca Design is an open source photonic integrated circuit (PIC) design framework for professional mask layout and more.

Nazca is created to solve bottle necks that PIC designers have encountered for over two decades. Nazca builds on design experience starting in the 90s, using modern software solutions. Nazca redefines PIC design from the ground up and introduces within one year of its conception a complete tool with a large number of features and innovations that assist you to create correct mask designs with ease and pleasure. Ultimately, as PIC designer you are responsible for the layouts that you create, hence quality is essential. You probably want to be a creative and efficient designer too. Therefore, Nazca focuses on

  • Flexibility – Python based, open source
  • Efficiency – Clear design concepts and faster than you might expect
  • Quality – Mature PDKs and strong visualization.

Nazca design is made for designers, by designers. It solves design problems in a no-nonsense manner.


PIC mask layout created in Nazca

Mask design of a photonic integrated circuit created in Nazca

In order to design layouts for specific technologies, a designer can greatly benefit from process design kits (PDK). These kits describe the building blocks made available by a foundry in its technology. Nazca is designed with PDKs in mind, and once again, with a focus on practical designer needs and work flow:

  • Easy of implementation
  • Extensive validation
  • Functional visualization

Nazca is highly suitable for the PIC design professional, but also provides a smooth path to get started in PIC design at work, in school or at home. The Nazca philosophy is that photonics layout is simple, and in order to keep it simple, also for complex designs, all basic Nazca functions and concepts have been designed and redesigned to be as simple as possible without losing functionality. It turns out that by defining clear functional scripting syntax with descriptive names, both the beginner and professional designer benefit, as you may expect.

Nazca has chosen a mature and well supported scripting language: Python, with readable syntax and excellent debugging, testing and editor options. With Nacza you get to work with one of the most popular languages, in a fast developing landscape. You may already know Python, as it has been around for 20+ years, or you may find in Nazca a good reason to try it. Either way, scripting in Python is a transferable skill, that can be reused in other fields and projects.

Nazca is open source, and because of that you can create your layout designs anywhere you want, now and in the future. Nazca runs on Windows, Linux and Mac. You can install and run it in your local folder, without the need for admin privileges. Just get started and have fun with this professional tool.


Getting started

For a quick tour and installing Nazca, checkout http://nazca-design.org/quick-tour

Nazca places elements in a layout using a standard Cartesian coordinate system with coordinates (x, y), and it adds a direction with angle a to the coordinate, hence we have (x, y, a). 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.

coordinate system

The Nazca concept is to create a mask element first and put that into the mask. If you do not specify where to put an element it will conveniently connect to the last element put. This is called a “chain connection” in Nazca.

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

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

First of all, import the nazca module with import nazca as nd. Next, the example connects straight and bend waveguide elements from the module using Nazca chain connections; First an element is created, e.g. nd.strt(), and subsequently the put() method places the elements head to tail in the mask. The put() defaults to (0, 0 ,0) at first occurrence. The GDS is created and saved to your work directory by nd.export_gds()

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 be used to 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 additionally import the demonstration foundry “demofab” that comes with Nazca. Demofab has predefined building blocks and interconnects that are technology compliant with a virtual technology. It is for playing around and gets you ready for designing in Nazca in any technology, because the exact same concepts apply to any PDK in Nazca, like Silicon Photonics, InP, SiN, glass and polymers PDKs.

This example creates a 1x2 MMI and connects waveguides using chain connections. Interconnects have been predefined in the PDK and we use the demo.shallow waveguide.

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 very simple to create new building blocks and build hierarchical designs. The hierarchy in Nazca can be directly transferred to the exported GDS file. Nazca calls a building block a ‘Cell’ object, because it will be exported as a cell in GDS.

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. Note 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 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)

nd.export_gds()

The resulting GDS image:

Create an MMI building block

6. Creating a parametrized building block

In this example a parametrized MMI building block is constructed 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 the Nazca hierarchy 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 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

Indices and tables