Parameters fitting

In this tutorial, we show how to fit the on-site energies and hopping terms by reducing the 8-band graphene primitive cell imported from the output of Wannier90 in Interfaces. We achieve this by truncating the hopping terms to the second nearest neighbor, and refitting the on-site energies and Slater-Koster parameters to minimize the residual between the reference and fitted band data, i.e.

\[\min_{\mathbf{x}} \sum_{i,\mathbf{k}}\omega_i\left|\bar{E}_{i,\mathbf{k}} - E_{i,\mathbf{k}}(\mathbf{x})\right|^2\]

where \(\mathbf{x}\) are the fitting parameters, \(\omega\) are the fitting weights, \(\bar{E}\) and \(E\) are the reference and fitted band data from the original and reduced cells, \(i\) and \(\mathbf{k}`\) are band and \(\mathbf{k}\)-point indices, respectively. The corresponding script can be found at examples/interface/wannier90/graphene/fit.py. We begin with importing the necessary packages:

import numpy as np
import matplotlib.pyplot as plt

import tbplas as tb

Auxialiary functions and classes

We define the following functions to build the primitive cell from the Slater-Koster paramaters. Most of them are similar to the functions in Slater-Koster formulation:

 1def calc_hop(sk: tb.SK, rij: np.ndarray, distance: float,
 2             label_i: str, label_j: str, sk_params: np.ndarray) -> complex:
 3    """
 4    Calculate hopping integral based on Slater-Koster formulation.
 5
 6    :param sk: SK instance
 7    :param rij: displacement vector from orbital i to j in nm
 8    :param distance: norm of rij
 9    :param label_i: label of orbital i
10    :param label_j: label of orbital j
11    :param sk_params: array containing SK parameters
12    :return: hopping integral in eV
13    """
14    # 1st and 2nd hopping distances in nm
15    d1 = 0.1419170044439990
16    d2 = 0.2458074906840380
17    if abs(distance - d1) < 1.0e-5:
18        v_sss, v_sps, v_pps, v_ppp = sk_params[2:6]
19    elif abs(distance - d2) < 1.0e-5:
20        v_sss, v_sps, v_pps, v_ppp = sk_params[6:10]
21    else:
22        raise ValueError(f"Too large distance {distance}")
23    return sk.eval(r=rij, label_i=label_i, label_j=label_j,
24                   v_sss=v_sss, v_sps=v_sps,
25                   v_pps=v_pps, v_ppp=v_ppp)
26
27
28def make_cell(sk_params: np.ndarray) -> tb.PrimitiveCell:
29    """
30    Make primitive cell from Slater-Koster parameters.
31
32    :param sk_params: (10,) float64 array containing Slater-Koster parameters
33    :return: created primitive cell
34    """
35    # Lattice constants and orbital info.
36    lat_vec = np.array([
37        [2.458075766398899, 0.000000000000000, 0.000000000000000],
38        [-1.229037883199450, 2.128755065595607, 0.000000000000000],
39        [0.000000000000000, 0.000000000000000, 15.000014072326660],
40    ])
41    orb_pos = np.array([
42        [0.000000000, 0.000000000, 0.000000000],
43        [0.666666667, 0.333333333, 0.000000000],
44    ])
45    orb_label = ("s", "px", "py", "pz")
46
47    # Create the cell and add orbitals
48    e_s, e_p = sk_params[0], sk_params[1]
49    cell = tb.PrimitiveCell(lat_vec, unit=tb.ANG)
50    for pos in orb_pos:
51        for label in orb_label:
52            if label == "s":
53                cell.add_orbital(pos, energy=e_s, label=label)
54            else:
55                cell.add_orbital(pos, energy=e_p, label=label)
56
57    # Add Hopping terms
58    neighbors = tb.find_neighbors(cell, a_max=5, b_max=5,
59                                  max_distance=0.25)
60    sk = tb.SK()
61    for term in neighbors:
62        i, j = term.pair
63        label_i = cell.get_orbital(i).label
64        label_j = cell.get_orbital(j).label
65        hop = calc_hop(sk, term.rij, term.distance, label_i, label_j,
66                       sk_params)
67        cell.add_hopping(term.rn, i, j, hop)
68    return cell

The fitting tool ParamFit is an abstract class. The users should derive their own fitting class from it, and implement the calc_bands_ref and calc_bands_fit methods, which return the reference and fitted band data, respectively. We define a MyFit class as

 1class MyFit(tb.ParamFit):
 2    def calc_bands_ref(self) -> np.ndarray:
 3        """
 4        Get reference band data for fitting.
 5
 6        :return: band structure on self._k_points
 7        """
 8        cell = tb.wan2pc("graphene")
 9        k_len, bands = cell.calc_bands(self._k_points)
10        return bands
11
12    def calc_bands_fit(self, sk_params: np.ndarray) -> np.ndarray:
13        """
14        Get band data of the model from given parameters.
15
16        :param sk_params: array containing SK parameters
17        :return: band structure on self._k_points
18        """
19        cell = make_cell(sk_params)
20        k_len, bands = cell.calc_bands(self._k_points, echo_details=False)
21        return bands

In calc_bands_ref, we import the primitive cell with the Wannier90 interface wan2pc(), then calculate and return the band data. The calc_bands_fit function does a similar job, with the only difference that the primitive cell is constructed from Slater-Koster parameters with the make_cell function we have just created.

Fitting the paramaters

The application of MyFit class is as following:

 1def main():
 2    # Fit the sk parameters
 3    # Reference:
 4    # https://journals.aps.org/prb/abstract/10.1103/PhysRevB.82.245412
 5    k_points = tb.gen_kmesh((120, 120, 1))
 6    weights = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
 7    fit = MyFit(k_points, weights)
 8    sk0 = np.array([-8.370, 0.0,
 9                    -5.729, 5.618, 6.050, -3.070,
10                    0.102, -0.171, -0.377, 0.070])
11    sk1 = fit.fit(sk0)
12    print("SK parameters after fitting:")
13    print(sk1[:2])
14    print(sk1[2:6])
15    print(sk1[6:10])
16
17    # Plot fitted band structure
18    k_points = np.array([
19        [0.0, 0.0, 0.0],
20        [1./3, 1./3, 0.0],
21        [1./2, 0.0, 0.0],
22        [0.0, 0.0, 0.0],
23    ])
24    k_path, k_idx = tb.gen_kpath(k_points, [40, 40, 40])
25    cell_ref = tb.wan2pc("graphene")
26    cell_fit = make_cell(sk1)
27    k_len, bands_ref = cell_ref.calc_bands(k_path)
28    k_len, bands_fit = cell_fit.calc_bands(k_path)
29    num_bands = bands_ref.shape[1]
30    for i in range(num_bands):
31        plt.plot(k_len, bands_ref[:, i], color="red", linewidth=1.0)
32        plt.plot(k_len, bands_fit[:, i], color="blue", linewidth=1.0)
33    plt.show()
34
35
36if __name__ == "__main__":
37    main()

To create a MyFit instance, we need to specify the \(\mathbf{k}\)-points and fitting weights, as shown in line 5-6. For the \(\mathbf{k}\)-points, we are going to use a \(\mathbf{k}\)-grid of \(120\times120\times1\). The length of weights should be equal to the number of orbitals of the primitive cell, which is 8 in our case. We assume all the bands to have the same weights, and set them to 1. Then we create the MyFit instance, define the initial guess of parameters from the reference, and get the fitted results with the fit function. The output should look like

SK parameters after fitting:
[-3.63102899 -1.08477167]
[-5.27742318  5.87219052  4.61650991 -2.75652966]
[-0.24734558  0.17599166  0.14798703  0.16545428]

The first two numbers are the on-site energies for \(s\) and \(p\) orbitals, while the following numbers are the Slater-Koster paramets \(V_{ss\sigma}\), \(V_{sp\sigma}\), \(V_{pp\sigma}\) and \(V_{pp\pi}`\) at first and second nearest hopping distances, respectively. We can also plot and compare the band structures from the reference and fitted primitive cells, as shown the left panel of the figure. It is clear that the fitted band structure agrees well with the reference data near the Fermi level (-1.7 eV) and at deep (-20 eV) or high energies (10 eV). However, the derivation from reference data of intermediate bands (-5 eV and 5 eV) is non-negligible. To improve this, we lower the weights of band 1-2 and 7-8 by

weights = np.array([0.1, 0.1, 1.0, 1.0, 1.0, 1.0, 0.1, 0.1])

and refitting the parameters. The results are shown in the right panel of the figure, where the fitted and reference band structures agree well from -5 to 5 eV.

../../_images/allin11.png

Band structures from reference (solid red lines) and fitted (dashed blue lines) primitive cells with (a) equal weights for all bands and (b) lower weights for bands 1-2 and 7-8. The horizontal dashed black lines indicate the Fermi level.