# 2. Simulating bound states in a quantum dot using the Poisson and Schrödinger solvers

## 2.1. Requirements

### 2.1.1. Software components

QTCAD

### 2.1.2. Mesh file

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

### 2.1.3. Python script

`qtcad/examples/practical_application/2-poisson_schrodinger.py`

### 2.1.4. References

## 2.2. Briefing

In the previous tutorial, we have used devicegen to generate
a `.msh`

file describing the geometry and mesh of a gated
heterostructure stack. This is a structure used in practice to
confine charge carriers to a finite volume of space, namely a quantum dot.
In this tutorial, we will use QTCAD to calculate this quantum dot’s bound
states eigenenergies and eigenstates. We will do so by

obtaining the dot’s confinement potential by solving the (classical) nonlinear Poisson equation throughout the entire device;

solving the Schrödinger equation within the dot.

## 2.3. Setting up the device

### 2.3.1. Header

We start by importing the relevant modules.

```
from qtcad.device import constants as ct
from qtcad.device.mesh3d import Mesh, SubMesh
from qtcad.device import io
from qtcad.device import materials as mt
from qtcad.device import Device, SubDevice
from qtcad.device.poisson import Solver as PoissonSolver
from qtcad.device.poisson import SolverParams as PoissonSolverParams
from qtcad.device.schrodinger import Solver as SchrodingerSolver
from qtcad.device import analysis as an
import pathlib
```

These modules were seen in some tutorials, notably Poisson and Schrödinger simulation of a nanowire quantum dot and
Schrödinger equation for a quantum dot. Importantly, both the *Mesh* and *SubMesh* classes
are imported; the former will model and store information about the mesh
of the entire gated heterostructure stack, while the latter will only do
so for a subset of the full mesh (here the quantum dot).

### 2.3.2. Loading the mesh, defining material composition, and applying external voltages

First, the heterostructure device is created from the mesh.

```
# Input file
script_dir = pathlib.Path(__file__).parent.resolve()
path_mesh = str(script_dir/'meshes'/'gated_dot.msh')
# Load the mesh
scaling = 1e-6
mesh = Mesh(scaling,path_mesh)
mesh.show()
# Create device
d = Device(mesh, conf_carriers="e")
d.set_temperature(0.1)
```

Note that the temperature was set to be equal to \(0.1\textrm{ K}\)
using the `set_temperature`

method, instead of the default temperature
(\(300\;\textrm K\)).

In addition, remark that `scaling`

is set to \(10^{-6}\), because length
units in QTCAD are SI base units (meters), while they are micrometers in
devicegen (in accordance with most layout editors).

Finally, the `conf_carriers`

attribute of the `Device`

object `d`

was set
to `"e"`

, meaning that the Schrödinger equation solver, which will be used
later in this tutorial, will compute the electron envelope function (but not
the hole envelope functions).

Next, the material composition of the heterostructure is specified.

```
# 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)
```

For example, the physical volume labeled by `dopant_layer`

in the `.msh`

file
is defined to be composed of n-doped AlGaAs with a dopant concentration
of \(3\times 10^{18}\textrm{ cm}^{-3}\). By default, the ionization model
is complete ionization—all dopants will thus be assumed to be ionized.

By default, in QTCAD, band alignments across heterojunctions are computed using Anderson’s rule. While this rule is simple and widely applicable, it often fails to accurately predict band alignments. Indeed, Anderson’s rule ignores interface physics which may play a significant role in band alignment, such as interface dipoles and Fermi level pinning. The QTCAD team has modeled such physics in density-functional-theory simulations to more accurately predict band alignments across certain common heterojunctions, notably GaAs/AlGaAs heterojunctions (see the Band alignment in heterostructures theory section and the Band alignment in heterostructures tutorial). To harness these results that go beyond Anderson’s rule, you may simply execute the following.

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

Here, GaAs is taken to be the reference material—i.e., *ab initio* band
alignments with respect to GaAs are used. Such *ab initio* band alignments
are currently only available for GaAs/AlGaAs heterostructures.

Next, we forbid any charge, either classical or quantum, from forming in the
`"cap"`

and `"barrier"`

regions. This is done using the `new_insulator`

method of the device `d`

.

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

If these two lines were omitted from the code, a secondary 2DEG
would form across the cap–barrier interface, which would greatly affect the
potential profile and charge density within the primary 2DEG, across the
spacer–substrate interface. In typical experimental devices, only the primary
2DEG would be connected to thermal reservoirs (electrodes) able to supply
electrons and/or holes to the device. Thus, to reach the secondary 2DEG,
electrons would need to tunnel across around \(30~\textrm{nm}\) of AlGaAs,
namely across the spacer, doped, and barrier layers of the heterostructure. The
corresponding tunneling probability would be extremely low, leading to an
extremely low electron density across the secondary 2DEG, thereby justifying
our use of the `new_insulator`

method.

Finally, we set the values of external voltages.

