Welcome to the SPiRA Framework!

Parameterized Cells (PCells) are key components used to increase flexibility and productivity during layout design. Creating a parameterized cell integrates all design information into one place. The development and maintenance of PCell libraries typically requires device knowledge and advanced programming skills.

Integrated circuit design requires many different levels of analysis. A lot of component design is done using a manual design flow, from SPICE simulations, to parameter extraction. While at the same time, circuit design requires abstraction at a much higher level.

SPiRA is a parametric design framework for Superconductor and Quantum Integrated Circuit (SQIC) design. It revolves around the sound engineering concept that creating circuit layouts are prone to unexpected design errors. The design process is highly dependent on data provided by the fabrication process. In SPiRA, parameterized cells can be generated that interactively binds data from any fabrication process. A novel design method is introduced, called validate-by-design, that integrates parameter restrictions into the design flow, which limits a designer from breaking process design rules.

SPiRA integrates different aspects into a single framework, where a parameterized cell can be defined once and then used throughout the design process, performing automatic device detection, netlist extraction, and design rule checking. Consequently, errors are significantly reduced by using the same component definition throughout the entire design flow.

Lore

Spira is the fictional world of the Square role-playing video game Final Fantasy X. The name Spira refers to the word “spiral”, alluding to one of the main themes of Final Fantasy X: continuity. The word “spiral” has multiple meanings depending on context. In geometry, it is a curve acting as the focus of a point rotating around a fixed point that continuously extends from that point. The inspiration behind the fictional Spira world, according to the producer Yoshinori Kitase, is the heuristic that players prefer a “simple fantasy world” over a more science fiction world.

The reason for naming this framework SPiRA:

The aim is to develop a continuous design environment for IC engineers to design circuit layouts, with focus falling on a simple script-based methodology. The SPiRA core is the focus point that revolves around the single idea of parameterizing layout geometries.

Basic termnology before getting started:

  1. PCell (Parameterized Cell): Also refered to as a layout generator is a cell that defines how layout elements must be generated based on a given set of parameters.
  2. RDD (Rule Deck Database): A novel script-based approach to develop a Process Design Kit (PDK).

Methodology

Using general mathematics and science as an analogy, a basic understanding can be constructed of the importance of software in IC design: Mathematics is the discipline of proving provable statements true and science is the discipline of proving provable statements false.

The Importance of Software in IC Design

In software development, testing shows the presence of bugs and not the absence thereof. Software development is not a mathematical endeavour, even though it seems to manipulate mathematical constructs. Rather, software is more connected to science in the sense that it shows correctness by failing to show incorrectness. Therefore, IC design is to some extent only as good as the software systems that can prove the design incorrect. Circuit verification software only shows the engineer where mistakes are made, if any.

Structured programming elicits a design architect that recursively decomposes a program into a set of small provable functions. Unit tests can be used to try and prove these small provable functions incorrect. If such tests fail to prove incorrectness, then the functions are deemed to be correct enough for all intents and purposes. Superconducting Electronic Design Automation (S-EDA) tools from a macro view can be thought of as a set of unit tests for a hardware design engineer to stress test their circuit layouts.

The Development Psychology of SPiRA

The development history of SPiRA can be broken down into four phases. First, a rudimentary version that consisted of a collection of scripts. Second, a small project that assimilated a minimum set of functions and class objects. Third, a large systemic project with multiple coherently connected modules. Finally, a meticulously designed framework that uses special coding techniques to create dynamic binding, software pattern implementation, data abstraction and custom classes.

Before understanding the reasoning behind developing a parameterized framework for physical design verification it is imporant to first categorize the type of problem we are dealing with:

Deductive Logic

Deductive logic implies applying general rules which hold over the entirety of a closed domain of discourse: Software solutions that will most likely not change in the near future, tend to solve problems that are implicit in the technology. The reading and parsing of GDSII layouts, and geometry modeling, are examples implemented, through solutions that use deductive reasoning.

Inductive Logic

Inductive logic is inherently uncertain; the results concluded from inductive reasoning are more nuanced than simply stating; if this, then that. Consequently, heuristics can be derived using inductive reasoning to develop a sufficient SDE verification tool: Implementing fabrication design rules must be done following inductive logic, since future technology changes in the superconductor electronics field are still speculative. For instance, currently there is no specific PDK setup for superconductor electronics, and consequently, it can either be assumed that the future PDK versions will be similar to that of semiconducting, or that it will prevail and create a new set of standards. Also, the rapid changes being made in the semiconductor field add to the uncertainty of future metadata in the field of superconductivity.

The symmetries between the superconductor and the semiconductor field are not necessarily indicative of the future evolution of superconductor electronics. Therefore, a paradigmatic software system (not just a solution) has to be developed, which can accommodate for dynamic “meta-changes”, while still being extendible and maintainable. Physical design verification is highly dependent on the modelling of design rules. Layout generators allows us to effectively create our own programmable circuit models. Only minor differences exist between different fabrication processes, which enables us to develop a general software verification package by focusing on creating solution heuristics, through inductive models, rather than trying to develop concrete deductive solutions.

Metaprogramming

Technical sophistication can, at some level, cause degradation. Metaprogramming forms the foundation of the SPiRA framework and therefore it becomes apparent to start with a more general explanation of a metamodel. A model is an abstraction of a real-world phenomena. A metamodel is an even higher level of abstraction, which coalesces the properties of the original model. A model conforms to its metamodel like a human conforms its understanding to the sophistication of its internal dictionary (language)—or lack thereof. Metamodeling is the construction of a collection of concepts within a certain domain. Metamodeling involves defining the output and input relationships and then fitting the correct metamodels to represent the desired behaviour. Analogously, binding generic data to a layout has to be done by developing a metamodel of the system. The inherit purpose of the base metaclasses in the SPiRA framework is to bind data to a class object depending on the class composition. This has to be done after defining the class constituents (parameters), but before class creation. Hence, the use of metaclasses. In Python a class is an object that can be dynamically manipulated. Therefore, constraints have to be implemented on the class so as to overcome information overloading. In order to constrain the class, it has to be known which data to filter and which to process, which must then be added to the class as attributes. In doing this, the concept evolves that the accepted data itself can be restricted to a specific domain. This is the core principle behind the validate-by-design method

Overview

This overview discusses the basic constituents of the SPiRA framework. This includes how data from the fabrication process is connected to the design environment, the basic design template for creating parameterized cells, and how different layout elements are defined.

Process Design Kit

The process design kit (PDK) is a set of technology files needed to implement the physical aspects of a layout design. Application-specific rules specified in the PDK controls how physical design applications work.

A new PDK scheme is introduced. The Python programming language is used to bind PDK data to a set of classes, called data trees, that uniquely categorises PDK data. This new PDK scheme is called the Rule Deck Database (RDD), also refered to as the Rule Design Database. By having a native PDK in Python it becomes possible to use methods from the SPiRA framework to create a more descriptive PDK database. A design process typically contains the following aspects:

  • GDSII Data: Contains general settings required by the GDSII library, such as grid size.
  • Process Data: Contains the process layers, layer purposes, layer parameters, and layer mappings.
  • Virtual Modelling: Define derived layers that describes layer boolean operations.
Initialization

All caps are used to represent the RDD syntax. The reason being to make the script structure clearly distinguishable from the rest of the framework source code. First, the RDD object is initialized, followed by the process name and description, and then the GDSII related variables are defined.

RDD.GDSII = ParameterDatabase()
RDD.GDSII.UNIT = 1e-6
RDD.GDSII.GRID = 1e-12
RDD.GDSII.PRECISION = 1e-9
Process Data

Process data relates to data provided by a specific fabrication technology.

Process Layer

The first step in creating a layer is to define the process step that it represents in mask fabrication. The layer process defines a specific fabrciation function, for examples metalization. There can be multiple different drawing layers for a single process. A process database object is created that contains all the different process steps in a specific fabrication process:

RDD.PROCESS = ProcessLayerDatabase()

RDD.PROCESS.R1 = ProcessLayer(name='Resistor 1', symbol='R1')
RDD.PROCESS.M1 = ProcessLayer(name='Metal 1', symbol='M1')
RDD.PROCESS.C1 = ProcessLayer(name='Contact 1', symbol='C1')

Each process has a name that describes the process function, and a symbol that is used to identify the process.

Purpose Layer

The purpose indicates the use of the layer. Multiple layers with the same process but different purposes can be created. Similar to a process value each purpose contains a name and a unique symbol. Purposes are defined using a purpose database object:

RDD.PURPOSE = PurposeLayerDatabase()

RDD.PURPOSE.METAL = PurposeLayer(name='Polygon metals', symbol='METAL')
RDD.PURPOSE.VIA = PurposeLayer(name='Contact', symbol='VIA')
Process Parameters

Parameters are added to a process by creating a parameter database object that has a key value equal to the symbol of a pre-defined process:

RDD.M1 = ParameterDatabase()
RDD.M1.MIN_SIZE = 0.7
RDD.M1.MAX_WIDTH = 20.0
RDD.M1.J5_MIN_SURROUND = 0.5
RDD.M1.MIN_SURROUND_OF_I5 = 0.5

Any number of variables can be added to the database using the dot operator. The code above defines a set of design parameters for the M1 process.

Physical Layers

Physical Layers are unique to SPiRA and is defined as a layer that has a defined process and purpose. A physical layer (PLayer) defines the different purposes that a single process can be used for in a layout design.

RDD.PLAYER.M1 = PhysicalLayerDatabase()
RDD.PLAYER.C1 = PhysicalLayerDatabase()

RDD.PLAYER.C1.VIA = PhysicalLayer(process=RDD.PROCESS.C1, purpose=RDD.PURPOSE.VIA)
RDD.PLAYER.M1.METAL = PhysicalLayer(process=RDD.PROCESS.M1, purpose=RDD.PURPOSE.METAL)

The code above illustrates that the layer M1 is a metal layer on process M1, and layer C1 is a contact via on process C1.

Virtual Modelling

Derived Layers are used to define different PLayer boolean operations. They are typically used for virtual modelling and polygon operations, such as merged polygons or polygon holes.

