Posts
The band structure of graphene
|0> to |1>: Why quantum technology is bound to rise.
Finite differences and second quantization
Dynamically adjustable HoppingAmplitudes using callbacks in TBTK
Retrieving the Hamiltonian from the Model in TBTK
Creating Models with complex geometries using an IndexFilter in TBTK
Direct access to wave function amplitudes and eigenvalues in TBTK
Using TBTK to calculate the density of states (DOS) of a 1D, 2D, and 3D square lattice
TBTK v1.0.0 released
Using TBTK to time C++ code
Introduction to TBTK – A software development kit for quantum mechanical calculations
The second quantum revolution

The band structure of graphene

Kristofer Björnson
(5th of July 2019)

Most recent TBTK release at the time of writing: v1.1.1. (Updated to work with: v2.0.0)

In condensed matter physics, the electronic band structure is one of the most commonly used tools for understanding the electronic properties of a material. Here we take a look at how to set up a tight-binding model of graphene and calculate the band structure along paths between certain high symmetry points in the Brillouin zone. We also calculate the density of states (DOS). A basic understanding of band theory is assumed and if further background is required a good starting point is to look at the nearly free electron model, Bloch waves, Bloch’s theorem, and the Brillouin zone.

1 Physical description

1.1 Lattice

Graphene has a hexagonal lattice that can be described using the lattice vectors 𝐫0=a(1,0,0), 𝐫1=a(1/2,3/2,0), and 𝐫2=a(0,0,1). We set the lattice constant to a = 2.5 Å, which only is meant to reflect the correct order of magnitude. Strictly speaking, only two two-dimensional vectors are required to describe the lattice. However, we have here made the vectors three-dimensional and included a third lattice vector 𝐫2 that is perpendicular to the lattice to simplify the calculation of the reciprocal lattice vectors. The resulting lattice is shown in the image below.

[Uncaptioned image]

Indicated in the image is also the unit cell and the A and B sublattice sites within the unit cell. We further define

𝐫AB(0)= 𝐫0+2𝐫13, (1)
𝐫AB(1)= 𝐫1+𝐫AB(0), (2)
𝐫AB(2)= 𝐫0𝐫1+𝐫AB(0), (3)

which can be verified to be the vector from A to its three nearest neighbor B sites.

1.2 Brillouin zone

The reciprocal lattice vectors can now be calculated using

𝐤0= 2π𝐫1×𝐫2𝐫0𝐫1×𝐫2, (4)
𝐤1= 2π𝐫2×𝐫0𝐫1𝐫2×𝐫0, (5)
𝐤2= 2π𝐫0×𝐫1𝐫2𝐫0×𝐫1. (6)

Here 𝐤2 just like 𝐫2 is related to the z-direction and will be dropped from here on. The two remaining reciprocal lattice vectors 𝐤0 and 𝐤1 defines the reciprocal lattice of interest to us. Below the reciprocal lattice is shown.

[Uncaptioned image]

In the the image above we have also indicated the first Brillouin zone in red, and outlined the path ΓMKΓ along which the band structure will be calculated. The high symmetry points can be verified to be given by

𝚪= (0,0,0), (7)
𝐌= (πa,π3a,0), (8)
𝐊= (4π3a,0,0). (9)

1.3 Hamiltonian

We will use a simple tight-binding model of graphene of the form

H=t𝐢δc𝐢,Ac𝐢+𝜹,B+H.c. (10)

Here t is a hopping amplitude connecting nearest neighbor sites which we take to be t = 3 eV (again, only correct up to order of magnitude). The index 𝐢 runs over the unit cells, while 𝜹 takes on the three values that makes 𝐢+𝜹,B the nearest neighbor of 𝐢,A.

Because of translation invariance, it is possible to transform the expression above to a simple expression in reciprocal space. To do so we write

c𝐢,A= 1N𝐤c𝐤,Aeik𝐑𝐢, (11)
c𝐢+𝜹,B = 1N𝐤c𝐤,Bei𝐤(𝐑𝐢+𝐫𝜹). (12)

Here 𝐑𝐢 is the position of the A atom in unit cell 𝐢, while 𝐫𝜹{𝐫AB(0),𝐫AB(1),𝐫AB(2)}. 111 Note that the definition of c𝐢+𝜹,B is threefold redundant since for each site B in unit cell 𝐢 there are three ways to label the same site. For each of the three vectors 𝐫AB(0), 𝐫AB(1), and 𝐫AB(2) there is a different unit cell from which the B site in cell 𝐢 can be reached. However, each of these three redundant definitions can be verified to be equivalent and the redundant expression simplifies the mathematical notation.