```
# Applied potentials
Vtop_1 = -0.5
Vtop_2 = -0.5
Vtop_3 = -0.5
Vbottom_1 = -0.5
Vbottom_2 = -0.5
Vbottom_3 = -0.5
# Work function of the metallic gates at midgap of GaAs
barrier = 0.834*ct.e # n-type Schottky barrier height
# 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")
```

For example, a *Schottky* boundary condition is set on the physical
surface labelled as `top_gate_1`

. To do so, the `new_schottky_bnd`

method is called, which takes as arguments the surface label, the
applied voltage (`V_top_1`

), and the Schottky barrier height (`barrier`

)
across the semiconductor–gate (GaAs–metal) junction. Mathematically, this
corresponds to a Dirichlet boundary condition with a boundary value
determined by `V_top_1`

and `barrier`

, but for which the actual potential
value also accounts for Schottky physics. Note that here, we use the n-type
Schottky barrier height for aluminium on GaAs. Similarly, an *Ohmic*
boundary condition is set on the whole bottom surface of the device
using the `new_ohmic_bnd`

method. It also corresponds to a Dirichlet
boundary condition; the boundary value is determined by the material
composition and doping of the nodes on the boundary. See the
Boundary Conditions theory section for more information about available
conditions and their corresponding equations.

## 2.4. Solving the nonlinear Poisson equation in the heterostructure

Now that the material composition of the device and the external voltages have been specified, we solve the nonlinear Poisson equation. We are especially interested in the potential in and around the quantum dot region, since this confining potential will be used to compute the bound states of the quantum dot.

First, we instantiate the solver parameters and the corresponding non-linear Poisson solver, as was done in Poisson and Schrödinger simulation of a nanowire quantum dot.

```
# Create the non-linear Poisson solver and set its parameters
solver_params = PoissonSolverParams({"tol": 1e-3, "maxiter": 100})
poisson_solver = PoissonSolver(d, solver_params=solver_params)
```

The default solver parameter values for the Poisson solver are used, with
the exceptions that the convergence threshold is set to `tol = 1e-3`

and
that the maximum number of iterations is set to `maxiter = 100`

.

Next, we define the submesh corresponding to the quantum dot region

```
# Create a submesh including only the dot region
submesh = SubMesh(mesh, ["spacer_dot","two_deg_dot","substrate_dot"])
submesh.show()
```

In this case, the quantum dot is the union of the regions labeled by
`spacer_dot`

, `two_deg_dot`

, and `substrate_dot`

