1. Coupled Xmons
1.1. Requirements
1.1.1. Software components
QTCAD®
Gmsh
KLayout (optional, for layout inspection)
1.1.2. Python script
qtcad/examples/tutorials/builder_sc_transmon_single.py
1.1.3. Layout file
qtcad/examples/tutorials/layouts/sc_transmon_single.oas
1.2. Briefing
In this short tutorial, we use an OASIS layout file describing a simple device with two coupled Xmons to demonstrate how to generate the 3D geometry and mesh files required for electromagnetic QTCAD simulations.
Fig. 1.2.4 Layout file opened in KLayout.
1.3. Setup
1.3.1. Header
We begin by importing the required modules and defining the working directories.
We first import the Builder class from the builder
module.
Its instances allow us to construct the geometry from the 2D layout provided in the
OASIS file.
We also import MeshAlgorithm3D, which provides access to
the different meshing algorithms available in Builder for generating a
3D mesh.
from pathlib import Path
import numpy as np
from qtcad.builder import Builder, MeshAlgorithm3D
script_dir = Path(__file__).parent.resolve()
layout_dir = script_dir / "layouts"
mesh_dir = script_dir / "meshes"
out_dir = script_dir / "output"
out_dir.mkdir(exist_ok=True)
Let us also define an auxiliary function to preview the geometry in Gmsh as we build the device.
# Function to preview the geometry as we build the device.
# Change `flag_show` to `False` if you do not want the Gmsh window to be manually
# closed before proceeding
flag_show = True
def preview_geometry(
builder,
output_filepath,
font_size=20,
angles=(-45, 0, 0),
show=flag_show,
):
builder.view(
surfaces=True,
volume_labels=False,
surface_labels=True,
angles=angles,
save=output_filepath,
font_size=font_size,
show=flag_show,
)
1.3.2. Length scales and constants
We begin by setting the length scale for all geometric quantities to 1 μm. This scale factor will be used when loading the OASIS file and in all subsequent Builder operations.
# Scale: 1 μm.
scale = 1e-6
Next, we define the thickness of the dielectric substrate, which hosts the Xmons and the resonator, and of the surrounding air box. The air box is a region added around the physical device, extending the simulation domain of electromagnetic (EM) simulations, allowing the solver to capture the fields surrounding the device. In fact, a significant portion of the fields will be found above (air) the resonant components, in addition to below (substrate) them.
# Thickness of the substrates.
thickness_substrate = 280
# Padding to add when enveloping the whole device in an air box.
airbox_padding_lateral = 200
airbox_padding_vertical = 300
Now, let us specify the thickness of the superconducting (SC) sheets.
In this tutorial, we set thickness_sc to zero, so the SC layers are
represented as 2D surfaces.
However, the necessary logic is included to extrude these surfaces into 3D
sheets when thickness_sc is nonzero.
# Thickness of the SC sheet.
# A suitable non-zero value would be 0.15 μm, here we set it to zero
# such that the SC sheet is added to the design as a 2D surface.
thickness_sc = 0
Lastly, let us specify the target characteristic length of the mesh.
# Characteristic length of the mesh.
mesh_size = 300
1.4. Loading the layout
We begin by instantiating a Builder object to load the
OASIS layout file using the load_layout method.
Here, we specify cell_name="TOP" to load the top-level cell of the layout.
Moreover, we specify the target characteristic length of the mesh using
set_mesh_size.
# Instantiate Builder and load the layout file.
builder = Builder(name="transmon-chip", length_unit_exponent=int(np.log10(scale)))
builder.load_layout(layout_dir / "sc_transmon_single.oas", cell_name="TOP")
# Set mesh size.
builder.set_mesh_size(mesh_size)
At this stage, the Builder is initialized and connected to the geometry stored in the
layout file.
Then, with the print_mask_tree method, we
can verify which masks and their associated polygons were loaded.
Note that each layer in the layout file is associated to a ‘mask’ in Builder.
# Show the list of layers and polygons.
builder.print_mask_tree()
The mask tree provides an overview of the masks imported from the layout and, in our case, will look like
Layout
├── Mask 1 "layer_1"
│ ├── 0 Polygon "ground_plane" (1341)
│ ├── 1 Polygon "bus_lr" (1294)
│ ├── 2 Polygon "xmon_l" (12)
│ ├── 3 Polygon "coupler_r" (10)
│ ├── 4 Polygon "coupler_l" (10)
│ └── 5 Polygon "xmon_r" (12)
└── Mask 10 "layer_10"
├── 0 Polygon "jj_l" (4)
└── 1 Polygon "jj_r" (4)
In the above, we can verify that each named object in the layout file was correctly identified.
In particular, the layout contains two masks corresponding to the SC sheet and the rectangle representing the Josephson junction. Each mask hosts several named polygon objects associated with different components.
1.5. Building the chip
To construct the SC chip, we will merge and extrude the different elements to form the desired 3D geometry using the related Builder operations. Whilst several approaches are possible with Builder, here, we will first create the SC sheet using the associated layers and add the rectangular ports representing the junctions. Next, we create the chip holder (substrate) and add an ‘air box’.
Note that, when building 3D structures, Builder supports multiple merge strategies
(fragmentation modes).
Since we will first create the chip using the available layers and finish with the
surrounding air box, let us use Builder’s
fill_mode.
It ensures that new entities will inherit existing physical groups at intersections,
such that previously named entities will remain identifiable.
# Fill mode: new entities will inherit existing physical groups
# at intersections.
builder.fill_mode()
Moreover, as we add elements from the masks to the geometry, Builder will create addressable physical groups named following certain naming modes.
Here, several SC components can be found in the same layer/mask and are individually
labelled.
Therefore, we need to make sure that new entities will be assigned to a physical group
that is named according to the name of the shape from which that the entity is created.
This is achieved via the use of
group_from_shape.
# Make sure new entities created will be assigned to a physical group
# that is named according the source shape.
builder.group_from_shape()
Later, when adding the substrates and the air box, however, we will specify the
associated physical groups manually using
set_group_name.
1.5.1. Adding the SC components and the Josephson-junction ports
Let us now add the SC layer. To do that, we only need to activate (‘use’) the relevant mask, set the z-coordinate and add the elements.
For a layer with vanishing thickness, we add a surface using
add_surface.
For a layer with finite thickness, we add the volume using
extrude.
# Add all the elements of layer 1 at z=0.
builder.use_mask("layer_1")
# z=0 is the starting point, but we set it explicitly.
builder.set_z(0)
if thickness_sc > 0:
builder.extrude(thickness_sc)
else:
builder.add_surface()
Next, we add the rectangular lumped ports, available in layer 10 and representing the Josephson junctions, as simple 2D surfaces.
# Add the rectangular ports representing the junctions.
builder.use_mask("layer_10")
# If `thickness_sc` was non-zero previously, we would be at `z=thickness_sc`.
# Hence, let us guarantee we are at `z=0`.
builder.set_z(0)
builder.add_surface()
preview_geometry(
builder, out_dir / "builder_sc_transmon_single_step-1.png", angles=(0, 0, 0)
)
1.5.2. Chip holder
Let us now add the substrate for the chip.
As expected, there is no element representing the substrate in the layout file. This does not pose a problem since we can use the limits of the SC sheet to define the appropriate substrate volume.
With Builder, this can be done via
extrude_mask_bbox, which extrudes the
bounding box of a given mask by a certain amount.
Also, we will manually define the name of the physical groups to our liking using
set_group_name.
# Extrudes the bounding box of layer 1 (SC sheet) to create the substrate.
builder.set_z(0)
builder.set_group_name("substrate")
builder.extrude_mask_bbox(height=-thickness_substrate, mask="layer_1")
preview_geometry(builder, out_dir / "builder_sc_transmon_single_step-2.png")
1.5.3. Air box
Let us then create a volume surrounding the whole device for the air box needed for electromagnetic simulations.
This can be trivially done using the
wrap_in_bbox method, which wraps a group or
the entire model in a bounding box with some given padding.
# Envelop the whole device in an air box.
builder.set_group_name("air")
builder.wrap_in_bbox(
padding=(airbox_padding_lateral, airbox_padding_lateral, airbox_padding_vertical)
)
preview_geometry(
builder, out_dir / "builder_sc_transmon_single_step-3.png", angles=(-45, 0, -15)
)
1.6. Generating the mesh
Finally, we generate and export the 3D mesh using the HXT algorithm. HXT is a parallelized tetrahedral meshing algorithm that efficiently generates high-quality unstructured meshes for complex device geometries.
# Generate the mesh and save it.
builder.mesh(3, algorithm3d=MeshAlgorithm3D.HXT, show_gmsh_output=True)
builder.write(mesh_dir / "builder_sc_transmon_single.msh")
We also export a XAO geometry file, which is needed if we want to perform EM simulations using adaptive-mesh refinement.
# Export the geometry as an XAO file.
builder.write(mesh_dir / "builder_sc_transmon_single.xao")
Lastly, let us visualize the generated mesh.
# Visualize the mesh.
builder.view(
surfaces=False,
volume_labels=True,
angles=(-45, 0, -15),
save=str(out_dir / "builder_sc_transmon_single_mesh.png"),
)
Fig. 1.6.5 Final 3D mesh of the device.
The generated geometry and mesh files can then be used to run electromagnetic simulations using QTCAD.
This is not covered in this tutorial, but the interested reader is referred to Device package (superconducting circuits). There, we present examples of the different types of simulation and analyses than can be performed using QTCAD.
1.7. Full code
__copyright__ = "Copyright 2022-2026, Nanoacademic Technologies Inc."
from pathlib import Path
import numpy as np
from qtcad.builder import Builder, MeshAlgorithm3D
script_dir = Path(__file__).parent.resolve()
layout_dir = script_dir / "layouts"
mesh_dir = script_dir / "meshes"
out_dir = script_dir / "output"
out_dir.mkdir(exist_ok=True)
# Function to preview the geometry as we build the device.
# Change `flag_show` to `False` if you do not want the Gmsh window to be manually
# closed before proceeding
flag_show = True
def preview_geometry(
builder,
output_filepath,
font_size=20,
angles=(-45, 0, 0),
show=flag_show,
):
builder.view(
surfaces=True,
volume_labels=False,
surface_labels=True,
angles=angles,
save=output_filepath,
font_size=font_size,
show=flag_show,
)
# Scale: 1 μm.
scale = 1e-6
# Thickness of the substrates.
thickness_substrate = 280
# Padding to add when enveloping the whole device in an air box.
airbox_padding_lateral = 200
airbox_padding_vertical = 300
# Thickness of the SC sheet.
# A suitable non-zero value would be 0.15 μm, here we set it to zero
# such that the SC sheet is added to the design as a 2D surface.
thickness_sc = 0
# Characteristic length of the mesh.
mesh_size = 300
# Instantiate Builder and load the layout file.
builder = Builder(name="transmon-chip", length_unit_exponent=int(np.log10(scale)))
builder.load_layout(layout_dir / "sc_transmon_single.oas", cell_name="TOP")
# Set mesh size.
builder.set_mesh_size(mesh_size)
# Show the list of layers and polygons.
builder.print_mask_tree()
# Fill mode: new entities will inherit existing physical groups
# at intersections.
builder.fill_mode()
# Make sure new entities created will be assigned to a physical group
# that is named according the source shape.
builder.group_from_shape()
# Add all the elements of layer 1 at z=0.
builder.use_mask("layer_1")
# z=0 is the starting point, but we set it explicitly.
builder.set_z(0)
if thickness_sc > 0:
builder.extrude(thickness_sc)
else:
builder.add_surface()
# Add the rectangular ports representing the junctions.
builder.use_mask("layer_10")
# If `thickness_sc` was non-zero previously, we would be at `z=thickness_sc`.
# Hence, let us guarantee we are at `z=0`.
builder.set_z(0)
builder.add_surface()
preview_geometry(
builder, out_dir / "builder_sc_transmon_single_step-1.png", angles=(0, 0, 0)
)
# Extrudes the bounding box of layer 1 (SC sheet) to create the substrate.
builder.set_z(0)
builder.set_group_name("substrate")
builder.extrude_mask_bbox(height=-thickness_substrate, mask="layer_1")
preview_geometry(builder, out_dir / "builder_sc_transmon_single_step-2.png")
# Envelop the whole device in an air box.
builder.set_group_name("air")
builder.wrap_in_bbox(
padding=(airbox_padding_lateral, airbox_padding_lateral, airbox_padding_vertical)
)
preview_geometry(
builder, out_dir / "builder_sc_transmon_single_step-3.png", angles=(-45, 0, -15)
)
# Generate the mesh and save it.
builder.mesh(3, algorithm3d=MeshAlgorithm3D.HXT, show_gmsh_output=True)
builder.write(mesh_dir / "builder_sc_transmon_single.msh")
# Export the geometry as an XAO file.
builder.write(mesh_dir / "builder_sc_transmon_single.xao")
# Visualize the mesh.
builder.view(
surfaces=False,
volume_labels=True,
angles=(-45, 0, -15),
save=str(out_dir / "builder_sc_transmon_single_mesh.png"),
)