summaryrefslogtreecommitdiff
path: root/eigen.py
blob: 1dbb2a9bbb43b7237394393cdd655db89a450fbc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
from math import *

import numpy as np
import numpy.fft

import visualize


# Normalize wavefunction with respect to volume element dV
def normalize(state, dV):
    norm2 = np.vdot(state, state) * dV
    return state / sqrt(abs(norm2))


# Gram-Schmidt orthonormalization with respect to dV
def orthonormalize(basis, dV):
    for n in range(len(basis)):
        psi = basis[n]

        for i in range(n):
            psi -= basis[i] * np.vdot(basis[i], psi) * dV

        basis[n] = normalize(psi, dV)

    return basis


# Zero all components with kinetic energy less than Ecut
def enforce_cutoff(s, psis_q, kq2):
    for i in range(s.Nres):
        for j in range(s.Nres):
            for k in range(s.Nres):
                if kq2[i, j, k] > 2 * s.Ecut:
                    for psi_q in psis_q:
                        psi_q[i, j, k] = 0
    return orthonormalize(psis_q, s.dq)


# Apply the Kohn-Sham Hamiltonian to a normalized state psi
def apply_hamiltonian(s, psi_q, Veff_r, kq2):
    # Apply kinetic energy in q-domain
    Tpsi_q = 0.5 * kq2 * psi_q

    # Apply local+nonlocal potential in r-domain
    psi_r = np.fft.ifftn(psi_q)
    # We don't normalize: the iFFT ensures a correctly normalized result
    Vpsi_r = Veff_r * psi_r # local part
    for a in s.atoms: # nonlocal part
        Vnon_r = a.apply_nonlocal_potential(psi_r) * s.dr
        Vpsi_r += Vnon_r
    Vpsi_q = np.fft.fftn(Vpsi_r)

    return Tpsi_q + Vpsi_q


# Resisual vector R, which the eigensolver tries to minimize
def get_residual(s, psi_q, Hpsi_q, kq2):
    ene = np.vdot(psi_q, Hpsi_q) * s.dq # assuming normalization
    R_q = Hpsi_q - ene * psi_q

    # Calculate preconditioned residual PR for faster convergence:
    # The ideal PR satisfies PR = (H - ene)^-1 R, such that (H - ene) PR = R.
    # This operator (H - ene) is hard to invert, so we approximate it using
    # the kinetic energy only: -0.5 \nabla^2 PR = R (the Poisson equation).
    PR_q = 2 * R_q / (kq2 + 1)
    # This is an even rougher approximation, but it works well enough.

    return PR_q


# One iteration of the block Davidson eigensolver algorithm
def diagonalize_subspace(s, psis, V, kq2):
    # Apply Hamiltonian to psis and cache results
    Hbasis = []
    for n in range(len(psis)):
        Hbasis.append(apply_hamiltonian(s, psis[n], V, kq2))

    # Calculate corresponding residuals
    resids = []
    for n in range(len(psis)):
        R = get_residual(s, psis[n], Hbasis[n], kq2)
        resids.append(R)

    # Augment the input basis (psis) with the residuals
    basis = psis + resids
    basis = enforce_cutoff(s, basis, kq2)

    Ndim  = len(basis)

    # Apply Hamiltonian to orthonormalized residuals and cache results
    for n in range(len(psis), Ndim):
        Hbasis.append(apply_hamiltonian(s, basis[n], V, kq2))

    # Subspace Hamiltonian matrix with elements <basis[i] | H | basis[j]>
    Hsub = np.zeros((Ndim, Ndim), dtype="complex128")
    # Exploit Hermiticity: only calculate the diagonal and upper triangle,
    # then copy the elements from the upper to the lower triangle.
    for r in range(Ndim):
        for c in range(r, Ndim):
            Hsub[r, c] = np.vdot(basis[r], Hbasis[c]) * s.dq
    for r in range(Ndim):
        for c in range(0, r):
            Hsub[r, c] = np.conj(Hsub[c, r])

    # Find the eigenvectors and eigenvalues of Hsub
    evals, evecs = np.linalg.eigh(Hsub)

    # Express the eigenvectors in the original basis (input psis)
    result = []
    for n in range(Ndim // 2):
        psi = np.zeros((s.Nres, s.Nres, s.Nres), dtype="complex128")
        for b in range(Ndim):
            psi += basis[b] * evecs[b, n]
        result.append(psi)

    return evals, result


# Small optimization: cache |k+q|^2 ahead of time
def cache_kinetic_factor(s, kpoint):
    kq2 = np.zeros((s.Nres, s.Nres, s.Nres))
    for i in range(s.Nres):
        for j in range(s.Nres):
            for k in range(s.Nres):
                q = np.array((s.qx[i, j, k], s.qy[i, j, k], s.qz[i, j, k]))
                kq = kpoint + q
                kq2[i, j, k] = np.dot(kq, kq)
    return kq2


# Initial guess from atoms' individual (approximate) orbitals
def guess_orbitals(s):
    psis_q = []
    for a in s.atoms:
        psis_r = a.guess_orbitals(s.Norb)

        # Convert guesses to Fourier space
        for psi_r in psis_r:
            psi_q = np.fft.fftn(psi_r)
            psis_q.append(psi_q)

    return orthonormalize(psis_q, s.dq)


# Get lowest eigenvalues and eigenstates of Kohn-Sham equation
def solve_kohnsham_equation(s, V, k, prefix="", thresh=1e-4):
    kq2 = cache_kinetic_factor(s, k)

    # Initialize psis, which we will refine shortly
    psis_q = guess_orbitals(s)

    print(prefix + "Eigensolver cycle 1 running...")
    cycles = 0
    improv = 1
    energy_old = 0
    # Iterative eigensolver loop
    while improv > thresh:
        cycles += 1

        # Eliminate high-frequency noise (little effect in practice)
        psis_q = enforce_cutoff(s, psis_q, kq2)

        # Refine eigenstates using one step of the block Davidson method
        energies, psis_q = diagonalize_subspace(s, psis_q, V, kq2)

        # To measure progress, we look at the change of (N+1)th eigenvalue,
        # where N is the number of bands we are interested in converging.
        energy_new = energies[s.Neig]
        improv = abs(energy_new - energy_old) / abs(energy_new)
        energy_old = energy_new

        # Clear line and (re)print progress information
        print("\033[A\033[2K" + prefix, end="")
        print("Eigensolver cycle {} done".format(cycles), end="")
        if cycles > 1:
            print(", improvement {:.2e}".format(improv), end="")
        print("")

    # Convert psis to real space and normalize them there
    psis_r = []
    for n in range(len(psis_q)):
        psi_r = np.fft.ifftn(psis_q[n])
        psis_r.append(normalize(psi_r, s.dr))

    # energies is a NumPy array right now
    return energies.tolist(), psis_r, psis_q