Inserting this into the expression above we get

H= tNi𝐤𝐤𝜹c𝐤,Ac𝐤,Bei𝐤𝐑𝐢ei𝐤(𝐑𝐢+𝐫𝜹)+H.c (13)
= t𝐤𝜹c𝐤,Ac𝐤,Bei𝐤𝐫𝜹+H.c (14)
= t𝐤c𝐤,Ac𝐤,B(ei𝐤𝐫AB(0)+ei𝐤𝐫AB(1)+ei𝐤𝐫AB(2))+H.c. (15)

In the second line we have summed over 𝐢 and used the delta function property of 𝐢ei(𝐤𝐤)𝐫𝐢/N to eliminat 𝐤, while in the last line we have made the sum over 𝜹 explicit.

2 Implementation

2.1 Parameters

We are now ready to implement the calculation using TBTK. To do so, we begin by specifying the parameters to be used in the calculation

1//Set the natural units for this calculation.
2UnitHandler::setScales({
3        ”1 rad”, ”1 C”, ”1 pcs”, ”1 eV”,
4        ”1 Ao”, ”1 K”, ”1 s”
5});
6
7//Define parameters.
8double t = 3;   //eV
9double a = 2.5; //Ångström
10unsigned int BRILLOUIN_ZONE_RESOLUTION = 1000;
11vector<unsigned int> numMeshPoints = {
12    BRILLOUIN_ZONE_RESOLUTION,
13    BRILLOUIN_ZONE_RESOLUTION
14};
15const int K_POINTS_PER_PATH = 100;
16const double ENERGY_LOWER_BOUND = -10;
17const double ENERGY_UPPER_BOUND = 10;
18const int ENERGY_RESOLUTION = 1000;

In lines 2-5, we specify what units that dimensionful parameters should be understood in terms of. For our purposes here, this means that energies and distances are measured in electron Volts (eV) and Ångström (Ao), respectively.

We next specify the value for t and a as given in the physical description above. The value ‘BRILLOUIN_ZONE_RESOLUTION’ determines the number of subintervals each of the reciprocal lattice vectors is divided into when generating a mesh for the Brillouin zone. ‘numMeshPoints’ combine this information into a vector. Similarly, ‘K_POINTS_PER_PATH’ determines the number of k-points to use along each of the paths ΓM, MK, and KΓ when calculating the band structure. Finally, the energy window that is used to calculate the DOS is specified in the three last lines.

2.2 Real and reciprocal lattice

The lattice vectors 𝐫0, 𝐫1, and 𝐫2 are now defined as follows.

1Vector3d r[3];
2r[0] = Vector3d({a,     0,              0});
3r[1] = Vector3d({-a/2,  a*sqrt(3)/2,    0});
4r[2] = Vector3d({0,     0,              a});

The nearest neighbor vectors 𝐫AB(0), 𝐫AB(1), and 𝐫AB(2) are similarly defined using

1Vector3d r_AB[3];
2r_AB[0] = (r[0] + 2*r[1])/3.;
3r_AB[1] = -r[1] + r_AB[0];
4r_AB[2] = -r[0] - r[1] + r_AB[0];

Next, the reciprocal lattice vectors are calculated using the expressions at the beginning of the Brillouin zone section.

1Vector3d k[3];
2for(unsigned int n = 0; n < 3; n++){
3    k[n] = 2*M_PI*r[(n+1)%3]*r[(n+2)%3]/(
4        Vector3d::dotProduct(
5            r[n],
6            r[(n+1)%3]*r[(n+2)%3]
7        )
8    );
9}

Note that here ‘*’ indicates the cross product, while the scalar product between u and v is calculated using Vector3d::dotProduct(u, v).

Having defined the lattice vectors, we can now construct a Brillouin zone.

1BrillouinZone brillouinZone(
2    {
3        {k[0].x, k[0].y},
4        {k[1].x, k[1].y}
5    },
6    SpacePartition::MeshType::Nodal
7);

Here the first argument is a list of lattice vectors, while the second argument specifies the type of mesh that is going to be associated with this Brillouin zone. SpacePartition::MeshType::Nodal means that when the Brillouin zone is divided into small parallelograms, the mesh points will lie on the corners of these parallelograms. If SpacePartition::MeshType::Interior is used instead, the mesh points will lie in the middle of the parallelograms.

