Skip to content

Minimal Kitaev Chain

Authorship

The file minimal_kitaev_chain.dxf used in this tutorial was developed by PhD student Dhananjay Joshi from the Goswami group at the Qubit Research division of QuTech.

This page uses the minimal-Kitaev-chain layout as a second worked example of the public Tempura workflow:

  1. choose an area of interest from the source DXF file;
  2. rasterize that layout window with prepare_layout(...);
  3. describe the physical stack with an ordered stack;
  4. build the finalized Device with build_device(...);
  5. choose per-layer resolution=[dx, dy, dz] values;
  6. build and solve the pescado problem;
  7. inspect representative mesh cuts through the finalized problem.

For the generated reference API, see API Reference.

Core Ideas

The same contracts from the other layout-backed examples apply here:

  • prepare_layout(...) is the layout step;
  • build_device(...) is the geometry step;
  • per-layer resolution=[dx, dy, dz] is the meshing step;
  • build_problem(...) assembles the pescado electrostatics problem;
  • solve_gate_potentials(...) computes one basis response per gate;
  • plot_problem_regions_with_mesh(...) visualizes cuts through the finalized mesh.

The important difference is that this example starts from a DXF file, so we use size_mode="explicit_x" and supply the physical x length explicitly.

Worked Example

1. Define the source layout window

The first step is again geometric: select the part of the source layout that will become the simulation grid, choose the internal grid constant, and decide where the physical device window should be sampled.

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from tempura.electrostatics.pescado_wrapper import (
    build_problem,
    solve_gate_potentials,
)
from tempura.formatting import format_mapping_block, format_voltage
from tempura.layout.extraction import (
    make_aoi_bbox_from_ranges,
    plot_gate_stencil_layers,
    plot_layout_layers,
)
from tempura.layout.layout_pipeline import (
    build_device,
    format_device_dimensions,
    format_roi_summary,
    prepare_layout,
)
from tempura.plotting import (
    add_plane_cut_guides,
    make_xy_emphasis_axes,
    make_standard_plane_specs,
    nearest_axis_value,
    plot_problem_regions_with_mesh,
    plot_scalar_field_on_planes,
    transpose_plane_spec,
)

selected_layers = [
    "L18D0_Al_fine_A",
    "L1009D0_G1_fine_A",
    "L1014D0_G2_fine_A",
]

Units after prepare_layout(...)

This example also switches into Tempura's internal grid units after layout preparation.

If grid_constant_m = 20e-9, then one internal unit is 20 nm.

That means:

  • the cropped AOI size is converted into grid units;
  • stack thicknesses are interpreted in those same grid units;
  • each layer's optional resolution=[dx, dy, dz] is also written in those same grid units;
  • in size_mode="explicit_x", the physical x length is fixed directly and the y length is inferred from the layout aspect ratio;
  • both physical ROI dimensions must be integer multiples of grid_constant_m.

2. Define the physical stack and layer resolutions

The stack still describes the physical layer ordering from bottom to top. Each gate entry names the source layout layer that should become one or more Tempura Gate objects. The values below are already written directly in Tempura's internal lattice units, so 1.0 means one grid_constant_m.

# The stack is written directly in internal units.
# With grid_constant_m = 20e-9, 1.0 means 20 nm.
# The top measurement gate is omitted here so the vertical cuts expose more of
# the buried gate stack in the later mesh plots.
stack = [
    {
        "kind": "dielectric",
        "name": "buffer",
        "permittivity": 12.9,
        "thickness": 4.0,
    },
    {
        "kind": "2deg",
        "name": "2deg",
        "permittivity": 12.9,
        "thickness": 1.0,
    },
    {
        "kind": "gate",
        "name": "gate_L18D0_Al_fine_A",
        "source_layer": "L18D0_Al_fine_A",
        "thickness": 1.0,
    },
    {
        "kind": "dielectric",
        "name": "blanket_1",
        "permittivity": 9.1,
        "thickness": 2.0,
    },
    {
        "kind": "gate",
        "name": "gate_L1009D0_G1_fine_A",
        "source_layer": "L1009D0_G1_fine_A",
        "thickness": 3.0,
    },
    {
        "kind": "dielectric",
        "name": "blanket_2",
        "permittivity": 9.1,
        "thickness": 2.0,
    },
    {
        "kind": "gate",
        "name": "gate_L1014D0_G2_fine_A",
        "source_layer": "L1014D0_G2_fine_A",
        "thickness": 4.0,
    },
]

3. Prepare the layout and apply the mask cleanup

prepare_layout(...) crops and rescales the DXF AOI in the same way as before, but this example intentionally uses a smaller physical width than the older 1.5 um version so the resulting device region stays manageable at 20 nm. It also needs two small layer-specific adjustments before we build the device:

  • the ohmic layer is removed from the electrostatics gate stack;
  • the aluminum opening is stored as an opening mask in the source layout, so we invert its rasterized stencil to obtain the metal region itself.
