Strain and external fields
In this tutorial, we introduce the common procedure of applying strain and external fields on the
model. It is difficult to design common out-of-the-box user APIs that offer such functionalities
since they are strongly case-dependent. Generally, the user should implement these perturbations by
modifying model attributes such as orbital positions, on-site energies and hopping integrals. For
the primitive cell, it is straightforward to achieve this goal with the set_orbital
and
add_hopping
methods. The Sample
class, on the contrary, does not offer such methods.
Instead, the user should work with the attributes directly. In the Sample
class, orbital
positions and on-site energies are stored in the orb_pos
and orb_eng
attributes. Hopping
terms are represented with 3 attributes: hop_i
and hop_j
for orbital indices, and hop_v
for hopping integrals. There is also an auxiliary attribute dr
which holds the hopping vectors.
All the attributes are NumPy arrays. The on-site energies and hopping terms can be modified
directly, while the orbital positions should be changed via a modifier function. The hopping
vectors are updated from the orbital positions and hopping terms automatically, thus no need of
explicit modification.
As the example, we will investigate the propagation of wave function in a graphene sample. The
script can be found at examples/advanced/strain_fields.py
. We begin with defining the
functions for adding strain and external fields, then calculate and plot the time-dependent wave
function to check their effects on the propagation. The essential packages of this tutorial can be
imported with:
import math
from typing import Callable
import numpy as np
from numpy.linalg import norm
import tbplas as tb
Functions for strain
Strain will introduce deformation into the model, changing both orbital positions and hopping integrals. It is a rule that orbital positions should not be modified directly, but through a modifier function. We consider a Gaussian bump deformation, and define the following function to generate the modifier:
1def make_deform(center: np.ndarray, sigma: float = 0.5,
2 extent: tuple = (1.0, 1.0),
3 scale: tuple = (0.5, 0.5)) -> Callable:
4 """
5 Generate deformation function as orb_pos_modifier.
6
7 :param center: Cartesian coordinate of deformation center in nm
8 :param sigma: broadening parameter
9 :param extent: extent of deformation along xOy and z directions
10 :param scale: scaling factor for deformation along xOy and z directions
11 :return: deformation function
12 """
13 def _deform(orb_pos):
14 x, y, z = orb_pos[:, 0], orb_pos[:, 1], orb_pos[:, 2]
15 dx = (x - center[0]) * extent[0]
16 dy = (y - center[1]) * extent[1]
17 amp = np.exp(-(dx**2 + dy**2) / (2 * sigma**2))
18 x += dx * amp * scale[0]
19 y += dy * amp * scale[0]
20 z += amp * scale[1]
21 return _deform
Here center
, sigma
and extent
control the location, width and extent of the bump. For
example, if extent
is set to \((1.0, 0.0)\), the bump will become one-dimensional which
varies along \(x\)-direction while remains constant along \(y\)-direction. scale
specifies the scaling factors for in-plane and out-of-plane displacements. The make_deform
function returns another function as the modifier, which updates the orbital positions in place
according to the following expression:
where \(\mathbf{r}_i\) is the position of \(i\)-th orbital, \(\Delta_i\) is the displacement, \(s\) is the scaling factor, \(\parallel\) and \(\perp\) are the in-plane and out-of-plane components. The location, width and extent of the bump are denoted as \(\mathbf{c}_0\), \(\sigma\) and \(\eta\), respectively.
In addition to the orbital position modifier, we also need to update hopping integrals
1def update_hop(sample: tb.Sample) -> None:
2 """
3 Update hopping terms in presence of deformation.
4
5 :param sample: Sample to modify
6 :return: None.
7 """
8 sample.init_hop()
9 for i, rij in enumerate(sample.dr):
10 sample.hop_v[i] = calc_hop(rij)
As we will make use of the hopping terms and vectors, we should call the init_hop
method to initialize the
attributes. Similar rule holds for the on-site energies and orbital positions. Then we loop over the hopping
terms to update the integrals in hop_v
according to the vectors in dr
with the calc_hop
function,
which is defined as:
1def calc_hop(rij: np.ndarray) -> float:
2 """
3 Calculate hopping parameter according to Slater-Koster relation.
4
5 :param rij: (3,) array, displacement vector between two orbitals in NM
6 :return: hopping parameter in eV
7 """
8 a0 = 0.1418
9 a1 = 0.3349
10 r_c = 0.6140
11 l_c = 0.0265
12 gamma0 = 2.7
13 gamma1 = 0.48
14 decay = 22.18
15 q_pi = decay * a0
16 q_sigma = decay * a1
17 dr = norm(rij).item()
18 n = rij.item(2) / dr
19 v_pp_pi = - gamma0 * math.exp(q_pi * (1 - dr / a0))
20 v_pp_sigma = gamma1 * math.exp(q_sigma * (1 - dr / a1))
21 fc = 1 / (1 + math.exp((dr - r_c) / l_c))
22 hop = (n**2 * v_pp_sigma + (1 - n**2) * v_pp_pi) * fc
23 return hop
Functions for external fields
The effects of external electric field can be modeled by adding position-dependent potential to the on-site energies. We consider a Gaussian-type scattering potential described by
and define the following function to add the potential to the sample
1def add_efield(sample: tb.Sample, center: np.ndarray, sigma: float = 0.5,
2 extent: tuple = (1.0, 1.0), v_pot: float = 1.0) -> None:
3 """
4 Add electric field to sample.
5
6 :param sample: sample to add the field
7 :param center: Cartesian coordinate of the center in nm
8 :param sigma: broadening parameter
9 :param extent: extent of electric field along xOy and z directions
10 :param v_pot: electric field intensity in eV
11 :return: None.
12 """
13 sample.init_orb_pos()
14 sample.init_orb_eng()
15 orb_pos = sample.orb_pos
16 orb_eng = sample.orb_eng
17 for i, pos in enumerate(orb_pos):
18 dx = (pos.item(0) - center[0]) * extent[0]
19 dy = (pos.item(1) - center[1]) * extent[1]
20 orb_eng[i] += v_pot * math.exp(-(dx**2 + dy**2) / (2 * sigma**2))
The arguments center
, sigma
and extent
are similar to that of the make_deform
function, while v_pot
specifies \(V_0\). Similar to update_hop
, we need to call
init_orb_pos
and init_orb_eng
to initialize orbital positions and on-site energies before
accessing them. Then the position-dependent scattering potential is added to the on-site energies.
The effects of magnetic field can be modeled with Peierls substitution. For homogeneous magnetic
field perpendicular to the \(xOy\)-plane along \(-z\) direction, the Sample
class offers an API set_magnetic_field
, which follows the Landau gauge of vector potential
\(\mathbf{A} = (By, 0, 0)\) and updates the hopping terms as
where \(B\) is the intensity of magnetic field, \(\mathbf{r}_i\) and \(\mathbf{r}_j\) are the positions of \(i\)-th and \(j\)-th orbitals, respectively.
Initial wave functions
The initial wave function we consider here as an example for the propagation is a Gaussian wave-packet, which is defined by
1def init_wfc_gaussian(sample: tb.Sample, center: np.ndarray, sigma: float = 0.5,
2 extent: tuple = (1.0, 1.0)) -> np.ndarray:
3 """
4 Generate Gaussian wave packet as initial wave function.
5
6 :param sample: sample for which the wave function shall be generated
7 :param center: Cartesian coordinate of the wave packet center in nm
8 :param sigma: broadening parameter
9 :param extent: extent of wave packet along xOy and z directions
10 :return: initial wave function
11 """
12 sample.init_orb_pos()
13 orb_pos = sample.orb_pos
14 wfc = np.zeros(orb_pos.shape[0], dtype=np.complex128)
15 for i, pos in enumerate(orb_pos):
16 dx = (pos.item(0) - center[0]) * extent[0]
17 dy = (pos.item(1) - center[1]) * extent[1]
18 wfc[i] = math.exp(-(dx**2 + dy**2) / (2 * sigma**2))
19 wfc /= np.linalg.norm(wfc)
20 return wfc
Note that the wave function should be a complex vector whose length must be equal to the number of orbitals. Also, it should be normalized before being returned.
Propagation of wave function
We consider a rectangular graphene sample with \(50\times20\times1\) primitive cells, as shown in Fig. 1(a). We begin with defining some geometric parameters:
1prim_cell = tb.make_graphene_rect()
2dim = (50, 20, 1)
3pbc = (True, True, False)
4x_max = prim_cell.lat_vec[0, 0] * dim[0]
5y_max = prim_cell.lat_vec[1, 1] * dim[1]
6wfc_center = (x_max * 0.5, y_max * 0.5)
7deform_center = (x_max * 0.75, y_max * 0.5)
Here dim
and pbc
define the dimension and boundary condition. x_max
and y_max
are
the lengths of the sample along \(x\) and \(y\) directions. The initial wave function will
be a Gaussian wave-packet located at the center of the sample given by wfc_center
.
The deformation and scattering potential will be located at the center of right half of the sample,
as specified by deform_center
and shown in Fig. 1(b)-(c).

