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:
- 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.
- 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
.
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:
- Create a new cell by inheriting from
spira.Cell
. This connects the class to the SPiRA framework when constructed. - Define the PCell parameters as class attributes.
- Elements and ports are defined in the
create_elements
andcreate_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()

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

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')

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

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:
- Extracting a netlist for each individual metal polygon.
- 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:
- A layer stack is created to defined the top, bottom, and via layers.
- 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. - 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

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

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
.

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:

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

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

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

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.

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

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
.

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.

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

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.

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:
- Devices: Contains defined device PCells for the specific technology.
- Circuits: Contains PCell circuits created using the specific technology.
- 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:
- The
create_points
create method is required by thespira.Shape
class and is similar to thecreate_elements
method for creating a cell. - In this example the importance of the
doc
attribute when defining a parameter becomes apparent. - 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()

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.

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.

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.

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.

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.

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.

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:
- The input port and first junction.
- The ouput port and second junction.
- 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
.

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)

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.

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:
- https://sphinxcontrib-napoleon.readthedocs.io/en/latest/
- https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
Introduction to Python Virtual Enviroments:
Mixins¶
The following are useful links to some of the mixin implementations used in the SPiRA framework,