# Convert the chosen DXF window into Tempura's internal grid units.
prepared = prepare_layout(
    layout_path=examples_dir / "minimal_kitaev_chain.dxf",
    aoi_bbox=aoi_bbox,
    size_mode="explicit_x",
    grid_constant_m=grid_constant_m,
    physical_x_length=physical_x_length,
)

# Remove the ohmic source layer before building the electrostatic gate stack.
prepared["gate_stencils"].pop("L1005D0_ohmics_A", None)

# The aluminum opening is stored as an opening mask, so invert it to get metal.
if "L18D0_Al_fine_A" in prepared["gate_stencils"] and prepared["gate_stencils"]["L18D0_Al_fine_A"]:
    prepared["gate_stencils"]["L18D0_Al_fine_A"][0] = ~np.asarray(
        prepared["gate_stencils"]["L18D0_Al_fine_A"][0],
        dtype=bool,
    )

print(format_roi_summary(prepared))
ROI summary:
  layout_size: (1.0, 2.240000000000009)
  physical_size: (1e-06 m (1.0 um), 2.240000000000009e-06 m (2.240000000000009 um))
  grid_constant: 2e-08 m (0.02 um)
  rescaled_size: (50.0, 112.0)
  grid_shape: (ny=112, nx=50)
  grid_points: 5600

4. Inspect the layout layers used by the stack

At this stage we still have not built a Device. We are checking the selected source layers and the rasterized masks that will become gates. Each panel below shows one source layer with all disconnected gates from that layer overlaid.

figure = plot_gate_stencil_layers(
    prepared["gate_stencils"],
    roi_size=prepared["roi_size"],
    layer_order=selected_layers,
    title="Rasterized gate masks by layer",
    figsize=(10.8, 5.6),
    coordinate_scale=nm_per_unit,
    axis_unit="nm",
    layer_thicknesses={
        layer["source_layer"]: layer["thickness"]
        for layer in stack
        if layer["kind"] == "gate"
    },
)
plt.show()
plt.close("all")

png

5. Build the finalized Device

build_device(...) now consumes the prepared masks and produces a finalized Tempura Device.

# build_device(...) consumes the masks and finalizes the 3D layer geometry.
device = build_device(prepared, stack)

print(format_device_dimensions(device, prepared))
Device summary:
  Lx: 50.0
  Ly: 112.0
  grid: 1.0
  grid_shape: (ny=112, nx=50)
  grid_points: 5600
  layers: buffer, 2deg, gate_L18D0_Al_fine_A_0, blanket_1, gate_L1009D0_G1_fine_A_0, gate_L1009D0_G1_fine_A_1, blanket_2, gate_L1014D0_G2_fine_A_0, gate_L1014D0_G2_fine_A_1, gate_L1014D0_G2_fine_A_2, gate_L1014D0_G2_fine_A_3, gate_L1014D0_G2_fine_A_4, gate_L1014D0_G2_fine_A_5, gate_L1014D0_G2_fine_A_6
  physical_size: (1e-06 m (1.0 um), 2.240000000000009e-06 m (2.240000000000009 um))
  grid_constant: 2e-08 m (0.02 um)

6. Mesh the device and solve the electrostatics problem

# Build the pescado problem directly from the finalized per-layer resolutions.
problem_builder, region_shapes, gate_names = build_problem(
    device,
    vacuum_scale=2.0,
    verbose=False,
)

# Solve one basis potential per gate and export the 2DEG bundle.
basis_potentials, problem = solve_gate_potentials(
    problem_builder,
    region_shapes,
    gate_names,
    rhs_block_size=1,
    verbose=False,
)

7. Inspect the finalized mesh

Before plotting, regroup the disconnected polygons back onto their source-layer voltages and pin the displayed cuts to one central xz line and one yz line at x = -20 nm.

def layer_voltage_for_gate(gate_name: str, layer_voltages: dict[str, float]) -> float:
    """Return the grouped source-layer voltage assigned to one solved gate."""
    for source_layer, voltage in layer_voltages.items():
        if gate_name.startswith(f"gate_{source_layer}"):
            return float(voltage)
    return 0.0

The figure below shows three stacked cuts through the finalized Pescado mesh: one transposed xy plane through the patterned G2 layer at z = 260 nm, one central xz cut, and one yz cut slightly left of center.

plot_region_shapes = dict(region_shapes)
plot_region_shapes.pop("Boundary", None)

region_display_names = {}
for region_name in region_shapes:
    if region_name.startswith("gate_L18D0_Al_fine_A"):
        region_display_names[region_name] = "Al gates"
    elif region_name.startswith("gate_L1009D0_G1_fine_A"):
        region_display_names[region_name] = "G1 gates"
    elif region_name.startswith("gate_L1014D0_G2_fine_A"):
        region_display_names[region_name] = "G2 gates"

