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.

../../../_images/builder_sc_transmon_single_klayout.png

Fig. 1.2.4 Layout file opened in KLayout.

1.3. Setup

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)
)
../../../_images/builder_sc_transmon_single_step-1.png

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")
../../../_images/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)
)
../../../_images/builder_sc_transmon_single_step-3.png

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"),
)
../../../_images/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"),
)