Simulation objects
The geometry of a simulation must be defined in a simulation object, which is an instance of a dedicated subclass of
either Simulation
or CrossSectionSimulation
classes.
The Simulation
is the base class for typical 3-dimensional simulation geometries.
The CrossSectionSimulation
is the base class for cross-sectional simulations, where the geometries are
typically 2-dimensional cross-sections of waveguides.
Both are intended to be subclassed by a specific simulation implementation.
This user guide mainly focuses on the usage of Simulation
class.
See the section Cross-sectional simulations for more information on the cross-sectional simulations.
The Simulation class
The Simulation
class works very similar with typical KQCircuits elements: the subclass should implement a
build()
method which generates the geometry and ports, typically
by inserting other elements into the model.
See also the build()
method of class Element
.
Convenience class method from_cell()
is provided to create a
simulation object from an existing cell.
In this case, the Simulation
class doesn’t have to be subclassed and no ports will be added.
See Geometry from Klayout GUI for more information.
Box
The Simulation
class introduces several PCell parameters to tune the simulation geometry.
One of them is box
, which defines the x- and y-dimensions of the simulation domain.
The value of box
should be given as pya.DBox
.
In practice, the parameter box
can be used to limit the area that is taken into account in the simulation
and/or to define the margins around the relevant shapes.
One should consider carefully on a case-by-case basis what is suitable simulation area for the purpose.
Ports
Ports typically define the input source terms of the simulation and distinguish the signal and ground metals from each
other.
Two types of ports are supported, InternalPort
between metal layers inside the geometry and
EdgePort
at the edge of the simulation box.
Ports should be defined in the build
method by adding instances of the port classes to the pre-defined list
Simulations.ports
.
An internal port is created by supplying an integer representing the port number and two pya.DPoint
points
signal_location
and ground_location
for the InternalPort
class builder.
The points should be located on opposite edges of two metal islands, and the actual port will be drawn as a polygon
between these edges (the opposing edges become two of the polygon edges).
For example, the following snippet adds an internal port across the junction of a single-island qubit, assuming that
refp
is the list of Refpoints
obtained when inserting the corresponding qubit cell.:
self.ports.append(
InternalPort(number=1, signal_location=refp["port_squid_a"], ground_location=refp["port_squid_b"])
)
An edge port is added similarly by creating an instance of EdgePort
.
The mandatory parameters for EdgePort
class builder are the port number and the signal_location
, which
must be located on the edge of Simulation.box
.
The convenience method produce_waveguide_to_port()
draws a
waveguide from a specified location and in a specified direction, and adds the required port at the end of the
waveguide.
It supports both internal and edge ports, for example:
# Create a 100um long waveguide that ends in an internal port
self.produce_waveguide_to_port(location=refp["port_2"], towards=refp["port_2_corner"], port_nr=2,
use_internal_ports=True, waveguide_length=100)
# Create a waveguide that bends and terminates as an edge port on the right side of Simulation.box
self.produce_waveguide_to_port(location=refp["port_3"], towards=refp["port_3_corner"], port_nr=3,
use_internal_ports=False, side="right")
Note
The ports are multipurpose and their implementation depends on the selected external simulation tool. For example in Ansys HFSS, the internal ports are mapped as a Lumped Ports and the edge ports are mapped as Terminal Wave Ports. In Q3D and Elmer capacitance simulations ports are used only to distinguish signal, ground, and floating islands and actual port polygons are omitted. For qubits with multiple islands, usually a separate port is needed for each island.
Face stack
The 3-dimensional geometry is built of uniform thickness dielectric substrates, which are typically separated from
each other by vacuum.
The thin metal and dielectric layers and other objects can be applied on any of the imaginable substrate surface and
between faces.
For example airbridges can be inserted to 3d geometry by drawing the shapes on airbridge pads
and
airbridge_flyover
layers.
Also metal connections between chips or between two sides of a chip can be applied using indium bump
or
through silicon via
layers, respectively.
The shapes of indium bumps and through silicon vias must be drawn on both the lower and upper faces of the joint.
The number of substrates is determined with two parameters, face_stack
and lower_box_height
:
The list
face_stack
determines which faces are taken into account in the simulation. The faces are listed from bottom to top and the length offace_stack
describes how many surfaces are taken into account in the simulation.The parameter
lower_box_height
determines if the first term inface_stack
corresponds to bottom or top surface of the lowest substrate. That is, iflower_box_height
> 0, there will be a vacuum box below the lowest substrate, and the counting of faces will start from the bottom surface of the lowest substrate. Otherwise, the counting of faces will start from the top surface of the lowest substrate.
The following figure indicates the parameterization of the most typical 3-dimensional layouts. The green and purple colors in the cross-sectional image illustrate substrate and thin metal layers, respectively.
The single chip geometry (left figure) is used by default, and two-substrate flip-chip geometry (middle figure) is typically obtained by setting:
face_stack = ['1t1', '2b1']
To produce multiple layers on a substrate interface one can introduce face_stack
as list of lists.
In that case, all the metal and dielectric layers of the inner list faces are piled up on the corresponding surface
in the respective order.
That means, the first term in the inner list indicates the face that is closest to the substrate.
One can use empty list in face_stack to leave certain surface without metallization.
Thicknesses of substrates (substrate_height
) and vacuum boxes between chips (chip_distance
) can be determined
individually from bottom to top or with single value.
Any of the heights can be left zero, to indicate that there is no vacuum between the chips or substrate between
the vacuum boxes.
Also, the metal thickness (metal_height
) can be set to zero, but that means the metal layer is modelled as
infinitely thin sheet.
A dielectric layer is added on top of the metal layer if non-zero dielectric_height
is given.
Simulation subclass
Subclassing the Simulation
is similar to subclassing the Element
, since both classes support most
of the same concepts.
For example, Refpoints can be used to connect child elements together and
simulations can have Parameters with the same syntax as in Element
.
A simulation subclass can inherit parameters from regular elements with the add_parameter()
and
add_parameters_from()
decorators.
Single element subclass
To save you the trouble of writing a Simulation
subclass for single element simulations, you can use the
get_single_element_sim_class()
class builder method, provided
that the element class to be simulated has the get_sim_ports()
method
implemented.
For example, suppose we want to simulate a Swissmon
qubit.
The simplest way to do it is to use the class builder to build a single element simulation:
from kqcircuits.qubits.swissmon import Swissmon
from kqcircuits.simulations.single_element_simulation import get_single_element_sim_class
from kqcircuits.util.export_helper import get_active_or_new_layout
sim_class = get_single_element_sim_class(Swissmon) # Builds a simulation class for Swissmon
layout = get_active_or_new_layout()
sim_parameters = {} # Dictionary of Swissmon parameters
simulation = sim_class(layout, **sim_parameters) # Builds an instance of the simulation class
Returned sim_class
is a dynamically built subclass of Simulation
that contains a cell of
the Swissmon qubit placed at the center of the simulation box.
sim_class
can be instantiated with a parameters dict that sets the parameter values to the internal Swissmon PCell.
You can see that currently
the Swissmon code
defines one RefpointToSimPort
object to return in the
get_sim_ports
method. That is the JunctionSimPort
,
which with default arguments places an internal port between refpoints "port_squid_a"
and "port_squid_b"
.
Suppose we want to also have waveguides connected to the Swissmon couplers in the simulation. We can do this
by simply adapting the function get_sim_ports()
to additionally return
WaveguideToSimPort
objects that lead from refpoints on the Swissmon couplers "port_cplr0"
,
"port_cplr1"
and "port_cplr2"
to refpoint specified by the towards
keyword argument of
WaveguideToSimPort
. That is:
@classmethod
def get_sim_ports(cls, simulation):
return [JunctionSimPort(), WaveguideToSimPort('port_cplr0'),
WaveguideToSimPort('port_cplr1'), WaveguideToSimPort('port_cplr2')]
If we then decide to not produce the waveguides for the next simulation, instead of reverting the change we just made
to Swissmon
we can specify which refpoints should not generate ports in the simulation object:
sim_class = get_single_element_sim_class(Swissmon, ignore_ports=['port_cplr0', 'port_cplr1', 'port_cplr2'])
For more information on how to use the get_single_element_sim_class()
simulation class builder, please consult the API docs for the method
as well as the API docs for different implementations of the RefpointToSimPort
.
General subclass
Instead of using the class builder we can also create the simulation subclass manually. The following code snippet
implements essentially the same simulation class as was returned by the
get_single_element_sim_class()
class builder:
from kqcircuits.pya_resolver import pya
from kqcircuits.qubits.swissmon import Swissmon
from kqcircuits.simulations.port import InternalPort
from kqcircuits.simulations.simulation import Simulation
from kqcircuits.util.export_helper import get_active_or_new_layout
from kqcircuits.util.parameters import add_parameters_from
@add_parameters_from(Swissmon, '*', junction_type="Sim", fluxline_type="none")
class SwissmonSimulation(Simulation):
def build(self):
# Place a Swissmon qubit in the center of the simulation
_, refpoints = self.insert_cell(Swissmon, trans=pya.DTrans(self.box.center()))
# Add waveguide ports to the three couplers
self.produce_waveguide_to_port(refpoints['port_cplr0'], refpoints['port_cplr0_corner'], 1, 'left')
self.produce_waveguide_to_port(refpoints['port_cplr1'], refpoints['port_cplr1_corner'], 2, 'top')
self.produce_waveguide_to_port(refpoints['port_cplr2'], refpoints['port_cplr2_corner'], 3, 'right')
# Add junction port
self.ports.append(InternalPort(4, refpoints['squid_port_squid_a'], refpoints['squid_port_squid_b']))
layout = get_active_or_new_layout()
sim_parameters = {} # Dictionary of Swissmon parameters
simulation = SwissmonSimulation(layout, **sim_parameters) # Builds an instance of the simulation class
This could be a better approach if further flexibility is required, for example, to place multiple elements into the same simulation or to simulate full chips or portions of the chip.
Geometry sweeps
Once a simulation subclass is defined, instance of it can be created with desired parameter values by passing keyword arguments to the constructor. An instance of a simulation subclass (also called a simulation object) represents single geometry variation. The procedure to create and simulate multiple geometry variations is to create multiple simulation objects and store them into a list.
There are helper functions sweep_simulation()
and cross_sweep_simulation()
to ease the construction
of geometry sweeps. The difference of these functions is that the sweep_simulation()
varies single parameter
at time as the cross_sweep_simulation()
cross-varies the parameters to go through all parameter combinations.
The following script shows how to generate some instances of the simulation subclass and create sweep over the gap_width parameter:
from kqcircuits.qubits.swissmon import Swissmon
from kqcircuits.simulations.export.simulation_export import sweep_simulation
from kqcircuits.simulations.single_element_simulation import get_single_element_sim_class
from kqcircuits.util.export_helper import get_active_or_new_layout
sim_class = get_single_element_sim_class(Swissmon) # Builds a simulation class for Swissmon
layout = get_active_or_new_layout()
simulations = []
# Generate the simulation with default parameters
simulations.append(sim_class(layout))
# Generate the simulation for some other parameter
simulations.append(sim_class(layout, arm_length=[500, 500, 500, 500], name='arm_length_500'))
# Make a 4-point sweep of gap width
simulations += sweep_simulation(
layout,
sim_class,
sim_parameters={
'name': 'gap_sweep',
'arm_length': [500, 500, 500, 500],
},
sweeps={
'gap_width': [[x, x, x, x] for x in [10, 15, 20, 25]],
}
)