6. Calculating the charge-stability diagram of a quantum dot

6.1. Requirements

6.1.1. Software components

  • QTCAD

6.1.2. Mesh file

  • qtcad/examples/practical_application/meshes/gated_dot.msh

6.1.3. Python script

  • qtcad/examples/practical_application/6-stability.py

6.1.4. References

6.2. Briefing

In the previous tutorial, we computed the chemical potentials and Coulomb peaks of a quantum dot, two physical quantities important to describe transport in the dot. These computations explicitly assumed a fixed value of the plunger gates potential of \(-0.4\textrm{ V}\) and implicitly assumed a fixed value of the source–drain voltage of \(0\textrm{ V}\) (more specifically in the calculation of the Coulomb peak positions). In this tutorial, we expand on the ideas of the previous tutorial and describe transport in the quantum dot—specifically in terms of differential conductance—as a function of the plunger gates potential and of the source–drain voltage. We will eventually obtain the charge-stability diagram of our quantum dot, which can be used to infer the number of electrons in the dot; for example, which configurations of the external voltages result in a single electron residing in the dot?

6.3. Setting up the device

6.3.2. Creating the junction

We start by defining the device as was previously done.

# Load the mesh
script_dir = pathlib.Path(__file__).parent.resolve()
path_mesh = str(script_dir/'meshes'/'gated_dot.msh')
scaling = 1e-6
mesh = Mesh(scaling, path_mesh)

# Create device
d = Device(mesh, conf_carriers="e")
d.set_temperature(0.1)

# # Applied potentials
Vtop_1 = -0.5
Vtop_2 = -0.4
Vtop_3 = -0.5
Vbottom_1 = -0.5
Vbottom_2 = -0.4
Vbottom_3 = -0.5

# Work function of the metallic gates at midgap of GaAs
barrier = 0.834*ct.e    # n-type Schottky barrier height

# Ionized dopant density
doping = 3e18*1e6   # In SI units

# Set up materials in heterostructure stack
d.new_region("cap", mt.GaAs)
d.new_region("barrier", mt.AlGaAs)
d.new_region("dopant_layer", mt.AlGaAs,ndoping=doping)
d.new_region("spacer_layer", mt.AlGaAs)
d.new_region("spacer_dot", mt.AlGaAs)
d.new_region("two_deg", mt.GaAs)
d.new_region("two_deg_dot", mt.GaAs)
d.new_region("substrate", mt.GaAs)
d.new_region("substrate_dot", mt.GaAs)

# Remove unphysical charge from cap and barrier
d.new_insulator("cap")
d.new_insulator("barrier")

# Align the bands across hetero-interfaces using GaAs as the reference material
d.align_bands(mt.GaAs)

# Set up boundary conditions
d.new_schottky_bnd("top_gate_1", Vtop_1, barrier)
d.new_schottky_bnd("top_gate_2", Vtop_2, barrier)
d.new_schottky_bnd("top_gate_3", Vtop_3, barrier)
d.new_schottky_bnd("bottom_gate_1", Vbottom_1, barrier)
d.new_schottky_bnd("bottom_gate_2", Vbottom_2, barrier)
d.new_schottky_bnd("bottom_gate_3", Vbottom_3, barrier)
d.new_ohmic_bnd("ohmic_bnd")

# Load the confinement potential from previous run
path_in = str(script_dir/"output"/"electric_potential_-0.400V.hdf5")
d.set_potential(io.load(path_in))

# Create a submesh in which to solve Schrodinger's equation
submesh = SubMesh(mesh, ["spacer_dot","two_deg_dot","substrate_dot"])

# Create subdevice
subdevice = SubDevice(d, submesh)

# Solver the single-electron problem to get basis set
slv = Solver(subdevice)
slv.solve()
subdevice.print_energies()

We are now ready to define a junction, i.e. a quantum dot coupled to two leads (which we may call “source” and “drain”). Note that the coupling between the dot and the leads is linear, and may in the simplest approximation be described by a single parameter: a tunneling amplitude between the dot and the lead. In this simplest case, the leads need not be explicitly defined in the mesh or device object—even though they could be using the WKBLead class in more realistic simulations that explicitly account for the lead wave functions (see the Broadening function: WKB calculation theory section and the Quantum transport—WKB approximation tutorial).

many_body_solver_params = SolverParams()
many_body_solver_params.num_states = 2  # Number of levels to keep
many_body_solver_params.n_degen = 2     # spin degenerate system
many_body_solver_params.alpha = 0.0714  # lever arm
jc = Junction(subdevice, many_body_solver_params=many_body_solver_params)

print('chemical potentials (eV):', jc.chem_potentials/ct.e)
print('positions of Coulomb peaks relative to reference value (V):',
    jc.coulomb_peak_pos)