RDD.PLAYER.M1.EDGE_CONNECTED = RDD.PLAYER.M1.METAL & RDD.PLAYER.M1.OUTSIDE_EDGE_DISABLED

The code above defines a derived layer that is generated when a layer with process M1 and purpose metal overlaps the outside edges of a M1 layer.

Parameters

Designing a generated layout requires modeling its parameters. To create an effective design environment it becomes paramount to define parameter restrictions. SPiRA uses a meta-configuration to define object parameters, which enables the following features:

  • Default values can be set to each parameter.
  • Documentation for each parameter can be added.
  • Parameters can be cached to ensure they aren’t calculated multiple times.
Introduction

Parameters are derived from the spira.Parameter class. The ParameterInitializer is responsible for storing the parameters of an instance. To define parameters the class has to inherit from the ParameterInitializer class. The following code creates a layer object with a number parameter.

import spira.all as spira
class Layer(spira.ParameterInitializer):
    number = spira.Parameter()

>>> layer = Layer(number=9)
>>> layer.number
9

At first glance this may not seem to add any value that Python by default does not already adds. The same example can be generated using native Python:

class Layer(object):
    def __init__(self, number=0):
        self.number = number

The true value of the parameterized framework becomes clear when adding attributes to the parameter, such as the default value, restrictions, preprocess and doc. These attributes allow a parameter to be type-checked and documented.

import spira.all as spira
class Layer(spira.ParameterInitializer):
    number = spira.Parameter(default=0,
                             restrictions=spira.INTEGER,
                             preprocess=spira.ProcessorInt(),
                             doc='Advanced parameter.')

The newly defined parameter has more advanced features that makes for a more powerful design framework:

# The default value of the parameter is 0.
>>> layer = Layer()
>>> layer.number
0

# The parameter can be updated with an integer.
>>> layer.number = 9
>>> layer.number
9

# The string can be preprocessed to an interger.
>>> layer.number = '8'
>>> layer.number
8

# The string cannot be preprocessed and throws an error.
>>> layer.number = 'Hi'
ValueError: invalid literal for int() with base 10: 'Hi'
Default

When defining a parameter the default value can be explicitly set using the default attribute. This is a simple method of declaring your parameter. For more complex functionality the default function attribute, fdef_name, can be used. This attribute defines the name of a class method that will be used to derive the default value of the parameter. Advantages of this implementation is:

  • Logic operations: The default value can be derived from other defined parameters.
  • Inheritance: The default value can be overwritten using class inheritance.
import spira.all as spira
class Layer(spira.ParameterInitializer):
    number = spira.Parameter(default=0)
    datatype = spira.Parameter(fdef_name='create_datatype')

    def create_datatype(self):
        return 2 + 3

>>> layer = Layer()
>>> (layer.number, layer.datatype)
(0, 5)
Restrictions

The validity of a parameter value is calculated by the restriction attribute. In certain cases we want to restrict a parameter value to a certain type or range of values, for example:

  • Validate that the value has a specific type, such as a via PCell.
  • Validate that the value falls between a specified minimum and maximum.
import spira.all as spira
class Layer(spira.ParameterInitializer):
    number = spira.Parameter(default=0, restrictions=spira.RestrictRange(2,5))

The example above restricts the number parameter of the layer to be between 2 and 5:

>>> layer = Layer()
>>> layer.number = 3
3
>>> layer.number = 1
ValueError: Invalid parameter assignment 'number' of cell 'Layer' with value '1', which is not compatible with 'Range Restriction: [2, 5)'.
Preprocessors

The preprocess attribute converts a received value before assigning it to the parameter. Preprocessors are typically used to convert a value of invalid type to one of a valid type, such as converting a float to an integer.

import spira.all as spira
class Layer(spira.ParameterInitializer):
    number = spira.Parameter(default=0, preprocess=spira.ProcessorInt())

>>> layer = Layer()
>>> layer.number = 1
1
>>> layer.number = 2.1
2
>>> layer.number = 'Hi'
ValueError: invalid literal for int() with base 10: 'Hi'
Documentation

Documentation can be added to the parameter using the doc attribute. The created class can also be documented using triple qoutation marks.

import spira.all as spira
class Layer(spira.ParameterInitializer):
    """ This is a layer class. """
    number = spira.Parameter(default=0, doc='Parameter documentation.')

>>> layer = Layer()
>>> layer.number
0
>>> layer.__doc__
This is a layer class.
>>> layer.number.__doc__
Parameter documentation.
Cache

SPiRA automatically caches parameters once they have been initialized. When using class methods to define default parameters using the fdef_name attribute, the value is stored when called for the first time. Calling this value for the second time will not lead to a re-calculation, but rather the value will be retrieved from the cached dictionary. The cache is automatically cleared when any parameter in the instance is updated, since other parameters might be dependent on the changed parameters.

Parameterized Cells

GDSII layouts encapsulate element design in the visual domain. Parameterized cells encapsulates elements in the programming domain, and utilizes this domain to map external data to elements. This external data can be data from the PDK or values extracted from an already designed layout using simulation software, such as InductEx. The SPiRA framework uses a scripting framework approach to connect the visual domain with a programming domain. The implemented architecture of SPiRA mimics the physical layout patterns implicit in hand-designed layouts. This framework architecture evolved by developing code heuristics that emerged from the process of creating a PCell.

Creating a PCell is done by defining the elements and parameters required to create the desired layout. The relationship between the elements and parameters are described in a template format. Template design is an innate feature of parameterizing cell layouts. This heuristic concludes to develop a framework to effectively describe the different constituents of a PCell, rather than developing an API. The SPiRA framework was built from the following concepts:

1. Defining Element Shapes This step defines the geometrical shapes from which an element polygon is generated. The supported shapes are rectangles, triangles, circles, as well as regular and irregular polygons. Each of these shapes has a set of parameters that control the pattern dimensions, e.g. the parameterized rectangle has two parameters, width and length, that defines its length and width, respectively.

2. Element Shape Transformations This step describes the relation between the elements through a set of operations, that includes transformations of a shape in the x-y plane. Transforming an element involves: movement with a specific offset relative to its original location, rotation of a shape around its center with a specific angle, reflection of a shape around a idefined line, and aligning a shape to another shape with a specific offset and angle.

3. PDK Binding The final step is binding data from the PDK to each created pattern. In SPiRA, process related data is defined in the RDD. From this database the required data can be linked to any specific pattern by defining parameters and their design restrictions.

Shapes

A shape is a basic 2-dimentional geometric pattern that consists of a list of points. These points can be manipulated and transformed as required by the designer, before commiting it to a layout cell.

class ShapeExample(spira.Cell):

    def create_points(self, points):
        points = [[0, 0], [2, 2], [2, 6], [-6, 6], [-6, -6], [-4, -4], [-4, 4], [0, 4]]
        return points

You can create your own shape by creating a class that inherits from spira.Shape. The shape coordinates are calculated by the create_points class method that is innate to any spira.Shape derived instance. The spira.Shape class offers a rich set of methods for basic and advanced shape manipulation:

>>> shape = ShapeExample()
>>> shape.points
[[0, 0], [2, 2], [2, 6], [-6, 6], [-6, -6], [-4, -4], [-4, 4], [0, 4]]
>>> shape.area
88
>>> shape.move((10, 0))
[[10, 0], [12, 2], [12, 6], [4, 6], [4, -6], [6, -4], [6, 4], [10, 4]]
>>> shape.x_coords
[10 12 12  4  4  6  6 10]
Elements

The purpose of elements are to wrap geometry data with GDSII layout data. In SPiRA the following elements are defined:

  • Polygon: Connects a shape object with layout data (layer number, datatype).
  • Label: Generates text data in a GDSII layout.
  • SRef: A structure references, or sometimes called a cell reference, refers to another cell object, but with difference transformations.

There are other special objects, called element groups that can be used in the design environment. These objects are mainly a combination of polygons and relations between polygons. These special objects are referenced as if they represent a single shape, and its outline is determined by its bounding box dimensions. The following element groups are defined in the SPiRA framework:

  • Cells: Is the most generic group that binds different parameterized elements or clusters, while conserving the geometrical relations between these polygons or clusters.
  • Group: A set of elements can be grouped in a logical container.
  • Ports: A port is simply a polygon with a label on a dedicated process layer. Typically, port elements are placed on conducting metal layers.
  • Routes: A route is defined as a cell that consists of a polygon element and a set of edge ports, that resembles a path-like structure.

The SPiRA design environment for creating a PCEll is broken down into the following basic templated steps:

class PCell(spira.Cell):
    """ My first parameterized cell. """

    # Define parameters here
    number = spira.IntegerParameter(default=0, doc='Parameter example number.')

    def create_elements(self, elems):
        # Define elements here.
        return elems

    def create_ports(self, ports):
        # Define ports here.
        return ports

The most basic SPiRA template to generate a PCell is shown above, and consists of three parts:

  1. Create a new cell by inheriting from spira.Cell. This connects the class to the SPiRA framework when constructed.
  2. Define the PCell parameters as class attributes.
  3. Elements and ports are defined in the create_elements and create_ports class methods, which is automatically added to the cell instance. The create methods are special SPiRA class methods that specify how the parameters are used to create the cell.
class PolygonExample(spira.Cell):

    def create_elements(self, elems):
        pts = [[0, 0], [2, 2], [2, 6], [-6, 6], [-6, -6], [-4, -4], [-4, 4], [0, 4]]
        shape = spira.Shape(points=pts)
        elems += spira.Polygon(shape=shape, layer=spira.Layer(1))
        return elems

>>> D = PolygonExample()
>>> D.gdsii_output()
_images/_elements.png

The code above illustrates the creation of a polygon object, using the already defined shape. The polygon object connects the shape to a GDSII library with a GDSII layer number equal to \(1\). Once the polygon has been created it can be added to the cell instance using the + operator to increment the elems list.

Group

Groups are used to apply an operation on a set of polygons, such a retrieving their combined bounding box. The following example illistrated the use of spira.Group to generate a metal bounding box around a set of polygons:

