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.
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.

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.