coordinates = np.asarray(problem.coordinates, dtype=float)
x_cut = nearest_axis_value(coordinates, 0, -1.0)
y_cut = nearest_axis_value(coordinates, 1, 0.0)
mesh_xy_z = 13.0
mesh_plane_specs = make_standard_plane_specs(
    device,
    xy_title=f"XY cut through G2 gates (z = {mesh_xy_z * nm_per_unit:.0f} nm)",
    xy_plane_z=mesh_xy_z,
    xz_title=f"XZ cut at y = {y_cut * nm_per_unit:.0f} nm",
    xz_plane_y=y_cut,
    yz_title=f"YZ cut at x = {x_cut * nm_per_unit:.0f} nm",
    yz_plane_x=x_cut,
)
mesh_plane_specs["XY"] = transpose_plane_spec(
    mesh_plane_specs["XY"],
    title=f"Transposed XY cut through G2 gates (z = {mesh_xy_z * nm_per_unit:.0f} nm)",
)
plane_axes = [mesh_plane_specs["XY"], mesh_plane_specs["XZ"], mesh_plane_specs["YZ"]]
figure, axes = make_xy_emphasis_axes(
    figsize=(11.2, 13.8),
    layout="stack",
    height_ratios=(2.0, 1.1, 0.9),
)

figure, _ = plot_problem_regions_with_mesh(
    problem,
    plot_region_shapes,
    plane_axes,
    axes=axes,
    region_display_names=region_display_names,
    suptitle="Minimal Kitaev chain mesh cuts",
    show_volume_edges=True,
    show_mesh_points=False,
    tile_edgecolors="#f3f3f3",
    coordinate_scale=nm_per_unit,
    axis_unit="nm",
)
add_plane_cut_guides(axes[0], x=y_cut, y=x_cut, coordinate_scale=nm_per_unit)
plt.show()
plt.close(figure)

png

8. Show A Dot-Like Voltage Configuration On The 2DEG Plane

The solved basis is still per disconnected polygon, but a static voltage state is easier to read when grouped back by source layer. The combination below highlights two dot-like islands along the G2-defined chain, and potential_vmin and potential_vmax expose the displayed color scale directly.

layer_voltages = {
    "L18D0_Al_fine_A": 0.,
    "L1009D0_G1_fine_A": 1,
    "L1014D0_G2_fine_A": -1.5,
}
potential_vmin = -1.5
potential_vmax = 1.5
combined_field = np.zeros(problem.coordinates.shape[0], dtype=float)
for gate_name in gate_names:
    combined_field += layer_voltage_for_gate(gate_name, layer_voltages) * basis_potentials[gate_name]
potential_xy_z = nearest_axis_value(
    coordinates,
    2,
    float(np.asarray(device.layers["2deg"].shape.bbox, dtype=float)[1, 2]),
)
potential_plane_specs = make_standard_plane_specs(
    device,
    xy_title=f"Potential on XY cut at t = {potential_xy_z * nm_per_unit:.0f} nm",
    xy_plane_z=potential_xy_z,
    xz_title=f"Potential on XZ cut at y = {y_cut * nm_per_unit:.0f} nm",
    xz_plane_y=y_cut,
    yz_title=f"Potential on YZ cut at x = {x_cut * nm_per_unit:.0f} nm",
    yz_plane_x=x_cut,
)
potential_plane_specs["XY"] = transpose_plane_spec(
    potential_plane_specs["XY"],
    title=f"Transposed XY cut at t = {potential_xy_z * nm_per_unit:.0f} nm",
)

fig, axes = make_xy_emphasis_axes(
    figsize=(11.2, 14.6),
    layout="stack",
    height_ratios=(2.0, 1.1, 0.9),
)
fig, axes = plot_scalar_field_on_planes(
    problem,
    combined_field,
    [
        potential_plane_specs["XY"],
        potential_plane_specs["XZ"],
        potential_plane_specs["YZ"],
    ],
    axes=axes,
    suptitle="Potential: Al = 0 V, G1 = +1.5 V, G2 = -1.5 V",
    colorbar_label="Potential (V)",
    vmin=potential_vmin,
    vmax=potential_vmax,
    symmetric=potential_vmin is None and potential_vmax is None,
    coordinate_scale=nm_per_unit,
    axis_unit="nm",
)
print(format_mapping_block("Layer voltages", layer_voltages, value_formatter=format_voltage))
plt.show()
plt.close(fig)
Layer voltages:
  L18D0_Al_fine_A: +0.00 V
  L1009D0_G1_fine_A: +1.00 V
  L1014D0_G2_fine_A: -1.50 V

png