2. Creating the FD-SOI Device

2.1. Requirements

2.1.1. Software components

  • QTCAD

2.1.2. Python script

  • qtcad/examples/practical_application/FDSOI/double_dot_fdsoi.py

2.1.3. References

2.2. Briefing

The file presented in this section is not meant to be run. Instead, it contains quantities and a function that are imported by other scripts in this practical application tutorial series. In particular, the function get_double_dot_fdsoi defined here is used to create the FD-SOI Device object. This function is analogous to the identically named function defined in A double quantum dot device in a fully-depleted silicon-on-insulator transistor.

2.4. Default values

We then define some default values that will be used in the different scripts.

# Default values
V_set       = 1.25              # Default SET potential (V)
V_qubit     = 0.75              # Default qubit potential (V)
scaling     = 1e-9              # scaling factor for the mesh
valley_splitting = 0.5e-3 * ct.e

These provide the default voltages to be applied to the single-electron transistor (SET) and qubit gates (plunger_gate_1_bnd and plunger_gate_2_bnd, respectively) and also define the length scale of the mesh. We also define a default valley splitting, denoted by \(\Delta_v\), which will later be passed to the device constructor so the example can explicitly include valley physics in the silicon regions. Here we take \(\Delta_v = 0.5\,\mathrm{meV}\), which is representative of experimentally observed valley splittings in silicon MOS/SiO2 quantum dots [YRR+13].

2.5. Visualization helpers

Next, we define helper functions for visualization and post-processing. The first one, save_slice, saves a slice of the input data normal to the growth (z) direction, \(1\, \mathrm{nm}\) below the gate oxides. The second one, state_probability_density, converts a Schrödinger eigenstate into a scalar probability density by computing \(\sum_\nu |\psi_{i,\nu}(\mathbf{r})|^2\) over the band axis when spin and/or valley components are present.

# Visualization parameters
normal = (0., 0., 1.) # the plane of the slice is normal to the z axis
origin = (0., 0., -1. * scaling) # the slice is inside the Si quantum well

def save_slice(
    device: Device | SubDevice,
    data: NDArray,
    file_name: str | pathlib.Path,
    label: str
)-> None:
    """Helper function to save a slice of the device data.

    Args:
        device: The Device object containing the data.
        data: The data array to be sliced and saved.
        file_name: The filename to save the slice plot.
        label: The label for the colorbar axis.
    """
    plot_slice(device.mesh, data, normal=normal, origin=origin, cb_axis_label=label,
        path=file_name, show_figure=False)

def state_probability_density(device: Device | SubDevice, state: int) -> NDArray:
    """Return the scalar probability density associated with an eigenstate.

    For multiband solutions, such as the explicit valley-resolved electron
    states used in this FD-SOI example, the final axis indexes band, spin,
    and/or valley components. Summing
    :math:`\\sum_\\nu |\\psi_{i,\\nu}(\\mathbf{r})|^2` over that axis recovers
    the scalar density used for visualization and charge-density
    calculations.

    Args:
        device: Device containing previously computed eigenfunctions.
        state: Single-particle state index for which to compute the
            probability density.

    Returns:
        Scalar probability density evaluated on the device mesh.
    """
    density = np.abs(device.eigenfunctions[:, state])**2
    if density.ndim > 1:
        density = np.sum(density, axis=-1)
    return density

The state_probability_density helper is used throughout the later practical-application scripts. It keeps the examples readable while making the valley-aware indexing explicit: the state index is chosen first, and only then are the associated band/valley components combined into a scalar density.

2.6. Creating the device

Finally, we define the function that creates and returns the FD-SOI device model.