Having constructed a Brillouin zone, we finally generate a mesh by passing the number of segments to divide each reciprocal lattice vector to the following function.

1vector<vector<double>> mesh
2    = brillouinZone.getMinorMesh(
3        numMeshPoints
4    );

2.3 Set up the model

The model is set up using the following code.

1Model model;
2for(unsigned int n = 0; n < mesh.size(); n++){
3    //Get Index representation of the current
4    //k-point.
5    Index kIndex = brillouinZone.getMinorCellIndex(
6        mesh[n],
7        numMeshPoints
8    );
9
10    //Calculate the matrix element.
11    Vector3d k({mesh[n][0], mesh[n][1], 0});
12    complex<double> h_01 = -t*(
13        exp(
14            -i*Vector3d::dotProduct(
15                k,
16                r_AB[0]
17            )
18        )
19        + exp(
20            -i*Vector3d::dotProduct(
21                k,
22                r_AB[1]
23            )
24        )
25        + exp(
26            -i*Vector3d::dotProduct(
27                k,
28                r_AB[2]
29            )
30        )
31    );
32
33    //Add the matrix element to the model.
34    model << HoppingAmplitude(
35        h_01,
36        {kIndex[0], kIndex[1], 0},
37        {kIndex[0], kIndex[1], 1}
38    ) + HC;
39}
40model.construct();

Here the loop runs over each lattice point in the mesh. To understand line 4-7, we first note that the mesh contains k-points of the form (kx,ky) with real numbers kx and ky. However, to specify a model, we need discrete indices with integer-valued subindices to label the points. Think of this as each k-value being of the form 𝐤𝐢, where 𝐤𝐢 is a real-valued vector while 𝐢 is an integer-valued vector that indexes the different k-points. The BrillouinZone solves this problem for us by automatically providing a mapping from continuous indices to discrete indices. Given a real index in the first Brillouin zone and information about the number of subdivisions of the lattice vectors, brillouinZone.getMinorCellIndex() finds the closest discrete point and returns its Index representation. This index can then be used to refer to the k-point whenever an Index object is required.

In line 10, the mesh point is converted to a Vector3d object that is then used in lines 11-30 to calculate the matrix element we derived in the section about the Hamiltonian. The matrix element, together with its Hermitian conjugate, is then fed to the model in lines 33-37. Here we construct the full index for the model by combining the integer kIndex[0] and kIndex[1] values, together with the sublattice index, into an index structure of the form kx, ky, sublattice. A zero in the sublattice index refers to an A site, while one refers to a B site. Finally, the model is constructed in line 39.

2.4 Selecting solver

We are now ready to solve the model and will use diagonalization to do so. In this case, we have a problem that is block-diagonal in the k-index. For this reason, we will use the solver Solver::BlockDiagonalizer that can take advantage of this block structure. To set up and run the diagonalization we write

1Solver::BlockDiagonalizer solver;
2solver.setModel(model);
3solver.run();

We then set up the corresponding property extractor and configure the energy window using

1PropertyExtractor::BlockDiagonalizer
2    propertyExtractor(solver);
3propertyExtractor.setEnergyWindow(
4    ENERGY_LOWER_BOUND,
5    ENERGY_UPPER_BOUND,
6    ENERGY_RESOLUTION
7);

2.5 Extracting the DOS

Extracting, smoothing, and plotting the DOS is now done as follows.

1//Calculate the density of states (DOS).
2Property::DOS dos
3    = propertyExtractor.calculateDOS();
4
5//Smooth the DOS.
6const double SMOOTHING_SIGMA = 0.03;
7const unsigned int SMOOTHING_WINDOW = 51;
8dos = Smooth::gaussian(
9    dos,
10    SMOOTHING_SIGMA,
11    SMOOTHING_WINDOW
12);
13
14//Plot the DOS.
15Plotter plotter;
16plotter.plot(dos);
17plotter.save(”figures/DOS.png”);

2.6 Extract the band structure

We are now ready to calculate the band structure. To do so, we begin by defining the high symmetry points

1Vector3d Gamma({0,        0,                 0});
2Vector3d M({M_PI/a,       -M_PI/(sqrt(3)*a), 0});
3Vector3d K({4*M_PI/(3*a), 0,                 0});

These are then packed in pairs to define the three paths Γ→M, M →K, and K →Γ

1vector<vector<Vector3d>> paths = {
2    {Gamma, M},
3    {M, K},
4    {K, Gamma}
5};

We now loop over each of the three paths and calculate the band structure at each k-point along these paths.