class GroupExample(spira.Cell):

    def create_elements(self, elems):

        group = spira.Group()
        group += spira.Rectangle(p1=(0,0), p2=(10,10), layer=spira.Layer(1))
        group += spira.Rectangle(p1=(0,15), p2=(10,30), layer=spira.Layer(1))

        elems += group

        bbox_shape = group.bbox_info.bounding_box(margin=1)
        elems += spira.Polygon(shape=bbox_shape, layer=spira.Layer(2))

        return elems
_images/_group.png

A group polygon is created around the two defined polygons with a marginal offset of 1 micrometer.

Ports

Port objects are unique to the SPiRA framework and are mainly used for connection purposes.

class Box(spira.Cell):

    width = spira.NumberParameter(default=1)
    height = spira.NumberParameter(default=1)
    layer = spira.LayerParameter(default=spira.Layer(1))

    def create_elements(self, elems):
        shape = shapes.BoxShape(width=self.width, height=self.height)
        elems += spira.Polygon(shape=shape, layer=self.layer)
        return elems

    def create_ports(self, ports):
        ports += spira.Port(name='P1_M1', midpoint=(-0.5,0), orientation=180, width=1)
        ports += spira.Port(name='P2_M1', midpoint=(0.5,0), orientation=0, width=1)
        return ports
>>> box = Box()
[SPiRA: Cell] (name ’Box ’, width 1, height 1, number 0, datatype 0)
>>> box.width
1
>>> box. height
1
>>> box. gds_layer
[SPiRA Layer] (name ’’, number 0, datatype 0)
>>> box.gdsii_output(name='Ports')
_images/_ports.png

The above example illustrates constructing a parameterized box using the proposed framework: First, defining the parameters that the user would want to change when creating a box instance. Here, three parameter are given namely, the width, the height and the layer properties for GDSII construction. Second, a shape is generated from the defined parameters using the shape module. Third, this box shape is added as a polygon element to the cell instance. This polygon takes the shape and connects it to a set of methods responsible for converting it to a GDSII element. Fourth, two terminal ports are added to the left and right edges of the box, with their directions pointing away from the polygon interior.

Routes

Most of the times in designing digital electronic circuit layouts it is required to define metal polygon connections between different devices. Defining the exact points connecting different devices can become a tedious task. Routes are polygon classes that automatically generates a polygon path between different devices. As previously explained, ports are used to define connection points to a cell instance. Therefore, routes can be defined as a polygon that connects to two ports through a path-dependent algorithm. SPiRA offers a variety of different route algorithms that can be generated depending on the relative port positions and the user requirements.

class RouteExample(spira.Cell):

    def create_elements(self, elems):
        elems += spira.RouteManhattan(ports=self.ports, layer=spira.Layer(1))
        return elems

    def create_ports(self, ports):
        ports += spira.Port(name='P1', midpoint=(0,0), orientation=180)
        ports += spira.Port(name='P2', midpoint=(20,10), orientation=0)
        return ports
_images/_routes.png

Filters

Filters are algorithms whos state can be toggled (enabled or disabled). These algorithms are typically used to add or remove extra information to an already working design, hence the name filter.

Boolean

Instead of individually looping through the entire tree hierarchy of a layout to apply boolean operations on all polygons, a boolean filter can be used to automate this process.

Layer

Sometimes we want to filter certain layers, since they only serve a temporary purpose, or because we only want to view layers in the design, for example a specific purpose type. In these cases we can use the layer filter to automatically filter certain layers in a cell.

Netlist

The netlist extraction algorithm consists of a chain of filtering methods. The basic algorithmic steps is divided into two categories:

  1. Extracting a netlist for each individual metal polygon.
  2. Chaining the metal netlists into a single mask netlist.

For each of these steps there is a chain of filter algorithms applied to ensure the correct extraction:

Polygon Netlist
  • Label all nodes in the netlist to the metal layer they represent.
  • Label the nodes that are represetative of detected devices.
  • Label the nodes that represents ERC connections between differenct metal polygons.
  • Calcaulte the cross-over nodes and determine the individual inductive branches.
Mask Netlist
  • Combine all metal netlists into a single netlist domain and connect shared nodes.
  • Calculate individual branches between device nodes.
  • Calculate cross-over nodes between different branches.
  • Recalcalate individual branches which includes the detected cross-over nodes.
  • Collapse all nodes belonging to the same branch into a single node representation.

RDD Advanced

The goal of the advanced RDD tutorial is to discuss:

  • How to define filters.
  • How to define derived layers.
  • How to create a LVS database.
Filters

Filters leverages the chain of responsiblity design pattern to chain a number of algorithms that has to be executed in a sequential order on a specific layout object.

# First we create a filters database.
RDD.FILTERS = ParameterDatabase()

class PCellFilterDatabase(LazyDatabase):
    """ Define the filters that will be used when creating a spira.PCell object. """

    def initialize(self):
        from spira.yevon import filters

        f = filters.ToggledCompositeFilter(filters=[])
        f += filters.ProcessBooleanFilter(name='boolean', metal_purpose=RDD.PURPOSE.DEVICE_METAL)
        f += filters.SimplifyFilter(name='simplify')
        f += filters.ContactAttachFilter(name='contact_attach')

        f['boolean'] = True
        f['simplify'] = True
        f['contact_attach'] = True

        self.DEVICE = f

        f = filters.ToggledCompositeFilter(filters=[])
        f += filters.ProcessBooleanFilter(name='boolean', metal_purpose=RDD.PURPOSE.CIRCUIT_METAL)
        f += filters.SimplifyFilter(name='simplify')

        f['boolean'] = True
        f['simplify'] = True

        self.CIRCUIT = f

        f = filters.ToggledCompositeFilter(name='mask_filters', filters=[])
        f += filters.ElectricalAttachFilter(name='erc')
        f += filters.PinAttachFilter(name='pin_attach')
        f += filters.DeviceMetalFilter(name='device_metal')

        f['erc'] = True
        f['pin_attach'] = True
        f['device_metal'] = False

        self.MASK = f

RDD.FILTERS.PCELL = PCellFilterDatabase()

The code above shows the creation of three composite filter algorithms:

  • The device filters will only be applied on detected device cells.
  • The circuit filters will only be aplied on non-device cell.
  • The mask filters will be executed on the top-level layout cell.

The PCell filter class inherits from the LazyDatabase class to delay its construction. Therefore, the PCell filter database is only instantiated when a specific filter is called using the dot operator as shown below:

f = RDD.FILTERS.PCELL.DEVICE
Derived Layers

Defining derived layers forms the basis of creating the LVS database, since derived layers almost by definition defines via connections.

RDD.VIAS.C5R = ParameterDatabase()

RDD.VIAS.C5R.LAYER_STACK = {
    'BOT_LAYER' : RDD.PLAYER.R5.METAL,
    'TOP_LAYER' : RDD.PLAYER.M6.METAL,
    'VIA_LAYER' : RDD.PLAYER.C5R.VIA
}
RDD.PLAYER.C5R.CLAYER_CONTACT = RDD.PLAYER.R5.METAL & RDD.PLAYER.M6.METAL & RDD.PLAYER.C5R.VIA
RDD.PLAYER.C5R.CLAYER_M1 = RDD.PLAYER.R5.METAL ^ RDD.PLAYER.C5R.VIA
RDD.PLAYER.C5R.CLAYER_M2 = RDD.PLAYER.M6.METAL ^ RDD.PLAYER.C5R.VIA

class C5R_PCELL_Database(LazyDatabase):
    def initialize(self):
        from ..devices.via import ViaC5RA, ViaC5RS
        self.DEFAULT = ViaC5RA
        self.STANDARD = ViaC5RS

RDD.VIAS.C5R.PCELLS = C5R_PCELL_Database()

The example above defines a C5R via which connects layer M6 to R5 through a contact layer C5R. The logic steps for creating this coding snippet is as follow:

  1. A layer stack is created to defined the top, bottom, and via layers.
  2. The derived layers are create that specifies the boolean operations between different layers required in order to detect a via connection. Note, that the RDD.PLAYER.C5R.CLAYER_CONTACT is the via derived layer that specifies the via connection, while the other two derived layers is used for debugging purposes.
  3. A set of different via PCell classes is added to the database. These classes will be constructed and used during the device detection run.
LVS Database

Defining devices in the LVS database is done similarly to defining vias as already explained:

RDD.DEVICES = ParameterDatabase()

RDD.DEVICES.JUNCTION = ParameterDatabase()

class Junction_PCELL_Database(LazyDatabase):
    def initialize(self):
        from ..devices.junction import Junction
        self.DEFAULT = Junction

RDD.DEVICES.JUNCTION.PCELLS = Junction_PCELL_Database()

A Josephson junction device is added to the LVS database by importing an already defined PCell class and can be constucted using the dot operator:

# Create a JTL instance from the definition in the RDD LVS database.
JtlPCell = RDD.DEVICES.JUNCTION.PCELLS.DEFAULT()

# View the created instance.
JtlPCell.gdsii_view()

Basic Tutorial

This tutorial consists of a set of examples that will guide you on how to create a basic PCell, manipulate layout elements, and connect process data to your design.

Parameterized Cell

First, we have to understand the basic PCell template structure innate to any SPiRA design environment.

Demonstrates
  • How to create a parameterized cell by inheriting form spira.PCell.
  • How to add parameters to the cell.
  • How to validate received parameters.

The first step in any SPiRA design environment is to import the framework:

import spira.all as spira

The spira namespace contains all the important functions and classes provided by the framework. In order to create a layout cell all classes has to inherit from spira.PCell:

class Resistor(spira.PCell):
    """ My first parameterized resistor cell. """

The spira.PCell class connects the design to the SPiRA core. In the exampe above we created a parameterized cell of type Resistor and a basic description given in qoutation marks. Now that a layout class has been constructed we need to define a set of parameters that will describe relations between layout elements.

class Resistor(spira.PCell):
    """ My first parameterized resistor cell. """

    width = spira.FloatParameter(default=0.3, doc='Width of the shunt resistance.')
    length = spira.FloatParameter(default=1.0, doc='Length of the shunt resistance.')

We defined two float restricted parameters, the width and the length of the resistor, along with documentation (using the doc attribute) and a default value equal to \(0.3\) and \(1.0\), respectively.