# Function to create the FD-SOI device
def get_double_dot_fdsoi(
    mesh_file_name: str = "refined_dqdfdsoi.msh",
    V_set: float = V_set,
    V_qubit: float = V_qubit,
    valley_splitting: float = valley_splitting,
) -> tuple[Device, list[str], list[str]]:
    """Create a Device object for the FD-SOI structure.

    Args:
        mesh_file_name: Filename of the mesh file.
        V_set: Potential applied to the SET gate.
        V_qubit: Potential applied to the qubit gate.
        valley_splitting: Valley splitting applied to the device.

    Returns:
        Device: The constructed Device object.
        list[str]: List of region names associated with the SET.
        list[str]: List of region names associated with the qubit quantum-dot.
    """
    # Create the mesh
    script_dir           = pathlib.Path(__file__).parent.resolve()
    path_mesh            = script_dir / "meshes"
    mesh_file            = path_mesh / mesh_file_name
    mesh                 = Mesh(scaling, mesh_file)

    # Define the device object
    dvc = Device(mesh, conf_carriers='e')
    dvc.set_temperature(0.1)

    # Create the regions
    dvc.new_region("oxide", mt.SiO2)
    dvc.new_region("oxide.QD1", mt.SiO2)
    dvc.new_region("oxide.QD2", mt.SiO2)
    dvc.new_region("channel", mt.Si)
    dvc.new_region("channel.QD1", mt.Si)
    dvc.new_region("channel.QD2", mt.Si)
    dvc.new_region("source", mt.Si, ndoping=1e20*1e6)
    dvc.new_region("drain", mt.Si, ndoping=1e20*1e6)

    # Set up boundary conditions
    Ew = mt.Si.Eg/2 + mt.Si.chi # Midgap
    dvc.new_gate_bnd("barrier_gate_1_bnd", 0.5, Ew)
    dvc.new_gate_bnd("plunger_gate_1_bnd", V_set, Ew)
    dvc.new_gate_bnd("barrier_gate_2_bnd", 0.5, Ew)
    dvc.new_gate_bnd("plunger_gate_2_bnd", V_qubit, Ew)
    dvc.new_gate_bnd("barrier_gate_3_bnd", 0.5, Ew)
    dvc.new_ohmic_bnd("source_bnd")
    dvc.new_ohmic_bnd("drain_bnd")
    dvc.new_frozen_bnd("back_gate_bnd", -0.5, mt.Si, 1e15*1e6,
        "n", 46*1e-3*ct.e)

    # Create the double quantum dot region
    SET_region_list = ["oxide.QD1", "channel.QD1"]
    qubit_region_list = ["oxide.QD2", "channel.QD2"]

    # Add valley splitting
    dvc.set_valley_splitting(valley_splitting)

    return dvc, SET_region_list, qubit_region_list

This function starts by generating the Mesh object using the specified mesh file. It then creates an instance of the Device class and sets up the material stack of the device by defining the different regions and assigning them appropriate materials. Next, it applies appropriate boundary conditions to the surfaces representing the gate contacts. The plunger-gate voltages are set according to the function arguments. In contrast, barrier-gate voltages are all set to a value of \(0.5\,\mathrm{V}\). This value of barrier-gate voltage was shown in Tunnel coupling in a double quantum dot in FD-SOI—Part 2: Tuning the barrier gate to lead to minimal tunneling between different regions of the device, allowing the quantum dots to be well-defined and isolated. The function also takes a valley_splitting argument, which is passed to set_valley_splitting Device method. The detailed consequences of this explicit valley splitting for the single-particle energies and the indexing of the eigenfunctions array are introduced later in Computing the chemical potential of an SET device, when the Schrödinger spectrum first appears explicitly in the practical application.

In addition to the Device object itself, get_double_dot_fdsoi returns the lists identifying the SET region and the qubit region, which are used later in the practical application when defining subdevices for Schrödinger calculations.

Note

In the next section of the practical application (Simulating electrostatics using the linear Poisson solver), we import get_double_dot_fdsoi to construct the FD-SOI device model passed to the linear Poisson Solver to solve the linear Poisson equation. Because the linear Poisson Solver neglects free carriers, the doped "source" and "drain" regions do not contribute any mobile charge to the solution, and therefore their doping has little practical effect in this context. We nevertheless keep the doping definitions here so the same function can be reused to generate a device model for the nonlinear Poisson Solver, which does include free carriers. For an example where the doping becomes relevant, see Tunnel coupling in a double quantum dot in FD-SOI—Part 1: Plunger gate tuning.

2.7. Full code

__copyright__ = "Copyright 2022-2026, Nanoacademic Technologies Inc."

import pathlib
import numpy as np
from numpy.typing import NDArray
from qtcad.device import materials as mt
from qtcad.device import constants as ct
from qtcad.device.mesh3d import Mesh
from qtcad.device import Device, SubDevice
from qtcad.device.analysis import plot_slice

# Default values
V_set       = 1.25              # Default SET potential (V)
V_qubit     = 0.75              # Default qubit potential (V)
scaling     = 1e-9              # scaling factor for the mesh
valley_splitting = 0.5e-3 * ct.e

