The DFT solver, as written
pwdft.viv
The same physics with H built as a matrix and diagonalized by eig_herm at every k-point: simple, O(N^3), and the step-for-step twin of the numpy reference.
6 functions
-
setup_slot— -
vnl_element— -
build_vnl— -
build_vtot— -
build_h— -
add_density—
# pwdft: the DENSE engine. H(k) is built and diagonalized at every k-point. # # dft/run.sh si (compile deck + engine + checks to C, and run) # # Right for a few hundred plane waves -- silicon, aluminum, diamond -- and the # simplest correct thing, which is why it exists: it is the check on the # iterative engine (dft/pwdft_it.viv), which is what copper needs. # Everything solver-independent is in dft/pw_common.viv. # Slots 0 .. NKPT-1 hold the SCF k-points; slot NKPT is scratch for the path. NSLOT = NKPT + 1.0 SCRATCH = NKPT NPW = zeros(NSLOT) MILL = zeros(NSLOT, NPWMAX, NDIM) #: MILL integer triple m of each plane wave KG = zeros(NSLOT, NPWMAX, NDIM + 1) #: KG k+G in cartesian, and its norm in [.., NDIM] PJ = zeros(NSLOT, NPWMAX, NCH) #: PJ every radial projector at every plane wave PH = czeros(NSLOT, NPWMAX, NAT) #: PH structure phases exp(-i G . tau_a) VNL = czeros(NSLOT, NPWMAX, NPWMAX) def setup_slot(slot, k1, k2, k3) { n = 0.0 big = 0.0 for i1 to NM { for i2 to NM { for i3 to NM { m1 = i1 - MM m2 = i2 - MM m3 = i3 - MM f1 = m1 + k1 f2 = m2 + k2 f3 = m3 + k3 gx = f1 * B[0, 0] + f2 * B[1, 0] + f3 * B[2, 0] gy = f1 * B[0, 1] + f2 * B[1, 1] + f3 * B[2, 1] gz = f1 * B[0, 2] + f2 * B[1, 2] + f3 * B[2, 2] g2 = gx * gx + gy * gy + gz * gz if 0.5 * g2 <= ECUT { step keep { MILL[slot, n, 0] = m1 MILL[slot, n, 1] = m2 MILL[slot, n, 2] = m3 KG[slot, n, 0] = gx KG[slot, n, 1] = gy KG[slot, n, 2] = gz KG[slot, n, 3] = sqrt(g2) if abs(m1) > big { big = abs(m1) } if abs(m2) > big { big = abs(m2) } if abs(m3) > big { big = abs(m3) } } step projectors { for c to NCH { PJ[slot, n, c] = gth_proj(CHL[c], CHI[c], RPRJ[CHS[c], CHL[c]], sqrt(g2)) } } step phases { # the phase uses G alone, not k + G: it is the structure factor px = m1 * B[0, 0] + m2 * B[1, 0] + m3 * B[2, 0] py = m1 * B[0, 1] + m2 * B[1, 1] + m3 * B[2, 1] pz = m1 * B[0, 2] + m2 * B[1, 2] + m3 * B[2, 2] for a to NAT { ph = px * TAU[a, 0] + py * TAU[a, 1] + pz * TAU[a, 2] PH[slot, n, a] = complex(cos(ph), 0.0 - sin(ph)) } } n = n + 1.0 } } } } NPW[slot] = n return big } def vnl_element(slot, i, j) { ki = KG[slot, i, 3] kj = KG[slot, j, 3] c = 1.0 if ki > 1.0e-12 and kj > 1.0e-12 { c = (KG[slot, i, 0] * KG[slot, j, 0] + KG[slot, i, 1] * KG[slot, j, 1] + KG[slot, i, 2] * KG[slot, j, 2]) / (ki * kj) } acc = complex(0.0, 0.0) for s to NSPEC { sf = complex(0.0, 0.0) for a to NAT { if ATSPEC[a] == s { sf = sf + PH[slot, i, a] * conj(PH[slot, j, a]) } } rad = 0.0 for l to GTH_NL { np = NPRJ[s, l] if np > 0.0 { c0 = CH0[s, l] q = 0.0 for a to np { for b to np { q = q + PJ[slot, i, c0 + a] * HPRJ[s, l, a, b] * PJ[slot, j, c0 + b] } } rad = rad + (2.0 * l + 1.0) / (4.0 * PI) * legendre(l, c) * q } } acc = acc + sf * rad } return acc } def build_vnl(slot) { n = NPW[slot] for i to n { for j to i + 1.0 { acc = vnl_element(slot, i, j) VNL[slot, i, j] = acc VNL[slot, j, i] = conj(acc) } } } narrate "building plane-wave bases and the nonlocal pseudopotential" step bases "build every SCF k-point's plane-wave basis and its nonlocal pseudopotential, once" { for k to NKPT { setup_slot(k, KFRAC[k, 0], KFRAC[k, 1], KFRAC[k, 2]) build_vnl(k) } } # ============================================================ 7. the SCF loop RHO = zeros(NG, NG, NG) + NELEC / OMEGA #: RHO the density, starting uniform VTOT = czeros(NG, NG, NG) #: VTOT the total local potential in G H = czeros(NPWMAX, NPWMAX) EV = zeros(NPWMAX) CV = czeros(NPWMAX, NPWMAX) BOX = czeros(NG, NG, NG) EIG = zeros(NKPT, NBANDS) #: EIG eigenvalues at the SCF k-points FOCC = zeros(NKPT, NBANDS) #: FOCC occupations, 0..2 CVK = czeros(NKPT, NPWMAX, NBANDS) #: CVK the eigenvectors, kept per k #: rg rho(G) * NGRID, straight out of fft3 #: vxg v_xc(G) * NGRID def build_vtot(rg, vxg) { eh = 0.0 for i1 to NG { for i2 to NG { for i3 to NG { g2 = G2[i1, i2, i3] v = VPS[i1, i2, i3] + vxg[i1, i2, i3] / NGRID if g2 > 1.0e-12 { r = rg[i1, i2, i3] / NGRID v = v + 4.0 * PI * r / g2 eh = eh + 4.0 * PI * abs(r) * abs(r) / g2 } VTOT[i1, i2, i3] = v } } } return 0.5 * OMEGA * eh } def build_h(slot) { n = NPW[slot] for i to NPWMAX { for j to NPWMAX { if i < n and j < n { d1 = wrap(MILL[slot, i, 0] - MILL[slot, j, 0]) d2 = wrap(MILL[slot, i, 1] - MILL[slot, j, 1]) d3 = wrap(MILL[slot, i, 2] - MILL[slot, j, 2]) H[i, j] = VTOT[d1, d2, d3] + VNL[slot, i, j] } else { H[i, j] = 0.0 } } if i < n { H[i, i] = H[i, i] + 0.5 * KG[slot, i, 3] * KG[slot, i, 3] } else { H[i, i] = 1000.0 + i } } } def add_density(slot, acc) { n = NPW[slot] out = acc for b to NBANDS { if FOCC[slot, b] > 1.0e-14 { fill(BOX, 0.0) for i to n { BOX[wrap(MILL[slot, i, 0]), wrap(MILL[slot, i, 1]), wrap(MILL[slot, i, 2])] = CVK[slot, i, b] } ifft3(BOX) mag = abs(BOX) out = out + (KWT[slot] * FOCC[slot, b] * NGRID * NGRID / OMEGA) * mag * mag } } return out } narrate "self-consistent field" ETOT = 0.0 EOLD = 0.0 EFERMI = 0.0 DRHO = 1.0 ITER = 0.0 DONE = false NHIST = 0.0 while not DONE and ITER < MAXIT { step xc "the LDA exchange-correlation energy density and potential at every grid point" { exc, vxc = lda_xc(RHO) } step potential "the local potential V_ps + V_H + V_xc in G space: density to G by FFT, Poisson for Hartree" { rg = complex(RHO, ZERO3) fft3(rg) vxg = complex(vxc, ZERO3) fft3(vxg) EH = build_vtot(rg, vxg) } step diagonalize "build H(k) at every k-point and diagonalize it" { # every k-point first: the Fermi level needs ALL the eigenvalues before # any band can be given an occupation for k to NKPT { build_h(k) eig_herm(H, EV, CV) EIG[k, :] = EV[0:NBANDS] CVK[k, 0:NPW[k], :] = CV[0:NPW[k], 0:NBANDS] } } step occupy "fill the lowest bands (or Fermi-Dirac occupations for a metal)" { if SMEAR > 0.0 { EFERMI = fermi_level() } ENT = fill_occupations(EFERMI) } step density "the new density from the occupied bands, and the band energy" { rhonew = ZERO3 * 0.0 EBAND = 0.0 for k to NKPT { for b to NBANDS { EBAND = EBAND + KWT[k] * FOCC[k, b] * EIG[k, b] } rhonew = add_density(k, rhonew) } } step energy "the total energy, and how far the new density is from the old one" { ETOT = EBAND - EH - sum(vxc * RHO) * DV + sum(exc * RHO) * DV - SMEAR * ENT diff = rhonew - RHO DRHO = sqrt(sum(diff * diff) * DV) show ETOT show DRHO DONE = DRHO < SCF_TOL and abs(ETOT - EOLD) < SCF_TOL EOLD = ETOT } step mix "if not converged, mix the old and new densities into the next input (Anderson)" { if not DONE { nxt = anderson_step(RHO, diff, NHIST) AND_R3 = AND_R2 AND_R2 = AND_R1 AND_R1 = RHO AND_F3 = AND_F2 AND_F2 = AND_F1 AND_F1 = diff if NHIST < 3.0 { NHIST = NHIST + 1.0 } RHO = nxt } } ITER = ITER + 1.0 } show ITER show DRHO # ======================================================= 8. the band structure narrate "band structure" BANDS = zeros(NPTS, NBANDS) step bands "the bands along the path: H(k) at each path point, with the converged potential" { for p to NPTS { setup_slot(SCRATCH, PATHK[p, 0], PATHK[p, 1], PATHK[p, 2]) build_vnl(SCRATCH) build_h(SCRATCH) eig_herm(H, EV, CV) BANDS[p, :] = EV[0:NBANDS] } } # Band energies in eV from the natural zero: the Fermi level of a metal, the # valence-band maximum (over the path) of an insulator. #: EREF that zero, in hartree step zero "band energies in eV, measured from the valence-band maximum (or the Fermi level)" { NVAL = NELEC / 2.0 EREF = EFERMI if SMEAR <= 0.0 { EREF = max(BANDS[:, NVAL - 1.0]) } EVB = (BANDS - EREF) * HA_EV } show NKPT show NPWMAX show NG # SPDX-License-Identifier: MIT OR Apache-2.0 # SPDX-FileCopyrightText: 2026 The Viv authors
This is the output of viv doc pwdft.viv, unchanged. The compiler lifts every
#| (what), #|| (how) and #||| (why) line out of the code and
attaches it to the line it describes — so the line numbers here count code only.
Its glossary holds 3 variables; ask for one with viv whatis.
#[ … #]).
1
# pwdft: the DENSE engine. H(k) is built and diagonalized at every k-point. 2
# 3
# dft/run.sh si (compile deck + engine + checks to C, and run) 4
# 5
# Right for a few hundred plane waves -- silicon, aluminum, diamond -- and the 6
# simplest correct thing, which is why it exists: it is the check on the 7
# iterative engine (dft/pwdft_it.viv), which is what copper needs. 8
# Everything solver-independent is in dft/pw_common.viv. 9
10
# Slots 0 .. NKPT-1 hold the SCF k-points; slot NKPT is scratch for the path. 11
NSLOT = NKPT + 1.0 12
SCRATCH = NKPT 13
NPW = zeros(NSLOT) 14
MILL = zeros(NSLOT, NPWMAX, NDIM) #: MILL integer triple m of each plane wave 15
KG = zeros(NSLOT, NPWMAX, NDIM + 1) #: KG k+G in cartesian, and its norm in [.., NDIM] 16
PJ = zeros(NSLOT, NPWMAX, NCH) #: PJ every radial projector at every plane wave 17
PH = czeros(NSLOT, NPWMAX, NAT) #: PH structure phases exp(-i G . tau_a) 18
VNL = czeros(NSLOT, NPWMAX, NPWMAX) 19
20
def setup_slot(slot, k1, k2, k3) { 21
n = 0.0 22
big = 0.0 23
for i1 to NM { 24
for i2 to NM { 25
for i3 to NM { 26
m1 = i1 - MM 27
m2 = i2 - MM 28
m3 = i3 - MM 29
f1 = m1 + k1 30
f2 = m2 + k2 31
f3 = m3 + k3 32
gx = f1 * B[0, 0] + f2 * B[1, 0] + f3 * B[2, 0] 33
gy = f1 * B[0, 1] + f2 * B[1, 1] + f3 * B[2, 1] 34
gz = f1 * B[0, 2] + f2 * B[1, 2] + f3 * B[2, 2] 35
g2 = gx * gx + gy * gy + gz * gz 36
if 0.5 * g2 <= ECUT { 37
step keep { 38
MILL[slot, n, 0] = m1 39
MILL[slot, n, 1] = m2 40
MILL[slot, n, 2] = m3 41
KG[slot, n, 0] = gx 42
KG[slot, n, 1] = gy 43
KG[slot, n, 2] = gz 44
KG[slot, n, 3] = sqrt(g2) 45
if abs(m1) > big { big = abs(m1) } 46
if abs(m2) > big { big = abs(m2) } 47
if abs(m3) > big { big = abs(m3) } 48
} 49
step projectors { 50
for c to NCH { 51
PJ[slot, n, c] = gth_proj(CHL[c], CHI[c], RPRJ[CHS[c], CHL[c]], sqrt(g2)) 52
} 53
} 54
step phases { 55
# the phase uses G alone, not k + G: it is the structure factor 56
px = m1 * B[0, 0] + m2 * B[1, 0] + m3 * B[2, 0] 57
py = m1 * B[0, 1] + m2 * B[1, 1] + m3 * B[2, 1] 58
pz = m1 * B[0, 2] + m2 * B[1, 2] + m3 * B[2, 2] 59
for a to NAT { 60
ph = px * TAU[a, 0] + py * TAU[a, 1] + pz * TAU[a, 2] 61
PH[slot, n, a] = complex(cos(ph), 0.0 - sin(ph)) 62
} 63
} 64
n = n + 1.0 65
} 66
} 67
} 68
} 69
NPW[slot] = n 70
return big 71
} 72
73
def vnl_element(slot, i, j) { 74
ki = KG[slot, i, 3] 75
kj = KG[slot, j, 3] 76
c = 1.0 77
if ki > 1.0e-12 and kj > 1.0e-12 { 78
c = (KG[slot, i, 0] * KG[slot, j, 0] + KG[slot, i, 1] * KG[slot, j, 1] + KG[slot, i, 2] * KG[slot, j, 2]) / (ki * kj) 79
} 80
acc = complex(0.0, 0.0) 81
for s to NSPEC { 82
sf = complex(0.0, 0.0) 83
for a to NAT { 84
if ATSPEC[a] == s { 85
sf = sf + PH[slot, i, a] * conj(PH[slot, j, a]) 86
} 87
} 88
rad = 0.0 89
for l to GTH_NL { 90
np = NPRJ[s, l] 91
if np > 0.0 { 92
c0 = CH0[s, l] 93
q = 0.0 94
for a to np { 95
for b to np { 96
q = q + PJ[slot, i, c0 + a] * HPRJ[s, l, a, b] * PJ[slot, j, c0 + b] 97
} 98
} 99
rad = rad + (2.0 * l + 1.0) / (4.0 * PI) * legendre(l, c) * q 100
} 101
} 102
acc = acc + sf * rad 103
} 104
return acc 105
} 106
107
def build_vnl(slot) { 108
n = NPW[slot] 109
for i to n { 110
for j to i + 1.0 { 111
acc = vnl_element(slot, i, j) 112
VNL[slot, i, j] = acc 113
VNL[slot, j, i] = conj(acc) 114
} 115
} 116
} 117
118
narrate "building plane-wave bases and the nonlocal pseudopotential" 119
step bases "build every SCF k-point's plane-wave basis and its nonlocal pseudopotential, once" { 120
for k to NKPT { 121
setup_slot(k, KFRAC[k, 0], KFRAC[k, 1], KFRAC[k, 2]) 122
build_vnl(k) 123
} 124
} 125
126
# ============================================================ 7. the SCF loop 127
128
RHO = zeros(NG, NG, NG) + NELEC / OMEGA #: RHO the density, starting uniform 129
VTOT = czeros(NG, NG, NG) #: VTOT the total local potential in G 130
H = czeros(NPWMAX, NPWMAX) 131
EV = zeros(NPWMAX) 132
CV = czeros(NPWMAX, NPWMAX) 133
BOX = czeros(NG, NG, NG) 134
EIG = zeros(NKPT, NBANDS) #: EIG eigenvalues at the SCF k-points 135
FOCC = zeros(NKPT, NBANDS) #: FOCC occupations, 0..2 136
CVK = czeros(NKPT, NPWMAX, NBANDS) #: CVK the eigenvectors, kept per k 137
138
def build_vtot(rg, vxg) { 139
eh = 0.0 140
for i1 to NG { 141
for i2 to NG { 142
for i3 to NG { 143
g2 = G2[i1, i2, i3] 144
v = VPS[i1, i2, i3] + vxg[i1, i2, i3] / NGRID 145
if g2 > 1.0e-12 { 146
r = rg[i1, i2, i3] / NGRID 147
v = v + 4.0 * PI * r / g2 148
eh = eh + 4.0 * PI * abs(r) * abs(r) / g2 149
} 150
VTOT[i1, i2, i3] = v 151
} 152
} 153
} 154
return 0.5 * OMEGA * eh 155
} 156
157
def build_h(slot) { 158
n = NPW[slot] 159
for i to NPWMAX { 160
for j to NPWMAX { 161
if i < n and j < n { 162
d1 = wrap(MILL[slot, i, 0] - MILL[slot, j, 0]) 163
d2 = wrap(MILL[slot, i, 1] - MILL[slot, j, 1]) 164
d3 = wrap(MILL[slot, i, 2] - MILL[slot, j, 2]) 165
H[i, j] = VTOT[d1, d2, d3] + VNL[slot, i, j] 166
} else { 167
H[i, j] = 0.0 168
} 169
} 170
if i < n { 171
H[i, i] = H[i, i] + 0.5 * KG[slot, i, 3] * KG[slot, i, 3] 172
} else { 173
H[i, i] = 1000.0 + i 174
} 175
} 176
} 177
178
def add_density(slot, acc) { 179
n = NPW[slot] 180
out = acc 181
for b to NBANDS { 182
if FOCC[slot, b] > 1.0e-14 { 183
fill(BOX, 0.0) 184
for i to n { 185
BOX[wrap(MILL[slot, i, 0]), wrap(MILL[slot, i, 1]), wrap(MILL[slot, i, 2])] = CVK[slot, i, b] 186
} 187
ifft3(BOX) 188
mag = abs(BOX) 189
out = out + (KWT[slot] * FOCC[slot, b] * NGRID * NGRID / OMEGA) * mag * mag 190
} 191
} 192
return out 193
} 194
195
narrate "self-consistent field" 196
ETOT = 0.0 197
EOLD = 0.0 198
EFERMI = 0.0 199
DRHO = 1.0 200
ITER = 0.0 201
DONE = false 202
NHIST = 0.0 203
while not DONE and ITER < MAXIT { 204
step xc "the LDA exchange-correlation energy density and potential at every grid point" { 205
exc, vxc = lda_xc(RHO) 206
} 207
208
step potential "the local potential V_ps + V_H + V_xc in G space: density to G by FFT, Poisson for Hartree" { 209
rg = complex(RHO, ZERO3) 210
fft3(rg) 211
vxg = complex(vxc, ZERO3) 212
fft3(vxg) 213
EH = build_vtot(rg, vxg) 214
} 215
216
step diagonalize "build H(k) at every k-point and diagonalize it" { 217
# every k-point first: the Fermi level needs ALL the eigenvalues before 218
# any band can be given an occupation 219
for k to NKPT { 220
build_h(k) 221
eig_herm(H, EV, CV) 222
EIG[k, :] = EV[0:NBANDS] 223
CVK[k, 0:NPW[k], :] = CV[0:NPW[k], 0:NBANDS] 224
} 225
} 226
step occupy "fill the lowest bands (or Fermi-Dirac occupations for a metal)" { 227
if SMEAR > 0.0 { EFERMI = fermi_level() } 228
ENT = fill_occupations(EFERMI) 229
} 230
231
step density "the new density from the occupied bands, and the band energy" { 232
rhonew = ZERO3 * 0.0 233
EBAND = 0.0 234
for k to NKPT { 235
for b to NBANDS { 236
EBAND = EBAND + KWT[k] * FOCC[k, b] * EIG[k, b] 237
} 238
rhonew = add_density(k, rhonew) 239
} 240
} 241
242
step energy "the total energy, and how far the new density is from the old one" { 243
ETOT = EBAND - EH - sum(vxc * RHO) * DV + sum(exc * RHO) * DV - SMEAR * ENT 244
diff = rhonew - RHO 245
DRHO = sqrt(sum(diff * diff) * DV) 246
show ETOT 247
show DRHO 248
DONE = DRHO < SCF_TOL and abs(ETOT - EOLD) < SCF_TOL 249
EOLD = ETOT 250
} 251
step mix "if not converged, mix the old and new densities into the next input (Anderson)" { 252
if not DONE { 253
nxt = anderson_step(RHO, diff, NHIST) 254
AND_R3 = AND_R2 255
AND_R2 = AND_R1 256
AND_R1 = RHO 257
AND_F3 = AND_F2 258
AND_F2 = AND_F1 259
AND_F1 = diff 260
if NHIST < 3.0 { NHIST = NHIST + 1.0 } 261
RHO = nxt 262
} 263
} 264
ITER = ITER + 1.0 265
} 266
show ITER 267
show DRHO 268
269
# ======================================================= 8. the band structure 270
271
narrate "band structure" 272
BANDS = zeros(NPTS, NBANDS) 273
step bands "the bands along the path: H(k) at each path point, with the converged potential" { 274
for p to NPTS { 275
setup_slot(SCRATCH, PATHK[p, 0], PATHK[p, 1], PATHK[p, 2]) 276
build_vnl(SCRATCH) 277
build_h(SCRATCH) 278
eig_herm(H, EV, CV) 279
BANDS[p, :] = EV[0:NBANDS] 280
} 281
} 282
283
# Band energies in eV from the natural zero: the Fermi level of a metal, the 284
# valence-band maximum (over the path) of an insulator. 285
step zero "band energies in eV, measured from the valence-band maximum (or the Fermi level)" { 286
NVAL = NELEC / 2.0 287
EREF = EFERMI 288
if SMEAR <= 0.0 { 289
EREF = max(BANDS[:, NVAL - 1.0]) 290
} 291
EVB = (BANDS - EREF) * HA_EV 292
} 293
show NKPT 294
show NPWMAX 295
show NG 296
297
# SPDX-License-Identifier: MIT OR Apache-2.0 298
# SPDX-FileCopyrightText: 2026 The Viv authors