class Resistor(spira.PCell):
    """ My first parameterized resistor cell. """

    width = spira.FloatParameter(default=0.3, doc='Width of the shunt resistance.')
    length = spira.FloatParameter(default=1.0, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

By definition we want to make sure the length of of the resistor is larger than the width. To check the validity of the parameters in relation to eachother, we can use the validate_parameters() method: This method consists of a series of if-statements that checks whether the defined parameters are valid or not after instantiation.

# 1. First create an instance of the resistor class.
>>> D = Resistor()

# 2. You van view the default values of the parameters.
>>> (D.width, D.length)
(0.3, 1.0)

# 3. The parameter is successfully updated if it is valid.
>>> D.width = 0.5
>>> (D.width, D.length)
(0.5, 1.0)

# 4. If an invalid value is received, an error is thrown.
>>> D.width = 1.1
ValueError: `Width` cannot be larger than `length`.

Connecting Process Data

Now that we have created a basic PCell and understand how to define parameters, we want to connect data from the fabrication process to these parameters.

Demonstrates
  • How to connect fabrication process data to a design.
  • How to change to a different fabrication process.

The Rule Deck Database is a set of Python scripting files that contains all the required fabrication process data, and is accessed using the RDD module. SPiRA contains a default process that can be used directly from the spira namespace:

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

We updated the parameter default values to equal that of the minimum design restrictions defined by the process for the resistor layer R1. After having imported the spira namespace the default process database can be changed by importing the desired RDD object.

import spira.all as spira
from spira.technologies.mit.process.database import RDD

>>> RDD
<RDD MiTLL>

Creating Elements

Next, we want to create geometric shapes based on the received instance parameters, before adding them to the cell instance as element objects.

Demonstrates
  • How to add elements to a cell instance.
  • How to create a shape geometry.
  • How to create a GDSII polygon from a shape.

The create_elements class method is a unique SPiRA method that automatically connects a list of elements to the class instance. Methods that starts with create_ are special methods in SPiRA and are called create methods.

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_elements(self, elems):
        w, l = self.width, self.length
        shape = spira.Shape(points=[[0,0], [l,0], [l,w], [0,w]])
        elems += spira.Polygon(shape=shape, layer=spira.RDD.PLAYER.R1.METAL)
        return elems
_images/_3_layout.png

The defined parameters are used to create a geometeric shape inside the create_elements method. Once the shape is defined it can be added to the layout as a polygon element. The purpose of a polygon is to add GDSII-related data to the defined shape.

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_elements(self, elems):
        elems += spira.Box(width=self.length, height=self.width, layer=spira.RDD.PLAYER.R1.METAL)
        return elems

Instead of manually creating shapes SPiRA offers a set of predefined polygons that can be used. The code snippet above illustrates the use of the spira.Box() polygon instead of creating a shape object and sending it the polygon container.

Creating Ports

Similar to the create_elements method that connects element to your cell instance, the create_ports method adds ports to your design. A port is defined as a vector object that is used to connect different layout elements.

Demonstrates
  • How to connect ports to your layout.
  • How to name and connect a process type to your port.
  • How to unlock edge specific ports.

Ports are used to connect different layout elements, such as routing different device cells via a metal polygon. Therefore, defining the port position, its orientation, and to what process layer is connects are extremely important. These are some of the most commonly used port parameters:

  • name The name of the port.
  • midpoint The position of the port.
  • orientation The direction of the port.
  • width The width of the port.
  • process The process to which the port object connects.

In the example below we first define a box polygon and then add ports to the left and right edges of the shape.

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_elements(self, elems):
        elems += spira.Box(width=self.length, height=self.width, center=(0,0), layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        w, l = self.width, self.length
        ports += spira.Port(name='P1_R1', midpoint=(-l/2,0), orientation=180, width=self.width)
        ports += spira.Port(name='P2', midpoint=(l/2,0), orientation=0, width=self.width, process=spira.RDD.PROCESS.R1)
        return ports
_images/_4_ports_0_enabled.png

Port names has to contain one of the following formats:

Pname_Process
The first letter is defines the purpose of the port followed by the port name, typically a number. After the underscore character the process symbol is added (as defined in the RDD). This port naming convention is used when no process parameter is added to the object, as shown in the example above with port P1_R1. This process symbol are compared to the defined processes in the RDD and automatically updates the process parameter of the port instance.
Pname
As shown with P2 the port name does not have to contain the process symbol if a process parameter is manually added to the creation of a port instance.

The most important port purposes for PCell creation are:

  • P (PinPort): The default port used as a terminal to horizontally connect different elements.
  • E (EdgePort): Ports that are automatically generated from the edges of metal polygons.
  • D (DummyPort): Typically used to snap a one side of a route object to a specific position.
class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_elements(self, elems):
        elems += spira.Box(alias='ply1', width=self.length, height=self.width, center=(0,0), layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        # Process symbol will automatically be added to the port name.
        ports += self.elements['ply1'].ports['E1_R1'].copy(name='P1')
        ports += self.elements['ply1'].ports['E3_R1'].copy(name='P2')
        return ports

Defining the exact midpoint of a port required knowledge of the boundary of the shape we want to connect to. SPiRA automatically generates edge ports for metal polygons. The generated box element is given an alias that is used to access that specific element. These edges can be activated as ports by simply changing the port name. The example above illustrates changing edge port E1_R1 to port P1.

_images/_4_ports_0.png

The image bove depicts the automatically generated edge ports that can be used for identifying which edges to convert to active port. In this example we are converting edges, E1_R1 and E3_R1, to ports P1_R1 and P2_R1, respectively. Note, that even though we only added P1 as the port name, the process symbol to which the port belongs are automatically added by the SPiRA framework, since the process parameter is already set within the edge port. The end result is shown in the figure below:

_images/_4_ports_1.png

Creating Routes

Generally metal polygons are used to connect different circuit devices. In this example we first define two ports and then generate a metal polygon between them using the spira.Route base class. SPiRA offers a variaty of different routing algorithm depending on the relative position between ports.

Demonstrates
  • How to create a route between two different ports.
  • How to externally cache parameters.

First, we define the ports as two separate parameters, p1 and p2. Second, we use create methods to generate port parameters. Doing so allows us to access the ports in both create_elements and create_ports without re-calculating the ports.

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    p1 = spira.Parameter(fdef_name='create_p1')
    p2 = spira.Parameter(fdef_name='create_p2')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_p1(self):
        return spira.Port(name='P1', midpoint=(-self.length/2,0), orientation=180, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_p2(self):
        return spira.Port(name='P2', midpoint=(self.length/2,0), orientation=0, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_elements(self, elems):
        # Create a straight route between ports p1 and p2.
        elems += spira.RouteStraight(p1=self.p1, p2=self.p2, layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        ports += [self.p1, self.p2]
        return ports
_images/_5_routes_0.png

It is also possible to define all ports in a single method and externally cache the method using the spira.cache decorator as shown in the following example.

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    @spira.cache()
    def get_ports(self):
        p1 = spira.Port(name='P1', midpoint=(-self.length/2,0), orientation=180, width=self.width, process=spira.RDD.PROCESS.R1)
        p2 = spira.Port(name='P2', midpoint=(self.length/2,0), orientation=0, width=self.width, process=spira.RDD.PROCESS.R1)
        return [p1, p2]

    def create_elements(self, elems):
        p1, p2 = self.get_ports()
        elems += spira.RouteStraight(p1=p1, p2=p2, layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        ports += self.get_ports()
        return ports

Cell Hierarchy

As layout designs becomes bigger and more complex with larger circuits, extending an maintaining PCells becomes a tedious task. Using basic object-oriented inheritance simplifies the overall structure of our designs.

Demonstrates
  • How to create a manhattan route between two ports.
  • How to use inheritance to mimic layout hierarchy.
  • How to extend a layout without changing the parent class.
  • How to pass cells as a parameter to another cell class.
  • How to connect different structures using their ports.

If two ports are not aligned on the same axis, the spira.RouteManhattan method can be used to generate a manhattan polygon between them. One prerequisite is that the absolute port orientation difference must equal \(180\) degrees.

class Resistor(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    p1 = spira.Parameter(fdef_name='create_p1')
    p2 = spira.Parameter(fdef_name='create_p2')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_p1(self):
        return spira.Port(name='P1', midpoint=(-self.length/2,0), orientation=180, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_p2(self):
        return spira.Port(name='P2', midpoint=(self.length/2,2), orientation=0, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_elements(self, elems):
        elems += spira.RouteManhattan(ports=[self.p1, self.p2], layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        ports += [self.p1, self.p2]
        return port
_images/_6_hierarchy_0.png

The created Resistor cell can be extended by creating a new cell that inherits from this class. To extend the elements we have to add the parent class elements to the current instance. This is done using Python’s super method: elems = super().create_elements(elems). A second route can then be generated starting from p2 and ending at p3 with a rounded corner bend.

# ...

class ResistorExtended(Resistor):

    p3 = spira.Parameter(fdef_name='create_p3')

    def create_p3(self):
        return spira.Port(name='P3', midpoint=(self.length,0), orientation=90, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_elements(self, elems):
        elems = super().create_elements(elems)
        elems += spira.RouteManhattan(ports=[self.p2, self.p3], corners='round', layer=spira.RDD.PLAYER.R1.METAL)
        return elems
_images/_6_hierarchy_1.png

Another method to mimic cell hierarchy is to pass a cell to another cell as a parameter:

class ResistorManhattan(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    p1 = spira.Parameter(fdef_name='create_p1')
    p2 = spira.Parameter(fdef_name='create_p2')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_p1(self):
        return spira.Port(name='P1', midpoint=(-self.length/2,0), orientation=180, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_p2(self):
        return spira.Port(name='P2', midpoint=(self.length/2,2), orientation=0, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_elements(self, elems):
        elems += spira.RouteManhattan(ports=[self.p1, self.p2], layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        ports += [self.p1, self.p2]
        return ports


class ResistorStraight(spira.PCell):

    width = spira.NumberParameter(default=spira.RDD.R1.MIN_WIDTH, doc='Width of the shunt resistance.')
    length = spira.NumberParameter(default=spira.RDD.R1.MIN_LENGTH, doc='Length of the shunt resistance.')

    p1 = spira.Parameter(fdef_name='create_p1')
    p2 = spira.Parameter(fdef_name='create_p2')

    def validate_parameters(self):
        if self.width > self.length:
            raise ValueError('`Width` cannot be larger than `length`.')
        return True

    def create_p1(self):
        return spira.Port(name='P1', midpoint=(-self.length/2,0), orientation=180, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_p2(self):
        return spira.Port(name='P2', midpoint=(self.length/2,0), orientation=0, width=self.width, process=spira.RDD.PROCESS.R1)

    def create_elements(self, elems):
        elems += spira.RouteStraight(p1=self.p1, p2=self.p2, layer=spira.RDD.PLAYER.R1.METAL)
        return elems

    def create_ports(self, ports):
        ports += [self.p1, self.p2]
        return ports


class ResistorConnect(spira.PCell):

    res0 = spira.CellParameter(default=ResistorManhattan)
    res1 = spira.CellParameter(default=ResistorStraight)

    def create_elements(self, elems):
        s1 = spira.SRef(reference=self.res0())
        s2 = spira.SRef(reference=self.res1())
        s2.connect(port=s2.ports['P1'], destination=s1.ports['P2'])
        elems += [s1, s2]
        return elem

We start by creating two resistor classes, ResistorManhattan and ResistorStraight, then we add them to a single cell instance were we can snap the two structures into place by connecting their respective instance ports. An instance for each resistor cell is created using spira.SRef and then P1 of instance ResistorStraight is connect to P2 of instance ResistorManhattan using the connect() method.

_images/RouteConnect.png

Transformations

Transformations is SPiRA are not directly applied to layout elements. Instead, they are abstraction build as a layer on top of the SPiRA core, and are connected to Elements as parameters. Doing so enable us to transform elements without losing hierarchical data implicit in the layout structure.

Demonstrates
  • Understand why transformations are parameterized in SPiRA.
  • How to apply transformations to layout elements.
  • How to keep the hierarchical structure of a flatened layout.

There are multiple different ways to apply a transformation to a layout element:

  • The first method creates a transform object and then applies it to an object.
  • The second directly uses a element method to apply the transform.
class TranslatePolygon(spira.Cell):

    ref_point = spira.Parameter(fdef_name='create_ref_point')
    t1 = spira.Parameter(fdef_name='create_t1')
    t2 = spira.Parameter(fdef_name='create_t2')
    t3 = spira.Parameter(fdef_name='create_t3')

    def create_ref_point(self):
        return spira.Rectangle(p1=(-2.5, -2.5), p2=(2.5, 2.5), layer=spira.Layer(number=1))

    def create_t1(self):
        """ Apply transformation by first creating a transform object. """
        T = spira.Translation(Coord(-10, 0))
        ply = spira.Rectangle(p1=(0,0), p2=(10, 50), layer=spira.Layer(number=2))
        ply.transform(T)
        return ply

    def create_t2(self):
        """ Apply transformation by creating a generic transform.
        Instead of using the `.ttransform` method, the transform
        is directly added as a parameter."""
        tf = spira.GenericTransform(translation=Coord(-22, 0))
        ply = spira.Rectangle(p1=(0,0), p2=(10, 50), layer=spira.Layer(number=3), transformation=tf)
        return ply

    def create_t3(self):
        """ Directly transform the element using the corresponding transform method. """
        ply = spira.Rectangle(p1=(0,0), p2=(10, 50), layer=spira.Layer(number=4))
        ply.translate((-34, 0))
        return ply

    def create_elements(self, elems):
        elems += self.ref_point
        elems += self.t1
        elems += self.t2
        elems += self.t3
        return elems
_images/_9_translate.png

The code snippet above illustrates the different ways to connect a transform to an element. The first method in create_t1 is typically used when we want to create a set of predefined transforms and later connect them to multiple elements.

The second method in create_t2 is very similar to that of the first, but instead manually creates a generic transformation object and connect it to the element as a parameter. This method is just shown for illustration purposes and rarely used in practice, since a generic transform is automatically created by the framework when multiple transform objects are added.

The thrid method in create_t3 uses the class method to automatically create the transform object and emidiately apply the transform to the subject element.

class TransformPolygon(spira.Cell):

    ref_point = spira.Parameter(fdef_name='create_ref_point')
    t1 = spira.Parameter(fdef_name='create_t1')
    t2 = spira.Parameter(fdef_name='create_t2')
    t3 = spira.Parameter(fdef_name='create_t3')

    def create_ref_point(self):
        return spira.Rectangle(p1=(-2.5, -2.5), p2=(2.5, 2.5), layer=spira.Layer(number=1))

    def create_t1(self):
        T = spira.Rotation(30) + spira.Translation(Coord(10, 0))
        ply = spira.Rectangle(p1=(0,0), p2=(10, 50), layer=spira.Layer(number=2))
        ply.transform(transformation=T)
        return ply

    def create_t2(self):
        T = spira.GenericTransform(translation=(20, 0), rotation=60)
        ply = spira.Rectangle(p1=(0,0), p2=(10, 50), layer=spira.Layer(number=3), transformation=T)
        return ply

    def create_t3(self):
        ply = spira.Rectangle(p1=(0,0), p2=(10, 50), layer=spira.Layer(number=4))
        ply.translate((30, 0))
        ply.rotate(90)
        return ply

    def create_elements(self, elems):
        elems += self.ref_point
        elems += self.t1
        elems += self.t2
        elems += self.t3
        return elems

Transformations can be compounded using the plus operator as shown in create_t1. The result is a generic transformation that can be added to any element. A generic transform can also be explicitly defined as in create_t2, or multiple transforms can be adding using the corresponding methods as shown in create_t3.

_images/_9_transform.png

Stretching

This tutorial builds from the previous tutorial of transformations. Here, we will look at how to stretch a cell reference and specific polygons using ports.

Demonstrates
  • How to stretch layout elements.
  • How to use expanded transformations to view flattened ports.
  • How to stretch a specific polygon while maintaining the layout hierarchy.

In this example we are stretching an entire cell reference by some factor. We have created a basic Josephson Junction to demonstrate the stretching of different polygons inside the PCell, while maintaining the hierarchical structure.

class Jj(spira.Cell):

    def create_elements(self, elems):
        elems += spira.Convex(radius=7.0, layer=RDD.PLAYER.C2.VIA)
        return elems


class ResVia(spira.Cell):

    def create_elements(self, elems):
        elems += spira.Rectangle(p1=(-7.5, -13.2), p2=(7.5, -8.2), layer=RDD.PLAYER.R1.METAL)
        elems += spira.Rectangle(p1=(-4, -12), p2=(4.1, -10), layer=RDD.PLAYER.C1.VIA)
        return elems


class Top(spira.Cell):

    def get_transforms(self):
        t1 = spira.Translation((0, 0))
        t2 = spira.Translation((0, -8))
        return [t1, t2]

    def create_elements(self, elems):
        t1, t2 = self.get_transforms()
        elems += spira.SRef(alias='Sj1', reference=Jj(), transformation=t1)
        elems += spira.SRef(alias='Sr1', reference=ResVia(), transformation=t2)
        elems += spira.Rectangle(p1=(-10, -23), p2=(10, 10), layer=RDD.PLAYER.M2.METAL)
        return elems


class Bot(spira.Cell):

    def get_transforms(self):
        t1 = spira.Translation((0, 0))
        t2 = spira.Translation((0, -30))
        return [t1, t2]

    def create_elements(self, elems):
        t1, t2 = self.get_transforms()
        elems += spira.SRef(alias='Sr2', reference=ResVia(), transformation=t2)
        elems += spira.Rectangle(p1=(-10, -55), p2=(10, -35), layer=RDD.PLAYER.M2.METAL)
        return elems


class Junction(spira.Cell):
    """ Josephson junction. """

    def get_transforms(self):
        t1 = spira.Translation((0, 0))
        t2 = spira.Translation((0, -5))
        return [t1, t2]

    def create_elements(self, elems):
        t1, t2 = self.get_transforms()
        elems += spira.Rectangle(p1=(-13, -60), p2=(13, 12), layer=RDD.PLAYER.M1.METAL)
        elems += spira.SRef(alias='S1', reference=Top(), transformation=t1)
        elems += spira.SRef(alias='S2', reference=Bot(), transformation=t2)
        return elems

An instance of the Junction cell can be created and added as a reference, which can be stretched using the stretch_by_factor() method:

junction = Junction()

C = spira.Cell(name='TestingCell')
S = spira.SRef(alias='Jj', reference=junction)

# Stretch the reference and add it to the cell.
C += S.stretch_by_factor(factor=(2,1))

# Generate an output using the build-in viewer.
C.gdsii_output()

The expanded flattened view of the junction cell is shown below.

_images/_9_expanded.png

All polygon elements that coalesces this cell is stretched by a factor of two in the horizontal direction (x-axis).

_images/_9_factor.png
junction = Junction()

C = spira.Cell(name='TestingCell')
S = spira.SRef(alias='Jj', reference=junction)

# Stretch the reference and add it to the cell.
S.stretch_p2p(port_name='S1:Sr1:E3_R1', destination_name='S2:Sr2:E1_R1')

# Generate an output using the build-in viewer.
C.gdsii_output()

The expanded view is used to access flattened ports using their hierarchically derived names. The port names used in the code above are shown in the expanded view of the cell. In this example we want to stretch the two shunt resistor polygons so form a single resistor connection polygon.

_images/_9_ports.png

Advanced Tutorial

This set of tutorials focuses on explaining more advanced features that the SPiRA framework has to offer. We go into more details on how to create device and circuit PCells, how to structure a design, and how to manipulate layout elements.

In SPiRA PCells can be divided into two categorises, spira.Device and spira.Circuit. Each of these classes contains a set of different back-end algorithms that are automatically executed when the layout class is constructed. Typically, these algorithms consists of boolean operations and filtering algorithms. Also, inheriting from these classes defines the purpose of the layout, either a device or a circuit.

Devices:

Similar to creating a PCell, constructing a device cell required inheriting from spira.Device instead of spira.PCell. In superconducting circuits a device layout is usually a Via or a Junction.

class Junction(spira.Device):
    pass

Circuits:

A circuit PCell is designed similar to that of a device. By definition a circuit layout contains polygon routes that connects different device and ports instances. Therefore, a spira.Circuit contains two extra, but optional, create methods to simplify the code structure:

  • create_structures: Defines the device instances.
  • create_routes: Defines the routing paths between different structures and ports.
class Jtl(spira.Circuit):

    def create_structures(self, elems):
        return elems

    def create_routes(self, elems):
        return elems

Note, it is not required to use these methods, but designing large circuits can cause the create_elements method to become cumbersome.

Library Structure

Every design environment connects to a specific fabrication process, also known as the PDK. In SPiPA, the PDK data is encapsulated in Python scripts and are collectively called the RDD. RDD script names start with db_ as illustrated below.

This section discusses how to organize your design project in SPiRA as a systematized library. Technically, your library can have any structure given that you compensates for the necessary importing changes. But it is highly adviced to use the proposed structure.

technologies
|__ mitll
    |__ devices
        |__ junction.py
        |__ vias.py
    |__ circuits
        |__ jtl.py
        |__ dcsfq.py
    |__ db_init.py
    |__ db_process.py
    |__ db_lvs.py

The technology library is broken down into 3 parts:

  1. Devices: Contains defined device PCells for the specific technology.
  2. Circuits: Contains PCell circuits created using the specific technology.
  3. Database: Contains a set database files that make up the RDD.

The technologies folder is the base folder inside the SPiRA design environment that contains all the different technology processes and PCell designs in a single place. The library structure above contains the mitll library, which consists of defined junction and via devices, a long with a JTL and DCSFQ circuit.

The db_init script is the first file to be executed when the RDD database is constructed. The db_process script contains most of the information required to design a PCell. This file contains the process layers, layer purposes, process parameters, etc. The db_lvs script defines the created device PCells to be used in the device detection algorithms when doing LVS extraction.

YTron

In this example we will start from the beginning. First, we will create a yTron shape and then using this shape we will create a device containing input/output ports. This device will then be used to create a full circuit layout.

Demonstrates
  • How to create your own shape class.
  • How to create a device and a circuit PCell.
  • How to restrict a design to only accept a specific shape or device.

We create our own yTron shape by inheriting from spira.Shape, which allows us to manipulate the shape once it has been instantiated.

class YtronShape(spira.Shape):
    """ Class for generating a yTron shape. """

    rho = NumberParameter(default=2, doc='Angle of concave bend between the arms.')
    arm_lengths = CoordParameter(default=(5,3), doc='Length or the left and right arms, respectively.')
    source_length = NumberParameter(default=5, doc='Length of the source arm.')
    arm_widths = CoordParameter(default=(2,2), doc='Width of the left and right arms, respectively.')
    theta = NumberParameter(default=10, doc='Angle of the left and right arms.')
    theta_resolution = NumberParameter(default=10, doc='Smoothness of the concave bend.')

    xc = Parameter(fdef_name='create_xc')
    yc = Parameter(fdef_name='create_yc')
    arm_x_left = Parameter(fdef_name='create_arm_x_left')
    arm_y_left = Parameter(fdef_name='create_arm_y_left')
    arm_x_right = Parameter(fdef_name='create_arm_x_right')
    arm_y_right = Parameter(fdef_name='create_arm_y_right')
    rad_theta = Parameter(fdef_name='create_rad_theta')
    ml = Parameter(fdef_name='create_midpoint_left')
    mr = Parameter(fdef_name='create_midpoint_right')
    ms = Parameter(fdef_name='create_midpoint_source')

    def create_rad_theta(self):
        return self.theta * np.pi/180

    def create_xc(self):
        return self.rho * np.cos(self.rad_theta)

    def create_yc(self):
        return self.rho * np.sin(self.rad_theta)

    def create_arm_x_left(self):
        return self.arm_lengths[0] * np.sin(self.rad_theta)

    def create_arm_y_left(self):
        return self.arm_lengths[0] * np.cos(self.rad_theta)

    def create_arm_x_right(self):
        return self.arm_lengths[1] * np.sin(self.rad_theta)

    def create_arm_y_right(self):
        return self.arm_lengths[1] * np.cos(self.rad_theta)

    def create_midpoint_left(self):
        xc = -(self.xc + self.arm_x_left + self.arm_widths[0]/2)
        yc = self.yc + self.arm_y_left
        return [xc, yc]

    def create_midpoint_right(self):
        xc = self.xc + self.arm_x_right + self.arm_widths[1]/2
        yc = self.yc + self.arm_y_right
        return [xc, yc]

    def create_midpoint_source(self):
        xc = (self.arm_widths[1] - self.arm_widths[0])/2
        yc = -self.source_length + self.yc
        return [xc, yc]

    def create_points(self, points):

        theta = self.theta * np.pi/180
        theta_resolution = self.theta_resolution * np.pi/180
        theta_norm = int((np.pi-2*theta)/theta_resolution) + 2
        thetalist = np.linspace(-(np.pi-theta), -theta, theta_norm)
        semicircle_x = self.rho * np.cos(thetalist)
        semicircle_y = self.rho * np.sin(thetalist)+self.rho

        xpts = semicircle_x.tolist() + [
            self.xc + self.arm_x_right,
            self.xc + self.arm_x_right + self.arm_widths[1],
            self.xc + self.arm_widths[1],
            self.xc + self.arm_widths[1],
            0, -(self.xc + self.arm_widths[0]),
            -(self.xc + self.arm_widths[0]),
            -(self.xc + self.arm_x_left + self.arm_widths[0]),
            -(self.xc + self.arm_x_left)
        ]

        ypts = semicircle_y.tolist() + [
            self.yc + self.arm_y_right,
            self.yc + self.arm_y_right,
            self.yc, self.yc - self.source_length,
            self.yc - self.source_length,
            self.yc - self.source_length,
            self.yc, self.yc + self.arm_y_left,
            self.yc + self.arm_y_left
        ]

        points = np.array(list(zip(xpts, ypts)))

        return points

There is a few important aspects to note in the YtronShape class:

  1. The create_points create method is required by the spira.Shape class and is similar to the create_elements method for creating a cell.
  2. In this example the importance of the doc attribute when defining a parameter becomes apparent.
  3. Using create methods to dynamically define the shape parameters makes the shape instance easier to use.

Once we have the desired shape we can use it to create a device cell, containing a GDSii layer and ports instances.

# ...

class YtronDevice(spira.Device):

    shape = spira.ShapeParameter(restriction=spira.RestrictType([YtronShape]))

    def create_elements(self, elems):
        elems += spira.Polygon(shape=self.shape, layer=RDD.PLAYER.M1.METAL)
        return elems

    def create_ports(self, ports):

        left_arm_width = self.shape.arm_widths[0]
        rigth_arm_width = self.shape.arm_widths[1]
        src_arm_width = self.shape.arm_widths[0] + self.shape.arm_widths[1] + 2*self.shape.xc

        ports += spira.Port(name='Pl_M1', midpoint=self.shape.ml, width=left_arm_width, orientation=90)
        ports += spira.Port(name='Pr_M1', midpoint=self.shape.mr, width=rigth_arm_width, orientation=90)
        ports += spira.Port(name='Psrc_M1', midpoint=self.shape.ms, width=src_arm_width, orientation=270)

        return ports

>>> shape = YtronShape(theta_resolution=100)
>>> D = YtronDevice(shape=shape)
>>> D.gdsii_output()
_images/_adv_0_ytron.png

The shape parameter defined in the YtronDevice class restricts the instance to only receive a shape of type YtronShape. Using the shape parameters the port instances for each arms can be defined and added to the PCell instance. The created yTron device can now be used in a circuit:

class YtronCircuit(spira.Circuit):

    ytron = spira.Parameter(fdef_name='create_ytron', doc='Places an instance of the ytron device.')

    @spira.cache()
    def get_io_ports(self):
        p1 = spira.Port(name='P1_M1', midpoint=(-10,10), orientation=0)
        p2 = spira.Port(name='P2_M1', midpoint=(5,10), width=0.5, orientation=270)
        p3 = spira.Port(name='P3_M1', midpoint=(0,-10), width=1, orientation=90)
        return [p1, p2, p3]

    def create_ytron(self):
        shape = YtronShape(rho=0.5, theta=5)
        D = YtronDevice(shape=shape)
        return spira.SRef(alias='ytron', reference=D)

    def create_elements(self, elems):
        p1, p2, p3 = self.get_io_ports()

        elems += self.ytron

        elems += spira.RouteManhattan(
            ports=[self.ytron.ports['Pl_M1'], p1],
            width=self.ytron.ref.shape.arm_widths[0],
            layer=RDD.PLAYER.M1.METAL,
            corners=self.corners)

        elems += spira.RouteStraight(p1=p2,
            p2=self.ytron.ports['Pr_M1'],
            layer=RDD.PLAYER.M1.METAL,
            path_type='sine', width_type='sine')

        elems += spira.RouteStraight(p1=p3,
            p2=self.ytron.ports['Psrc_M1'],
            layer=RDD.PLAYER.M1.METAL,
            path_type='sine', width_type='sine')

        return elems

    def create_ports(self, ports):
        ports += self.get_io_ports()
        return ports

The figure below shows the output of the yTron PCell if the class was constructed inheriting from spira.PCell. The metal layers are separated and the connection ports are still visible.

_images/_adv_0_ytron_pcell.png

The following figure is the final result when inheriting from spira.Circuit rather than spira.PCell. The contacting metal layers are merged and the redundant ports are filtered.

_images/_adv_0_ytron_circuit.png

From the code above we can see that three routes are defined. The first, connects the left arm with the first port using a basic manhattan structure. The second and third, connects the right arm to the second port and the source arm to the third port, but uses a sine path type to generate the routing polygons.

Via Device

Via devices generally following the same design patterns, but still require explicit construction to describe how PDK data should be handled on instance creation. This example illustrated the creation of the alternative resistor via contact that is responsible to connecting resistive layer R5 to inductive layer M6.

Demonstrates
  • How to create a via device.
  • How to add range restrictions to parameters.
  • How to create a cell that validates design rules on instance creation.

Recall, that by definition a PCell script is responsible for describing the interrelations between layout elements and defined parameters. These parameters can be design restrictions imposed by the specific fabrication technology.

class ViaC5RA(spira.Device):
    """ Via component for the MiTLL process. """

    width = spira.NumberParameter(default=RDD.R5.MIN_SIZE, restriction=spira.RestrictRange(lower=RDD.R5.MIN_SIZE))

    height = spira.Parameter(fdef_name='create_height')
    via_width = spira.Parameter(fdef_name='create_via_width')
    via_height = spira.Parameter(fdef_name='create_via_height')

    m6_width = spira.Parameter(fdef_name='create_m6_width', doc='Width of the via layer polygon.')
    m6_height = spira.Parameter(fdef_name='create_m6_height', doc='Width of the via layer polygon.')

    def create_m6_width(self):
        return (self.via_width + 2*RDD.C5R.M6_MIN_SURROUND)

    def create_via_width(self):
        return (self.width + 2*RDD.C5R.R5_MAX_SIDE_SURROUND)

    def create_via_height(self):
        return RDD.C5R.MIN_SIZE

    def create_height(self):
        return self.via_height + 2*RDD.R5.C5R_MIN_SURROUND

    def create_elements(self, elems):
        elems += spira.Box(layer=RDD.PLAYER.C5R.VIA, width=self.via_width, height=self.via_height, enable_edges=False)
        elems += spira.Box(alias='M6', layer=RDD.PLAYER.M6.METAL, width=self.m6_width, height=self.height, enable_edges=False)
        elems += spira.Box(alias='R5', layer=RDD.PLAYER.R5.METAL, width=self.width, height=self.height, enable_edges=False)
        return elems

    def create_ports(self, ports):
        p0 = self.elements['M6'].ports.unlock
        p1 = self.elements['R5'].ports.unlock
        return ports

Thus, the code for the via PCell defined above is responsible for describing how the top and bottom metal layers must be constructed in relation to the contact layer without violating any design rules. The PCell defines the specific design rules applicable to the creation of this via device.

Resistor

In Single Flux Quantum (SFQ) logic circuits, we typically use a shunt resistance for the biasing section of the circuit. Therefore, we would want to create a single resistor PCell that can be used as a template in more complex circuit PCells. Here, we design a resistor that parameterized its width, length, and type of via connection to other metal layers.

Demonstrates
  • How to design a circuit that can interchange different via devices.
  • How to restrict the circuit to only accept vias of a certain type.
  • How to activate specific port edges that can be used for external connetions.

This PCell can iterate between two different vias connections that connect metal layer R5 and M6; the alternative version of the standard version.

class Resistor(spira.Circuit):
    """ Resistor PCell of type Circuit between two vias connecting to layer M6. """

    length = spira.NumberParameter(default=7)
    width = spira.NumberParameter(
        default=RDD.R5.MIN_SIZE,
        restriction=spira.RestrictRange(lower=RDD.R5.MIN_SIZE),
        doc='Width of the shunt resistance.')
    via = spira.CellParameter(
        default=dev.ViaC5RS,
        restriction=spira.RestrictType([dev.ViaC5RA, dev.ViaC5RS]),
        doc='Via component for connecting R5 to M6')
    text_type = spira.NumberParameter(default=92)

    via_left = spira.Parameter(fdef_name='create_via_left')
    via_right = spira.Parameter(fdef_name='create_via_right')

    def validate_parameters(self):
        if self.length < self.width:
            raise ValueError('Length cannot be less than width.')
        return True

    def create_via_left(self):
        via = self.via(width=0.3+self.width)
        T = spira.Rotation(rotation=-90)
        S = spira.SRef(via, transformation=T)
        return S

    def create_via_right(self):
        via = self.via(width=0.3+self.width)
        T = spira.Rotation(rotation=-90, rotation_center=(self.length, 0))
        S = spira.SRef(via, midpoint=(self.length, 0), transformation=T)
        return S

    def create_elements(self, elems):

        elems += [self.via_left, self.via_right]

        elems += RouteStraight(
            p1=self.via_left.ports['E0_R5'],
            p2=self.via_right.ports['E2_R5'],
            layer=RDD.PLAYER.R5.METAL)

        return elems

    def create_ports(self, ports):

        ports += self.via_left.ports['E1_M6'].copy(name='P1_M6')
        ports += self.via_left.ports['E2_M6'].copy(name='P2_M6')
        ports += self.via_left.ports['E3_M6'].copy(name='P3_M6')

        ports += self.via_right.ports['E0_M6'].copy(name='P4_M6')
        ports += self.via_right.ports['E1_M6'].copy(name='P5_M6')
        ports += self.via_right.ports['E3_M6'].copy(name='P6_M6')

        return ports

The length parameter can be any value as long as it is larger than the width. Therefore, the length parameter has no restrictions, but are validated once all parameters have been defined using the validate_parameters method. The width parameter is restricted to a minimum size, which implicitly mean the length is also restricted to this size value. The via parameter has to be a PCell class and has to be of type dev.ViaC5RA or dev.ViaC5RS.

We only want to connect to the connection vias of the instance, and therefore we only activate the ports of the two via instance, instead of activating all possible edge ports, as shown in the create_ports method.

Josephson Junction

The Josephson junction is the most important device in any SDE circuit. We want to create a junction PCell that parameterizes the following device attributes:

  • The shunt resistor width.
  • The shunt resistor length.
  • The junction layer radius.
  • Boolean parameters to include/exclude via connections to ground and skyplane.
Demonstrates
  • How to design a fully parameterized Josephson junction.
  • How to add a bounding box around a set of polygon objects.

The design of the junction is broken down into three sections; a top section, a bottom section, and the shunt resistor that connects the top and bottom sections. The top and bottom section each are wrapped with a bounding box polygon of metal layer M6.

class __Junction__(spira.Cell):
    """ Base class for Junction PCell. """

    radius = spira.NumberParameter()
    width = spira.NumberParameter(doc='Shunt resistance width')
    c5r = spira.Parameter(fdef_name='create_c5r')


class I5Contacts(__Junction__):
    """ Cell that contains all the vias of the bottom halve of the Junction. """

    i5 = spira.Parameter(fdef_name='create_i5')
    i6 = spira.Parameter(fdef_name='create_i6')

    sky_via = spira.BoolParameter(default=False)

    def create_i5(self):
        via = dev.ViaI5()
        V = spira.SRef(via, midpoint=(0,0))
        return V

    def create_i6(self):
        c = self.i5.midpoint
        w = (self.i5.ref.width + 4*RDD.I6.I5_MIN_SURROUND)
        via = dev.ViaI6(width=w, height=w)
        V = spira.SRef(via, midpoint=c)
        return V

    def create_c5r(self):
        # via = dev.ViaC5RA(width=self.width)
        via = dev.ViaC5RS()
        V = spira.SRef(via)
        if self.sky_via is True:
            V.connect(port=V.ports['E0_R5'], destination=self.i6.ports['E2_M6'], ignore_process=True)
        else:
            V.connect(port=V.ports['E0_R5'], destination=self.i5.ports['E2_M5'], ignore_process=True)
        return V

    def create_elements(self, elems):

        # Add the two via instances.
        elems += [self.i5, self.c5r]

        # Add the skyplane via instance if required.
        if self.sky_via is True:
            elems += self.i6

        # Add bounding box around all elements.
        box_shape = elems.bbox_info.bounding_box(margin=0.1)
        elems += spira.Polygon(shape=box_shape, layer=RDD.PLAYER.M6.METAL)

        return elems

    def create_ports(self, ports):
        ports += self.i5.ports['E2_M5'].copy(name='P2_M5')
        ports += self.c5r.ports['E2_R5'].copy(name='P2_R5')
        return ports


class J5Contacts(__Junction__):
    """ Cell that contains all the vias of the top halve of the Junction. """

    j5 = spira.Parameter(fdef_name='create_j5')

    def create_j5(self):
        jj = dev.JJ(width=2*self.radius)
        D = spira.SRef(jj, midpoint=(0,0))
        return D

    def create_c5r(self):
        # via = dev.ViaC5RA(width=self.width)
        via = dev.ViaC5RS()
        V = spira.SRef(via)
        V.connect(port=V.ports['E0_R5'], destination=self.j5.ports['E0_M5'], ignore_process=True)
        return V

    def create_elements(self, elems):

        # Add the two via instances.
        elems += [self.j5, self.c5r]

        # Add bounding box around all elements.
        box_shape = elems.bbox_info.bounding_box(margin=0.1)
        elems += spira.Polygon(shape=box_shape, layer=RDD.PLAYER.M6.METAL)

        return elems

    def create_ports(self, ports):
        ports += self.j5.ports['E0_M5'].copy(name='P0_M5')
        ports += self.c5r.ports['E2_R5'].copy(name='P2_R5')
        return ports

The J5Contacts and I5Contacts classes are the top and bottom sections, respectively. The __Junction__ class is a base class that contains parameters common to both of these classes. As shown in the create_elements methods for both classes a metal bounding box is added around all defined elements.

The results for J5Contacts is shown below and consists of a C5R via that connects layer R5 and a junction via that contains the actually junction layer.

_images/_adv_junction_top.png

The result for I5Contacts is shown below and consists of a C5R via that connects layer R5 and a I5 via that connects layer M5 to layer M6. The skyplane via that connects M6 to M7 is optional depending on the boolean value of the sky_via parameter.

_images/_adv_junction_bot.png
class Junction(spira.Device):

    text_type = spira.NumberParameter(default=91)

    length = spira.NumberParameter(default=1.5, doc='Length of the shunt resistance.')

    width = spira.NumberParameter(
        default=RDD.R5.MIN_SIZE,
        restriction=spira.RestrictRange(lower=RDD.R5.MIN_SIZE, upper=RDD.R5.MAX_WIDTH),
        doc='Width of the shunt resistance.')

    radius = spira.NumberParameter(
        default=RDD.J5.MIN_SIZE,
        restriction=spira.RestrictRange(lower=RDD.J5.MIN_SIZE, upper=RDD.J5.MAX_SIZE),
        doc='Radius of the circular junction layer.')

    i5 = spira.Parameter(fdef_name='create_i5_cell')
    j5 = spira.Parameter(fdef_name='create_j5_cell')

    gnd_via = spira.BoolParameter(default=False)
    sky_via = spira.BoolParameter(default=False)

    def create_i5_cell(self):
        D = I5Contacts(width=self.width, radius=self.radius, sky_via=self.sky_via)
        S = spira.SRef(D)
        S.move(midpoint=S.ports['P2_R5'], destination=(0, self.length))
        return S

    def create_j5_cell(self):
        D = J5Contacts(width=self.width, radius=self.radius)
        S = spira.SRef(D)
        S.move(midpoint=S.ports['P2_R5'], destination=(0,0))
        return S

    def create_elements(self, elems):

        elems += self.i5
        elems += self.j5

        elems += RouteStraight(
            p1=self.i5.ports['P2_R5'].copy(width=self.width),
            p2=self.j5.ports['P2_R5'].copy(width=self.width),
            layer=RDD.PLAYER.R5.METAL)

        if self.gnd_via is True:
            i4 = dev.ViaI4()
            elems += spira.SRef(i4, midpoint=m5_block.center)

        box_shape = elems.bbox_info.bounding_box(margin=0.1)
        elems += spira.Polygon(shape=box_shape, layer=RDD.PLAYER.M5.METAL)

        return elems

    def create_ports(self, ports):
        ports += self.j5.ports['E0_M6'].copy(name='P0_M6')
        ports += self.j5.ports['E1_M6'].copy(name='P1_M6')
        ports += self.j5.ports['E3_M6'].copy(name='P3_M6')
        ports += self.i5.ports['E1_M6'].copy(name='P4_M6')
        ports += self.i5.ports['E2_M6'].copy(name='P5_M6')
        ports += self.i5.ports['E3_M6'].copy(name='P6_M6')
        return ports

The Junction class is created and instances of the J5Contacts and I5Contacts cells are added and moved relative to eachother with a separation distance equal to the length of the shunt resistor. The instances of of these two cells are then connection via a resistive route. For debugging purposes we can disable the operations preformed by the spira.Device class by setting pcell=False. The output is shown below displays the individual layers of each instance.

_images/_adv_junction_false.png

By enabling PCell operations again we can see that the overlapping metal layers are merged by similar process polygon, as shown in the figure below.

_images/_adv_junction_true.png

Josephson Transmission Line

The Josephson Transmission Line (JTL) is the most basic SFQ circuit and consist of two junctions, an input and output port, and a biasing port.

Demonstrates
  • How to define routes between different ports and devices.
  • How to parameterize the route widths.
  • How to include a device PCell into higher hierarchical designs.

We define three width parameters to control the polygon routing width between:

  1. The input port and first junction.
  2. The ouput port and second junction.
  3. The first junction and second junction.

Next, we create a set of create methods to define device and port instances.

class Jtl(spira.PCell):

    w1 = spira.NumberParameter(
        default=RDD.M6.MIN_SIZE,
        restriction=RestrictRange(lower=RDD.M6.MIN_SIZE, upper=RDD.M6.MAX_WIDTH),
        doc='Width of left inductor.'
    )
    w2 = spira.NumberParameter(
        default=RDD.M6.MIN_SIZE,
        restriction=RestrictRange(lower=RDD.M6.MIN_SIZE, upper=RDD.M6.MAX_WIDTH),
        doc='Width of middle inductor.'
    )
    w3 = spira.NumberParameter(
        default=RDD.M6.MIN_SIZE,
        restriction=RestrictRange(lower=RDD.M6.MIN_SIZE, upper=RDD.M6.MAX_WIDTH),
        doc='Width of rigth inductor.'
    )

    p1 = spira.Parameter(fdef_name='create_p1')
    p2 = spira.Parameter(fdef_name='create_p2')
    p3 = spira.Parameter(fdef_name='create_p3')
    p4 = spira.Parameter(fdef_name='create_p4')

    jj1 = spira.Parameter(fdef_name='create_jj_left')
    jj2 = spira.Parameter(fdef_name='create_jj_right')

    shunt = spira.Parameter(fdef_name='create_shunt')

    bias_res = spira.Parameter(fdef_name='create_bias_res')
    via1 = spira.Parameter(fdef_name='create_via1')

    def create_p1(self):
        p1 = spira.Port(name='P1_M6', width=self.w1)
        return p1.distance_alignment(port=p1, destination=self.jj1.ports['P1_M6'], distance=-10)

    def create_p2(self):
        p2 = spira.Port(name='P2_M6', width=self.w1)
        return p2.distance_alignment(port=p2, destination=self.jj2.ports['P3_M6'], distance=10)

    def create_p3(self):
        return spira.Port(name='P3_M6', midpoint=(0, 15), orientation=270, width=self.w1)

    def create_p4(self):
        return spira.Port(name='P4_M6', midpoint=(0, 1.5), orientation=90, width=self.w1)

    def create_jj_left(self):
        jj = dev.Junction(length=1.9, width=1, radius=0.91)
        T = spira.Rotation(rotation=180, rotation_center=(-10,0))
        S = spira.SRef(jj, midpoint=(-10,0), transformation=T)
        return S

    def create_jj_right(self):
        jj = dev.Junction(length=1.9, width=1, radius=0.91)
        T = spira.Rotation(rotation=180, rotation_center=(10,0))
        S = spira.SRef(jj, midpoint=(10,0), transformation=T)
        return S

    def create_shunt(self):
        D = Resistor(width=1, length=3.7)
        S = spira.SRef(reference=D, midpoint=(0,0))
        S.distance_alignment(port='P2_M6', destination=self.p3, distance=-2.5)
        return S

    def create_elements(self, elems):

        elems += self.jj1
        elems += self.jj2
        elems += self.shunt

        elems += RouteStraight(p1=self.p1,
            p2=self.jj1.ports['P1_M6'].copy(width=self.p1.width),
            layer=RDD.PLAYER.M6.ROUTE)

        elems += RouteStraight(p1=self.p2,
            p2=self.jj2.ports['P3_M6'].copy(width=self.p2.width),
            layer=RDD.PLAYER.M6.ROUTE)

        elems += RouteStraight(
            p1=self.jj1.ports['P3_M6'].copy(width=self.w2),
            p2=self.jj2.ports['P1_M6'].copy(width=self.w2),
            layer=RDD.PLAYER.M6.ROUTE)

        elems += RouteStraight(p1=self.shunt.ports['P2_M6'], p2=self.p3, layer=RDD.PLAYER.M6.ROUTE)
        elems += RouteStraight(p1=self.shunt.ports['P4_M6'], p2=self.p4, layer=RDD.PLAYER.M6.ROUTE)

        return elems

    def create_ports(self, ports):
        ports += self.p1
        ports += self.p2
        ports += self.p3
        ports += self.p4
        return ports

This examples place two junctions, jj_left and jj_right, at positions (-10,0) and (10,0). The input port is placed a ditance of -10 to the left of jj_left, and the ouput port a distance of 10 to the right of jj_right.

The biasing port, p3 is place at position (0,15) and port P2_M6 of the biasing resistor PCell is place a distance of 2.5 to the bottom of p3.

_images/_adv_jtl_false.png

Electrical Rule Checking

The electrical rule checking algorithm is applied on an instance using a filtering method. Therefore, it is easily enabled/disabled for debugging purposes.

Demonstrates
  • How to toggle the ERC algorithm.
  • How to view the electrical rule checking results using virtual modeling.
# Create an instance of the PCell class.
D = Jtl()

# Apply the ERC and Port Excitation algorithms to the cell.
f = RDD.FILTERS.PCELL.MASK

D = f(D)

from spira.yevon.vmodel.virtual import virtual_connect
v_model = virtual_connect(device=D)

v_model.view_virtual_connect(show_layers=True)
_images/_adv_jtl_erc.png

The resultant layout or view of a cicuit that contains virtual elements that will not be included in the final design, is called a virtual model. The above example illustrates how electrical rule checking can be debugged using virtually constructed polygons.

Netlist Extraction

Netlists for PCells can be extracted and viewed in a graph representation.

Demonstrates
  • How to extract the netlist graph of a PCell.
  • How to view the extracted graph.
# Create an instance of the PCell class.
D = Jtl()

# Apply the ERC and Port Excitation algorithms to the cell.
D = RDD.FILTERS.PCELL.MASK(D)

# Extract the physical netlist.
net = D.extract_netlist

# View the netlist.
D.netlist_view(net=net)

Before running the netlist extraction algorithm it is important to first apply the required filters to the pcell instance. These filters includes running electrical rule checking algorithm and compressing terminal ports down onto their corresponding polygon instances. It is also possible to toggle certain filters for debugging purposes:

D = Jtl()

f = RDD.FILTERS.PCELL.MASK

f['pin_attach'] = False

D = f(D)

net = D.extract_netlist

D.netlist_view(net=net)

The above example illustrates the extracted netlist if the pin attach algorithm is disabled. The added terminal ports are not detected by the netlist run, since they are not compressed down the layout hierarchy onto their corresponding polygons. The following image shows the different extracted netlists for a basic JTL layout using the code snippets previously discussed.

_images/_adv_jtl_net.png

Reference

Developers

Documentation for developers for maintaining and extending.

Distribtuion

Uploading package to PyPi using twine. Remember to remove all Eggs before doing a push to PyPi.

1
2
sudo python3 setup.py bdist_wheel
twine upload dist/*

To install package systemwide set the prefix value when running setuptools:

1
sudo python3 setup.py install --prefix=/usr
1
sudo python3 -m pip install --upgrade .

Unit testing overview: http://docs.python-guide.org/en/latest/writing/tests/

Documentation

If you want to generate the docs make sure the Napoleon package is installed:

1
pip install sphinxcontrib-napoleon

Coding standards for parsing the correct docs is given in:

Introduction to Python Virtual Enviroments:

Mixins

The following are useful links to some of the mixin implementations used in the SPiRA framework,

Indices and tables