Build hetero-structure

In this tutorial, we show how to build hetero-structure taking twisted bilayer graphene as the example. The workflow of constructing hetero structures is shown in Fig. 1(a). First of all, we determine the twisting angle and lattice vectors of the hetero-structure. Then we build the primitive cells of each layer, shift the twisted layer along \(z\)-axis by the interlayer distance and rotate it by the twisting angle. After that, we reshape the primitive cells to the lattice vectors of the hetero-structure to yield the layers, as depicted in Fig. 1(b). When all the layers are ready, we merge them into one cell and add the intralayer and interlayer hopping terms up to a given cutoff distance. For the visualization of Moiré pattern, we also need to build a sample from the merged cell. The script can be found at examples/advanced/tbg.py. Before constructing the model, we need to import the required packages:

import math

import numpy as np
from numpy.linalg import norm

import tbplas as tb
../../_images/hetero.png

(a) Workflow of constructing hetero-structure. (b) Schematic plot of lattice vectors of fixed (\(\mathbf{a}_1\), \(\mathbf{a}_2\)) and twisted (\(\mathbf{a}_1^\prime\), \(\mathbf{a}_2^\prime\)) primitive cells and the hetero-structure (\(\mathbf{A}_1\), \(\mathbf{A}_2\)), as well as the twisting angle \(\theta\). (c) Twisted bilayer graphene sample with \(4\times4\times1\) merged cells of \(i=5\).

Auxiliary functions

The twisting angle and lattice vectors are determined following the formulation in ref1:

\[\begin{split}\theta_i &= \arccos \frac{3i^2+3i+1/2}{3i^2+3i+1} \\ \mathbf{A}_1 &= i\cdot\mathbf{a}_1 + (i+1)\cdot\mathbf{a}_2 \\ \mathbf{A}_2 &= -(i+1)\cdot\mathbf{a}_1 + (2i+1)\cdot\mathbf{a}_2\end{split}\]

where \(\mathbf{a}_1\) and \(\mathbf{a}_2\) are the lattice vectors of the primitive cell of fixed layer and \(i\) is the index of hetero-structure. We define the following functions accordingly:

 1def calc_twist_angle(i: int) -> float:
 2    """
 3    Calculate twisting angle according to the reference.
 4
 5    :param i: parameter controlling the twisting angle
 6    :return: twisting angle in RADIANs, NOT degrees
 7    """
 8    cos_ang = (3 * i ** 2 + 3 * i + 0.5) / (3 * i ** 2 + 3 * i + 1)
 9    return math.acos(cos_ang)
10
11
12def calc_hetero_lattice(i: int, prim_cell_fixed: tb.PrimitiveCell) -> np.ndarray:
13    """
14    Calculate Cartesian coordinates of lattice vectors of hetero-structure
15    according to the reference.
16
17    :param i: parameter controlling the twisting angle
18    :param prim_cell_fixed: primitive cell of fixed layer
19    :return: (3, 3) float64 array, Cartesian coordinates of hetero-structure
20        lattice vectors in NANOMETER
21    """
22    hetero_lattice = np.array([[i, i + 1, 0],
23                               [-(i + 1), 2 * i + 1, 0],
24                               [0, 0, 1]])
25    hetero_lattice = tb.frac2cart(prim_cell_fixed.lat_vec, hetero_lattice)
26    return hetero_lattice

calc_twist_angle returns the twisting angle in radians, while calc_hetero_lattice returns the Cartesian coordinates of lattce vectors in nm. After merging the layers, we need to add the interlayer hopping terms. Meanwhile, the intralayer hoppings terms should also be extended in the same approach. We define the extend_hop function to achieve these goals:

 1def extend_hop(prim_cell: tb.PrimitiveCell, max_distance: float = 0.75) -> None:
 2    """
 3    Extend the hopping terms in primitive cell up to cutoff distance.
 4
 5    :param prim_cell: primitive cell to extend
 6    :param max_distance: cutoff distance in NM
 7    :return: None. Incoming primitive cell is modified
 8    """
 9    neighbors = tb.find_neighbors(prim_cell, a_max=1, b_max=1,
10                                  max_distance=max_distance)
11    for term in neighbors:
12        i, j = term.pair
13        prim_cell.add_hopping(term.rn, i, j, calc_hop(term.rij))