# Visualization parameters
normal = (0., 0., 1.) # the plane of the slice is normal to the z axis
origin = (0., 0., -1. * scaling) # the slice is inside the Si quantum well

def save_slice(
    device: Device | SubDevice, 
    data: NDArray, 
    file_name: str | pathlib.Path, 
    label: str
)-> None:
    """Helper function to save a slice of the device data.

    Args:
        device: The Device object containing the data.
        data: The data array to be sliced and saved.
        file_name: The filename to save the slice plot.
        label: The label for the colorbar axis.
    """
    plot_slice(device.mesh, data, normal=normal, origin=origin, cb_axis_label=label,
        path=file_name, show_figure=False)

def state_probability_density(device: Device | SubDevice, state: int) -> NDArray:
    """Return the scalar probability density associated with an eigenstate.

    For multiband solutions, such as the explicit valley-resolved electron
    states used in this FD-SOI example, the final axis indexes band, spin,
    and/or valley components. Summing
    :math:`\\sum_\\nu |\\psi_{i,\\nu}(\\mathbf{r})|^2` over that axis recovers
    the scalar density used for visualization and charge-density
    calculations.

    Args:
        device: Device containing previously computed eigenfunctions.
        state: Single-particle state index for which to compute the
            probability density.

    Returns:
        Scalar probability density evaluated on the device mesh.
    """
    density = np.abs(device.eigenfunctions[:, state])**2
    if density.ndim > 1:
        density = np.sum(density, axis=-1)
    return density

# Function to create the FD-SOI device
def get_double_dot_fdsoi(
    mesh_file_name: str = "refined_dqdfdsoi.msh",
    V_set: float = V_set,
    V_qubit: float = V_qubit,
    valley_splitting: float = valley_splitting,
) -> tuple[Device, list[str], list[str]]:
    """Create a Device object for the FD-SOI structure.

    Args:
        mesh_file_name: Filename of the mesh file.
        V_set: Potential applied to the SET gate.
        V_qubit: Potential applied to the qubit gate.
        valley_splitting: Valley splitting applied to the device.

    Returns:
        Device: The constructed Device object.
        list[str]: List of region names associated with the SET.
        list[str]: List of region names associated with the qubit quantum-dot.
    """
    # Create the mesh
    script_dir           = pathlib.Path(__file__).parent.resolve()
    path_mesh            = script_dir / "meshes"
    mesh_file            = path_mesh / mesh_file_name
    mesh                 = Mesh(scaling, mesh_file)

    # Define the device object
    dvc = Device(mesh, conf_carriers='e')
    dvc.set_temperature(0.1)

    # Create the regions
    dvc.new_region("oxide", mt.SiO2)
    dvc.new_region("oxide.QD1", mt.SiO2)
    dvc.new_region("oxide.QD2", mt.SiO2)
    dvc.new_region("channel", mt.Si)
    dvc.new_region("channel.QD1", mt.Si)
    dvc.new_region("channel.QD2", mt.Si)
    dvc.new_region("source", mt.Si, ndoping=1e20*1e6)
    dvc.new_region("drain", mt.Si, ndoping=1e20*1e6)

    # Set up boundary conditions
    Ew = mt.Si.Eg/2 + mt.Si.chi # Midgap
    dvc.new_gate_bnd("barrier_gate_1_bnd", 0.5, Ew)
    dvc.new_gate_bnd("plunger_gate_1_bnd", V_set, Ew)
    dvc.new_gate_bnd("barrier_gate_2_bnd", 0.5, Ew)
    dvc.new_gate_bnd("plunger_gate_2_bnd", V_qubit, Ew)
    dvc.new_gate_bnd("barrier_gate_3_bnd", 0.5, Ew)
    dvc.new_ohmic_bnd("source_bnd")
    dvc.new_ohmic_bnd("drain_bnd")
    dvc.new_frozen_bnd("back_gate_bnd", -0.5, mt.Si, 1e15*1e6, 
        "n", 46*1e-3*ct.e)

    # Create the double quantum dot region
    SET_region_list = ["oxide.QD1", "channel.QD1"]
    qubit_region_list = ["oxide.QD2", "channel.QD2"]

    # Add valley splitting
    dvc.set_valley_splitting(valley_splitting)

    return dvc, SET_region_list, qubit_region_list