1Array<double> bandStructure(
2    {2, 3*K_POINTS_PER_PATH},
3    0
4);
5Range interpolator(0, 1, K_POINTS_PER_PATH);
6for(unsigned int p = 0; p < 3; p++){
7    //Select the start and endpoints for the
8    //current path.
9    Vector3d startPoint = paths[p][0];
10    Vector3d endPoint = paths[p][1];
11
12    //Loop over a single path.
13    for(
14        unsigned int n = 0;
15        n < K_POINTS_PER_PATH;
16        n++
17    ){
18        //Interpolate between the paths start
19        //and end point.
20        Vector3d k = (
21            interpolator[n]*endPoint
22            + (1 - interpolator[n])*startPoint
23        );
24
25        //Get the Index representation of the
26        //current k-point.
27        Index kIndex = brillouinZone.getMinorCellIndex(
28            {k.x, k.y},
29            numMeshPoints
30        );
31
32        //Extract the eigenvalues for the
33        //current k-point.
34        bandStructure[
35            {0, n+p*K_POINTS_PER_PATH}
36        ] = propertyExtractor.getEigenValue(
37            kIndex,
38            0
39        );
40        bandStructure[
41            {1, n+p*K_POINTS_PER_PATH}
42        ] = propertyExtractor.getEigenValue(
43            kIndex,
44            1
45        );
46    }
47}

Here we first set up the array ‘bandStructure’ that is to contain the band structure with two eigenvalues per k-point. The variable ‘interpolator’ is then defined, which is an array of values from 0 to 1 that will be used to interpolate between the paths start and endpoints. In lines 9 and 10 we then select the start and endpoints for the current path, and the loop over this path begins in line 13.

In lines 20-23, we calculate an interpolated k-point value between the first and last points in the given path. Similarly, as when setting up the model, we then request the Index that corresponds to the given k-point. Having obtained this Index, we extract the lowest eigenvalue for the corresponding block in lines 34-39 and store it in the array ‘bandStructure’. Similarly, the highest eigenvalue is obtained for the same block in lines 40-45.

Next, we calculate the maximum and minimum value of the band structure along the calculated paths. We do this to allow for drawing vertical lines of the correct height at the M and K points when plotting the data.

1double min = bandStructure[{0, 0}];
2double max = bandStructure[{1, 0}];
3for(
4    unsigned int n = 0;
5    n < 3*K_POINTS_PER_PATH; n++ ){ if(min > bandStructure[{0, n}])
6        min = bandStructure[{0, n}];
7    if(max < bandStructure[{1, n}])
8        max = bandStructure[{1, n}];
9}

Finally, we plot the band structure.

1plotter.clear();
2plotter.setLabelX(”k”);
3plotter.setLabelY(”Energy”);
4plotter.plot(
5    bandStructure.getSlice({0, _a_}),
6    {{”color”, ”black”}}
7);
8plotter.plot(
9    bandStructure.getSlice({1, _a_}),
10    {{”color”, ”black”}}
11);
12plotter.plot(
13    {K_POINTS_PER_PATH, K_POINTS_PER_PATH},
14    {min, max},
15    {{”color”, ”black”}}
16);
17plotter.plot(
18    {2*K_POINTS_PER_PATH, 2*K_POINTS_PER_PATH},
19    {min, max}
20    {{”color”, ”black”}}
21);
22plotter.save(”figures/BandStructure.png );

Here the ‘_a_’ flag in the call to bandStructure.getSlice(0, _a_, “color”, “black”) on line 5 (and 6) is a wildcard. It indicates that we want to get a new array that contains all the elements of the original array that has a zero in the first index and an arbitrary index in the second. ‘a’ stands for ‘all’.

3 Results

Below the results are shown. The DOS is characteristically v-shaped around E=0 and is peaked 1/3 of the way to the band edge. In the band structure, we can see the Dirac cone at the K-point (the second vertical line). The low density of states at E=0 can be understood as a consequence of the small number of k-points around the K-point for which the energy is close to zero. The cone has a finite slope, and therefore only a single k-point is at E=0 (one more point is at the K’-point, which we have not considered here but which is at another corner of the Brillouin zone). Compare this with the flat region at the M-point (first vertical line), which results in a significant amount of k-points with roughly the same energy. This flat region is responsible for the strong peaks in the DOS.

3.1 DOS

[Uncaptioned image]

3.2 Band structure

[Uncaptioned image]

4 Full code

The full code is available in src/main.cpp in the project 2019_07_05 of the Second Tech code package. See the README for instructions on how to build and run.