Creating a simulation object¶
To export a simulation, the geometry to simulate must be defined in a simulation object, which is an instance of the
Simulation
class. The Simulation
class works very similar to regular KQCircuits elements: the
simulation should implement a build()
method which generates the
geometry, typically by inserting other elements. See also the build()
method of class Element
.
Note
Often, a simulation is defined in code by implementing a subclass of Simulation
. This allows arbitrary
geometry and port definitions. To save you the trouble of writing a Simulation
subclass for single
element simulations, you can use the
get_single_element_sim_class()
classbuilder method, provided that the element class to be simulated has the
get_sim_ports()
method implemented.
There are also macros and convenience methods to create simulations directly
from existing geometry. See Geometry from KLayout GUI below for more information on these.
The Simulation
class supports most of the same concepts as Element
.
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 can
inherit parameters from regular elements with the add_parameter()
and add_parameters_from()
decorators.
The parameter values can be set when creating an instance of the simulation, by passing keyword arguments to the
constructor. Parameters are often used to export parameter sweeps of a simulation, and there are helper functions
sweep_simulation()
and cross_sweep_simulation()
to generate sweeps. See Simulation scripts
for examples on their usage.
Simulation box¶
The Simulation
also has some extra features that are important for creating simulations. First of all,
the simulation area is defined by the box
parameter, which should be a pya.DBox
.
The simulation area should not be overly large; consider what is necessary for the purpose. Since box
is a
parameter, it can be set during export.
Ports¶
Ports define the inputs and outputs of the simulation. Two types of ports are supported, EdgePort
at the edge
of the simulation box and InternalPort
for ports inside the geometry. Ports are defined in the build
method by adding instances of the corresponding port class to the pre-defined Simulations.ports
list.
To create an internal port, two points signal_location
and ground_location
must be supplied as pya.DPoint
.
These must be exactly on the midpoint of geometry edges in the simulation. The actual port will be drawn as a rectangle
touching these edges. For example, the following snippet creates an internal port across the junction of a single-island qubit,
where refp
is a Refpoints
instance 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"])
)
The InternalPort
is mapped to a Lumped Port in Ansys HFSS. In Q3D and Elmer capacitance simulations only
signal_location
is used and ground_location
can be omitted. For qubits with multiple islands, usually a
separate port is needed for each island.
Edge ports can be created similarly by adding EdgePort
instances. Edge ports only
have a signal_location
, and it must be on the edge of Simulation.box
.
For ports that connect via waveguides, the convenience method
produce_waveguide_to_port()
draws both the waveguide and adds
the required port. 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")
Example simulation¶
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
view = KLayoutView()
sim_parameters = {...} # Some Swissmon parameters
sim_class = get_single_element_sim_class(Swissmon) # Builds a simulation class for Swissmon
simulation = sim_class(view.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 having get_sim_ports()
return WaveguideToSimPort
objects
that lead to refpoints "port_cplr0"
, "port_cplr1"
and "port_cplr2"
:
@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
.
Instead of using the class builder we can also code the simulation class by hand. 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.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(3, refpoints['squid_port_squid_a'], refpoints['squid_port_squid_b']))
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.
Simulation scripts¶
Once a simulation class is defined, instances of it can be created with desired parameter values, and the instances can be exported as geometry or to one of the supported simulation tools. The following example script shows how to generate some instances and sweep a parameter, and export the resulting geometry as an OAS file.:
from kqcircuits.klayout_view import KLayoutView
from kqcircuits.qubits.swissmon import Swissmon
from kqcircuits.simulations.single_element_simulation import get_single_element_sim_class
from kqcircuits.simulations.export.simulation_export import export_simulation_oas, sweep_simulation
from kqcircuits.util.export_helper import create_or_empty_tmp_directory
view = KLayoutView()
simulations = []
# Using the class builder to define the simulaiton class of Swissmon
sim_class = get_single_element_sim_class(Swissmon)
# Generate the simulation with default parameters
simulations.append(sim_class(view.layout))
# Generate the simulation for some other parameter
simulations.append(sim_class(view.layout, arm_length=[500, 500, 500, 500], name='arm_length_500'))
# Make a 4-point sweep of gap width
simulations.extend(sweep_simulation(
view.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]],
}
))
# Export the list of simulation instances as OAS file in the kqcircuits/tmp/swissmon_simulation_output folder
dir_path = create_or_empty_tmp_directory("swissmon_simulation_output")
export_simulation_oas(simulations, dir_path)
This script can be run as a regular python script, or in the KLayout macro editor.
The output files will be written to a folder in the KQCircuits tmp
folder.
The example above only exports an OAS file with the geometry. See the following sections for information on exporting to different simulation tools.
More example scripts are available in klayout_package/python/scripts/simulations.
Geometry from Klayout GUI¶
An alternative way to export simulations is by drawing the geometry (placing elements or drawing manually) in KLayout, and running one of the following macros.
Similarly, simulation instances can be created from an existing KLayout Cell cell
in code:
simulation = Simulation.from_cell(cell, name='Dev', margin=100)
These methods export the geometry, but do not add any ports to the simulation. Hence, this is most useful if you want to manually create ports or make other changes for example in Ansys.