Upon instantiation, the Junction object creates and runs a many-body Solver to calculate the chemical potentials, Coulomb peak positions, and many-body eigenstates of the subdevice, which here corresponds to the quantum dot. Indeed, these quantities are all needed for transport calculations with the master-equation solver. Therefore, the arguments of the junction used above are the same as those of the many-body solver in Calculating the Coulomb peaks of a quantum dot, with the exception that the parameter num_states is set to 2 rather than 3 to speed up example calculations (num_states may easily be increased for improved accuracy at the expense of extended computation time).

After the Coulomb integrals calculation ends, the values of the chemical potentials and Coulomb peaks are printed.

chemical potentials (eV): [-0.00461879  0.00383573  0.01785463  0.02442585]
positions of Coulomb peaks relative to reference value (V): [-0.06468898  0.05372171  0.2500649   0.34209876]

Note

The position of the Coulomb peaks is given relative to the reference gate potential. The reference gate potential is the potential applied at the boundary condition when solving Poisson’s equation. Here, this reference potential is Vtop_2 = Vbottom_2 = -0.4 V, so that this value should be added to the above peak positions to find the true potentials to be applied at the gates to observe transport.

6.4. Computing the current as a function of plunger gates potential

As a first example for the seq_tunnel_curr method, we compute the sequential tunneling charge current of our quantum dot at fixed drain-to-source voltage and varying plunger gates potential. To do so, we first set the source and drain potentials using the setVs and setVd methods of the Junction class.

# Set a near-zero source-drain bias
jc.setVs(0.0001)
jc.setVd(-0.0001)

Next, we set a constant value of \(10\;\textrm{MHz}\) for the source and drain broadening functions within the featureless approximation (see The sequential tunneling master equation).

jc.set_source_broad_func(10e6)
jc.set_drain_broad_func(10e6)

Here, our drain-to-source voltage is \(0.2\textrm{ mV}\). The plunger gates potential is uniformly sampled from \(-0.1\textrm{ V}\) to \(0.4\textrm{ V}\).

v_gate_rng = np.linspace(-0.1, 0.4, num=200)

Next, we initialize an empty list vec_Il in which the sequential tunneling charge current as a function of gate potential will be stored. We then loop over the elements of v_gate_rng. In each iteration, the junction plunger gate potential is adjusted using the setVg method. We then call the seq_tunnel_curr method on the Junction object jc, which returns the current in the left lead (i.e. the source), Il, the current in the right lead (i.e. the drain), Ir, and the occupation rate of each quantum dot state, prob, as computed using a master-equation approach (see Transport in the Coulomb blockade regime). Finally, Il is appended to vec_Il. Recall that the Il and Ir currents must be equal up to a negative sign due to charge conservation.

# Calculate the current for each plunger gate potential
vec_Il = []
for i in range(v_gate_rng.size):        # loop over gate voltage
    v_gate = v_gate_rng[i]
    jc.setVg(v_gate)
    Il,Ir,prob = seq_tunnel_curr(jc)    # transport calculation
    vec_Il += [Il]

Next, a tuple containing v_gate_rng and vec_Il is saved in the output directory as current.pkl.

path_out = str(script_dir/'output'/'current.pkl')
outfile = open(path_out, 'wb')
pickle.dump((v_gate_rng,vec_Il), outfile)
outfile.close()

In addition, vec_Il is plotted as a function of v_gate_rng. The resulting figure is saved as coulomb_peaks.png.

fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(1,1,1)
ax.set_xlabel("Gate potential (V)")
ax.set_ylabel("Current (pA)")
ax.plot(v_gate_rng, np.array(vec_Il)/1e-12)
path_fig = str(script_dir/"output"/"coulomb_peaks.png")
plt.savefig(path_fig)
plt.show()

This results in the figure below. Note that the positions of the local maxima of the current, i.e. the Coulomb peaks, perfectly match those calculated earlier at such small source–drain bias and low temperature. For larger source–drain bias and higher temperature, the Coulomb peaks would be broader.

Coulomb peaks

Fig. 6.4.9 Coulomb peaks with respect to reference gate potential (see the note in Creating the junction, above).

6.5. Computing the charge-stability diagram

As a second example for the seq_tunnel_curr method, we compute the charge-stability diagram of our quantum dot. The first step is to define the grid of plunger gate and drain potentials (respectively, v_gate_rng and v_drain_rng) over which we will sample the charge current.

### Charge-stability diagram ###
v_gate_rng = np.linspace(-0.2, 0.5, num=200)
v_drain_rng = np.linspace(-0.02, 0.02, num=50)