Top and side views of (a) pristine graphene sample and (b) sample with deformation. (c) Plot of on-site energies of graphene sample with scattering potential.
We firstly investigate the propagation of a one-dimensional Gaussian wave-packet in pristine sample, which is given by
1# Prepare the sample and inital wave function
2sample = tb.Sample(tb.SuperCell(prim_cell, dim, pbc))
3psi0 = init_wfc_gaussian(sample, center=wfc_center, extent=(1.0, 0.0))
4
5# Propagate the wave function
6config = tb.Config()
7config.generic["nr_time_steps"] = 128
8time_log = np.array([0, 16, 32, 64, 128])
9sample.rescale_ham()
10solver = tb.Solver(sample, config)
11psi_t = solver.calc_psi_t(psi0, time_log)
12
13# Visualize the time-dependent wave function
14vis = tb.Visualizer()
15for i in range(len(time_log)):
16 vis.plot_wfc(sample, np.abs(psi_t[i])**2, cmap="hot", scatter=False)
As the propagation is performed with the calc_psi_t
function of Solver
class, it follows
the common procedure of TBPM calculation. We propagate the wave function by 128 steps, and save the
snapshots in psi_t
at the time steps specified in time_log
. The snapshots are then
visualized by the plot_wfc
function of Visualizer
class, as shown in Fig. 2(a)-(e), where
the wave-packet diffuses freely, hits the boundary and forms interference pattern.
We then add the bump deformation to the sample, by assigning the modifier function to the supercell
and calling update_hop
to update the hopping terms
deform = make_deform(center=deform_center)
sample = tb.Sample(tb.SuperCell(prim_cell, dim, pbc, orb_pos_modifier=deform))
update_hop(sample)
The propagation of wave-packet in deformed graphene sample is shown in Fig. 2(f)-(j). Obviously, the wave function gets scattered by the bump. Although similar interference pattern is formed, the propagation in the right part of the sample is significantly hindered, due to the increased inter-atomic distances and reduced hopping integrals at the bump.
Similar phenomena are observed when the scattering potential is added to the sample by
add_efield(sample, center=deform_center)
The time-dependent wave function is shown in Fig. 2(k)-(o). Due to the higher on-site energies, the probability of emergence of electron is suppressed near the scattering center.
As for the effects of magnetic field, it is well known that Landau levels will emerge in the DOS. The analytical solution to Schrodinger’s equation for free electron in homogeneous magnetic field with \(\mathbf{A}=(By, 0, 0)\) shows that the wave function will propagate freely along \(x\) and \(z\)-directions while oscillates along \(y\)-direction. To simulate this process, we apply the magnetic field to the sample by
sample.set_magnetic_field(50)
The snapshots of time-dependent wave function are shown in Fig. 2(p)-(t). The interference pattern is similar to the case without magnetic field, as the wave function propagates freely along \(x\) direction. However, due to the oscillation along \(y\)-direction, the interference pattern gets distorted during the propagation. These phenomena agree well with the analytical results.

(a)-(e) Propagation of one-dimensional Gaussian wave-packet in pristine graphene sample. (f)-(j) Propagation in graphene sample with deformation, (k)-(o) with scattering potential and (p)-(t) with magnetic field of 50 Tesla.