. It is important to
include not only the physical region within the 2DEG part of the structure,
but also in the regions above and below. This is because a potential well
must be clearly defined along the \(z`\) direction for quantum confinement
to be correctly modelled. In particular, if the `spacer_dot`

region is not
included, the potential barrier at the GaAs/AlGaAs interface will be missing
in the dot region, and the output solutions of the Schrödinger solver will be
wrong.

Next, the
`set_dot_region`

method is called on the device object `d`

. This
sets the classical charge distribution to be zero in the quantum dot
region; indeed, charge in this region will later be treated quantum
mechanically by solving the Schrödinger equation.

```
# Set up the dot region in which no classical charge is allowed
d.set_dot_region(submesh)
```

Next, the nonlinear Poisson equation is solved in `d`

, i.e. the
self-consistent iteration described in Non-linear Poisson solver is executed
until convergence.

```
# Solve
poisson_solver.solve()
```

Finally, the results are saved in `.vtu`

files for visualization.

```
# Save the conduction band edge in .vtu format
outfile_cond_band_edge = str(script_dir / "output" / "cond_band_edge.vtu")
out_dict = {"EC (eV)": d.cond_band_edge()/ct.e}
io.save(outfile_cond_band_edge, out_dict, mesh)
# Save the electron density in .vtu format
outfile_electron_dens = str(script_dir / "output" / "electron_dens.vtu")
out_dict = {"n (1/m^3)": d.n}
io.save(outfile_electron_dens, out_dict, mesh)
```

For example, the figure below shows a side view of the electron density.

## 2.5. Solving Schrödinger’s equation in the quantum dot

It remains to compute the eigenenergies and eigenstates of the bound states
in the quantum dot. To do so, first, the potential energy profile is computed
and stored in the device `d`

.

```
# Get the potential energy from the band edge for usage in the Schrodinger
# solver
d.set_V_from_phi()
```

Next, a subdevice for the quantum dot is created; a Schrödinger equation solver is defined for this subdevice.

```
# Create a subdevice for the dot region
subdevice = SubDevice(d,submesh)
# Create a Schrodinger solver
schrod_solver = SchrodingerSolver(subdevice)
```

The Schrödinger equation can then be solved.

```
# Solve Schrodinger's equation
schrod_solver.solve()
```

Next, the eigenenergies are printed.

```
# Print energies
subdevice.print_energies()
```

This results in the following.

```
Energy levels (eV)
[0.00248833 0.01306186 0.01696493 0.02377321 0.02762847 0.03198176
0.035086 0.03851718 0.04282455 0.04608607]
```

In addition, we may compute and print the geometric properties of the
quantum dot using the `analyze_dot`

function of the `analysis`

module:

```
# Get and print the quantum-dot geometric properties
dot_props = an.analyze_dot(subdevice, verbose=True)
```

These properties are stored in the dictionary `dot_props`

. If the
`verbose`

keyword argument is set to `True`

, the function will also
print the properties on the screen. For the current example, this yields:

```
--------------------------------------------------------------------------------
Geometric properties of the quantum dot:
--------------------------------------------------------------------------------
position : [ 2.34054497e-07 3.29354128e-07 -3.85266553e-08]
std : [7.99033271e-09 9.61433352e-09 1.79929068e-09]
size : [3.19613308e-08 3.84573341e-08 7.19716274e-09]
--------------------------------------------------------------------------------
```

Here, the length units are in meters. Definitions of dot position and size are
given in the API reference of the
`analyze_dot`

function.

Finally, the ground state, 1st excited
state, and 2nd excited states are saved in a `.vtu`

file.

```
# Save the first few eigenstates
outfile_eigenfunctions = str(script_dir / "output" / "eigenfunctions.vtu")
out_dict = {"Ground": subdevice.eigenfunctions[:,0],
"1st excited": subdevice.eigenfunctions[:,1],
"2nd excited": subdevice.eigenfunctions[:,2]}
io.save(outfile_eigenfunctions, out_dict, submesh)
```

For example, the figure below shows the second excited state on a plane normal to the \(y\) axis going through the center of the quantum dot.

## 2.6. Full code

```
__copyright__ = "Copyright 2024, Nanoacademic Technologies Inc."
from qtcad.device import constants as ct
from qtcad.device.mesh3d import Mesh, SubMesh
from qtcad.device import io
from qtcad.device import materials as mt
from qtcad.device import Device, SubDevice
from qtcad.device.poisson import Solver as PoissonSolver
from qtcad.device.poisson import SolverParams as PoissonSolverParams
from qtcad.device.schrodinger import Solver as SchrodingerSolver
from qtcad.device import analysis as an
import pathlib
# Input file
script_dir = pathlib.Path(__file__).parent.resolve()
path_mesh = str(script_dir/'meshes'/'gated_dot.msh')
# Load the mesh
scaling = 1e-6
mesh = Mesh(scaling,path_mesh)
mesh.show()
# Create device
d = Device(mesh, conf_carriers="e")
d.set_temperature(0.1)
# 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)
# Align the bands across hetero-interfaces using GaAs as the reference material
d.align_bands(mt.GaAs)
# Remove unphysical charge from cap and barrier
d.new_insulator("cap")
d.new_insulator("barrier")
# Applied potentials
Vtop_1 = -0.5
Vtop_2 = -0.5
Vtop_3 = -0.5
Vbottom_1 = -0.5
Vbottom_2 = -0.5
Vbottom_3 = -0.5
# Work function of the metallic gates at midgap of GaAs
barrier = 0.834*ct.e # n-type Schottky barrier height
# 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")
# Create the non-linear Poisson solver and set its parameters
solver_params = PoissonSolverParams({"tol": 1e-3, "maxiter": 100})
poisson_solver = PoissonSolver(d, solver_params=solver_params)
# Create a submesh including only the dot region
submesh = SubMesh(mesh, ["spacer_dot","two_deg_dot","substrate_dot"])
submesh.show()
# Set up the dot region in which no classical charge is allowed
d.set_dot_region(submesh)
# Solve
poisson_solver.solve()
# Save the conduction band edge in .vtu format
outfile_cond_band_edge = str(script_dir / "output" / "cond_band_edge.vtu")
out_dict = {"EC (eV)": d.cond_band_edge()/ct.e}
io.save(outfile_cond_band_edge, out_dict, mesh)
# Save the electron density in .vtu format
outfile_electron_dens = str(script_dir / "output" / "electron_dens.vtu")
out_dict = {"n (1/m^3)": d.n}
io.save(outfile_electron_dens, out_dict, mesh)
# Get the potential energy from the band edge for usage in the Schrodinger
# solver
d.set_V_from_phi()
# Create a subdevice for the dot region
subdevice = SubDevice(d,submesh)
# Create a Schrodinger solver
schrod_solver = SchrodingerSolver(subdevice)
# Solve Schrodinger's equation
schrod_solver.solve()
# Print energies
subdevice.print_energies()
# Get and print the quantum-dot geometric properties
dot_props = an.analyze_dot(subdevice, verbose=True)
# Save the first few eigenstates
outfile_eigenfunctions = str(script_dir / "output" / "eigenfunctions.vtu")
out_dict = {"Ground": subdevice.eigenfunctions[:,0],
"1st excited": subdevice.eigenfunctions[:,1],
"2nd excited": subdevice.eigenfunctions[:,2]}
io.save(outfile_eigenfunctions, out_dict, submesh)
```