We then define diam, the array in which we will store the charge current. Its dimensions are v_gate_rng.size by v_drain_rng.size: the current will be computed for all pairs consisting in one element of v_gate_rng.size and one element of v_drain_rng.size.

diam = np.zeros((v_gate_rng.size,v_drain_rng.size), dtype=float)

Note that the data type of the elements of diam is specified to be dtype=float.

Since the master-equation calculation will be performed for each element of diam, we initialize a progress bar. The argument max = number_grid_points specifies the total number of such calculations.

number_grid_points = v_gate_rng.size * v_drain_rng.size # number of sampled grid points
progress_bar = Bar("Computing charge-stability diagram", max = number_grid_points) # initialize progress bar

Next, we iterate over all elements of v_gate_rng, adjusting the gate potential of the junction jc using the setVg method.

for i in range(v_gate_rng.size):        # loop over gate voltage
    v_gate = v_gate_rng[i]
    jc.setVg(v_gate)

We perform a nested iteration over the elements of v_drain_rng, adjusting the drain potentials using the setVd methods, computing the source current using the seq_tunnel_curr method, and storing the result, Il, into diam. To denote the progress of the calculation on the progress bar, the next() method is called on progress_bar at the end of each iteration of the inner loop.

for j in range(v_drain_rng.size):      # loop over drain voltage
    v_drain = v_drain_rng[j]
    jc.setVd(v_drain)
    Il,Ir,prob = seq_tunnel_curr(jc)    # transport calculation
    diam[i,j] = Il
    progress_bar.next()

Once the calculations performed using the nested for loops are done, we call the finish() method on progress_bar.

progress_bar.finish()

We then postprocess the current to turn it into an image to be plotted.

# Postprocess the current to turn it into an image to be plotted
current_image = np.flip(np.transpose(np.abs(diam)), axis=0)

Finally, this current is plotted on a color plot and saved as stability.png.

# plot results
fig, axs = plt.subplots()
axs.set_ylabel('$V_d(V)$',fontsize=16)
axs.set_xlabel('$V_g(V)$',fontsize=16)
diff_cond_map = axs.imshow(current_image/1e-12, interpolation='bilinear',
        extent=[Vtop_2+v_gate_rng[0], Vtop_2+v_gate_rng[-1],
                v_drain_rng[0], v_drain_rng[-1]], aspect="auto", cmap="Reds")
fig.colorbar(diff_cond_map, ax=axs, label="Current (pA)")
fig.tight_layout()
path_fig = str(script_dir/"output"/"stability.png")
plt.savefig(path_fig)
plt.show()

This results in the following diagram, which displays the Coulomb diamonds structure expected for a quantum dot.

Charge-stability diagram

Fig. 6.5.1 Charge-stability diagram with respect to reference gate potential (see the note in Creating the junction, above).

Note

Please refer to the Quantum transport—Master equation tutorial for an explanation of how to calculate and plot the differential conductance.

6.6. Full code

__copyright__ = "Copyright 2024, Nanoacademic Technologies Inc."

import numpy as np
import pathlib
from matplotlib import pyplot as plt
import pickle
from progress.bar import ChargingBar as Bar
from qtcad.device import constants as ct
from qtcad.device.mesh3d import Mesh, SubMesh
from qtcad.device import io
from qtcad.device import Device, SubDevice
from qtcad.device.schrodinger import Solver
from qtcad.device.many_body import SolverParams
from qtcad.transport.mastereq import seq_tunnel_curr
from qtcad.transport.junction import Junction
from qtcad.device import materials as mt

# Load the mesh
script_dir = pathlib.Path(__file__).parent.resolve()
path_mesh = str(script_dir/'meshes'/'gated_dot.msh')
scaling = 1e-6
mesh = Mesh(scaling, path_mesh)

# Create device
d = Device(mesh, conf_carriers="e")
d.set_temperature(0.1)

# # Applied potentials
Vtop_1 = -0.5
Vtop_2 = -0.4
Vtop_3 = -0.5
Vbottom_1 = -0.5
Vbottom_2 = -0.4
Vbottom_3 = -0.5

# Work function of the metallic gates at midgap of GaAs
barrier = 0.834*ct.e    # n-type Schottky barrier height

# Ionized dopant density
doping = 3e18*1e6   # In SI units

# Set up materials in heterostructure stack
d.new_region("cap", mt.GaAs)
d.new_region("barrier", mt.AlGaAs)
d.new_region("dopant_layer", mt.AlGaAs,ndoping=doping)
d.new_region("spacer_layer", mt.AlGaAs)
d.new_region("spacer_dot", mt.AlGaAs)
d.new_region("two_deg", mt.GaAs)
d.new_region("two_deg_dot", mt.GaAs)
d.new_region("substrate", mt.GaAs)
d.new_region("substrate_dot", mt.GaAs)