Here we call the find_neighbors function to get the neighboring orbital pairs up to the cutoff distance max_distance. Then the hopping terms are evaluated according to the displacement vector rij with the calc_hop function and added to the primitive cell. The calc_hop function is defined according to the formulation in ref2:

 1def calc_hop(rij: np.ndarray) -> float:
 2    """
 3    Calculate hopping parameter according to Slater-Koster relation.
 4    See ref. [2] for the formulae.
 5
 6    :param rij: (3,) array, displacement vector between two orbitals in NM
 7    :return: hopping parameter in eV
 8    """
 9    a0 = 0.1418
10    a1 = 0.3349
11    r_c = 0.6140
12    l_c = 0.0265
13    gamma0 = 2.7
14    gamma1 = 0.48
15    decay = 22.18
16    q_pi = decay * a0
17    q_sigma = decay * a1
18    dr = norm(rij).item()
19    n = rij.item(2) / dr
20    v_pp_pi = - gamma0 * math.exp(q_pi * (1 - dr / a0))
21    v_pp_sigma = gamma1 * math.exp(q_sigma * (1 - dr / a1))
22    fc = 1 / (1 + math.exp((dr - r_c) / l_c))
23    hop = (n**2 * v_pp_sigma + (1 - n**2) * v_pp_pi) * fc
24    return hop

Build the model

With all the functions ready, we proceed to build the hetero-structure. Firstly, we evaluate the twisting angle of bilayer graphene for \(i=5\). Then we construct the primitive cells of the fixed and twisted layers with the make_graphene_diamond() function. The fixed primitive cell is located at \(z=0\) and does not need rotation or shifting. On the other hand, the twisted primitive cell needs to be rotated counter-clockwise by the twisting angle and shifted towards \(+z\) by 0.3349 nm, which is done with the spiral_prim_cell() function. After that, we reshape the primitive cells to the lattice vectors of hetero-structure with the make_hetero_layer() function, which is a wrapper to coordinate conversion and reshape_prim_cell(). Then the layers are merged with merge_prim_cell() and the hopping terms are extended with extend_hop using a cutoff distance of 0.75 nm. Finally, a sample with \(4\times4\times1`\) merged cells is created and plotted, with the hopping terms below 0.3 eV hidden for clarity. The output is shown in Fig. 1(c), where the Moiré pattern can be clearly observed.

 1def main():
 2    # Evaluate twisting angle
 3    i = 5
 4    angle = calc_twist_angle(i)
 5
 6    # Prepare primitive cells of fixed and twisted layer
 7    prim_cell_fixed = tb.make_graphene_diamond()
 8    prim_cell_twisted = tb.make_graphene_diamond()
 9
10    # Shift and rotate the twisted layer
11    tb.spiral_prim_cell(prim_cell_twisted, angle=angle, shift=0.3349)
12
13    # Reshape primitive cells to the lattice vectors of hetero-structure
14    hetero_lattice = calc_hetero_lattice(i, prim_cell_fixed)
15    layer_fixed = tb.make_hetero_layer(prim_cell_fixed, hetero_lattice)
16    layer_twisted = tb.make_hetero_layer(prim_cell_twisted, hetero_lattice)
17
18    # Merge layers
19    merged_cell = tb.merge_prim_cell(layer_fixed, layer_twisted)
20
21    # Extend hopping terms
22    extend_hop(merged_cell, max_distance=0.75)
23
24    # Visualize Moire pattern
25    sample = tb.Sample(tb.SuperCell(merged_cell, dim=(4, 4, 1), pbc=(True, True, False)))
26    sample.plot(with_orbitals=False, hop_as_arrows=False, hop_eng_cutoff=0.3)
27
28
29if __name__ == "__main__":
30    main()