# Remove unphysical charge from cap and barrier
d.new_insulator("cap")
d.new_insulator("barrier")

# Align the bands across hetero-interfaces using GaAs as the reference material
d.align_bands(mt.GaAs)

# Set up boundary conditions
d.new_schottky_bnd("top_gate_1", Vtop_1, barrier)
d.new_schottky_bnd("top_gate_2", Vtop_2, barrier)
d.new_schottky_bnd("top_gate_3", Vtop_3, barrier)
d.new_schottky_bnd("bottom_gate_1", Vbottom_1, barrier)
d.new_schottky_bnd("bottom_gate_2", Vbottom_2, barrier)
d.new_schottky_bnd("bottom_gate_3", Vbottom_3, barrier)
d.new_ohmic_bnd("ohmic_bnd")

# Load the confinement potential from previous run
path_in = str(script_dir/"output"/"electric_potential_-0.400V.hdf5")
d.set_potential(io.load(path_in))

# Create a submesh in which to solve Schrodinger's equation
submesh = SubMesh(mesh, ["spacer_dot","two_deg_dot","substrate_dot"])

# Create subdevice
subdevice = SubDevice(d, submesh)

# Solver the single-electron problem to get basis set
slv = Solver(subdevice)
slv.solve()
subdevice.print_energies()

many_body_solver_params = SolverParams()
many_body_solver_params.num_states = 2  # Number of levels to keep
many_body_solver_params.n_degen = 2     # spin degenerate system
many_body_solver_params.alpha = 0.0714  # lever arm
jc = Junction(subdevice, many_body_solver_params=many_body_solver_params)

print('chemical potentials (eV):', jc.chem_potentials/ct.e)
print('positions of Coulomb peaks relative to reference value (V):',
    jc.coulomb_peak_pos)

# Set a near-zero source-drain bias
jc.setVs(0.0001)
jc.setVd(-0.0001)

jc.set_source_broad_func(10e6)
jc.set_drain_broad_func(10e6)

v_gate_rng = np.linspace(-0.1, 0.4, num=200)

# Calculate the current for each plunger gate potential
vec_Il = []
for i in range(v_gate_rng.size):        # loop over gate voltage
    v_gate = v_gate_rng[i]
    jc.setVg(v_gate)
    Il,Ir,prob = seq_tunnel_curr(jc)    # transport calculation
    vec_Il += [Il]

path_out = str(script_dir/'output'/'current.pkl')
outfile = open(path_out, 'wb')
pickle.dump((v_gate_rng,vec_Il), outfile)
outfile.close()

fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(1,1,1)
ax.set_xlabel("Gate potential (V)")
ax.set_ylabel("Current (pA)")
ax.plot(v_gate_rng, np.array(vec_Il)/1e-12)
path_fig = str(script_dir/"output"/"coulomb_peaks.png")
plt.savefig(path_fig)
plt.show()

### Charge-stability diagram ###
v_gate_rng = np.linspace(-0.2, 0.5, num=200)
v_drain_rng = np.linspace(-0.02, 0.02, num=50)

diam = np.zeros((v_gate_rng.size,v_drain_rng.size), dtype=float)

number_grid_points = v_gate_rng.size * v_drain_rng.size # number of sampled grid points
progress_bar = Bar("Computing charge-stability diagram", max = number_grid_points) # initialize progress bar

for i in range(v_gate_rng.size):        # loop over gate voltage
    v_gate = v_gate_rng[i]
    jc.setVg(v_gate)

    for j in range(v_drain_rng.size):      # loop over drain voltage
        v_drain = v_drain_rng[j]
        jc.setVd(v_drain)
        Il,Ir,prob = seq_tunnel_curr(jc)    # transport calculation
        diam[i,j] = Il
        progress_bar.next()

progress_bar.finish()

# Postprocess the current to turn it into an image to be plotted
current_image = np.flip(np.transpose(np.abs(diam)), axis=0)

# plot results
fig, axs = plt.subplots()
axs.set_ylabel('$V_d(V)$',fontsize=16)
axs.set_xlabel('$V_g(V)$',fontsize=16)
diff_cond_map = axs.imshow(current_image/1e-12, interpolation='bilinear',
        extent=[Vtop_2+v_gate_rng[0], Vtop_2+v_gate_rng[-1],
                v_drain_rng[0], v_drain_rng[-1]], aspect="auto", cmap="Reds")
fig.colorbar(diff_cond_map, ax=axs, label="Current (pA)")
fig.tight_layout()
path_fig = str(script_dir/"output"/"stability.png")
plt.savefig(path_fig)
plt.show()