Skip to main content

Key Algorithms

This page collects pseudocode, parameter tables, and implementation notes for the six core algorithms in KRONOS: the Davidson iterative eigensolver, Pulay/DIIS density mixing with Kerker preconditioning, Fermi level bisection, the Hamiltonian application (H|ψ>), Ewald summation, and the Kleinman-Bylander nonlocal pseudopotential. These algorithms live in src/solver/, src/hamiltonian/, and src/potential/; see Source Layout for exact file paths. The Data Flow page shows how the inputs and outputs of these algorithms connect.

5.1 Davidson Iterative Eigensolver

Finds the lowest num_bands eigenvalues of H without forming the full Hamiltonian matrix. Implemented in src/solver/davidson.cpp.

Parameters:

  • Max subspace size: 3 * num_bands (configurable via subspace_factor)
  • Convergence tolerance: 1e-6 on residual norm
  • Max iterations: 100
  • Deterministic random seed (42) for reproducibility

Algorithm:

Input: H|.> operator, kinetic diagonal preconditioner, num_bands, num_pw
Output: {epsilon_n, psi_n} for n = 1..num_bands

1. Initialize V with num_bands random vectors (seed = 42)
2. Orthogonalize via modified Gram-Schmidt with reorthogonalization
(two passes for numerical stability)
3. Apply H to each basis vector: HV[i] = H|V[i]>
4. LOOP (max 100 iterations):
a. Build projected Hamiltonian: H_ij = <V_i|HV_j>
(Hermitian: only compute upper triangle)
b. Diagonalize via LAPACK zheev (complex Hermitian eigensolver)
c. Compute Ritz vectors: psi_n = sum_i c_ni * V_i
and H*psi_n = sum_i c_ni * HV_i
d. Compute residuals: r_n = H|psi_n> - epsilon_n * |psi_n>
e. If max(||r_n||) < tolerance: CONVERGED, return
f. If subspace full (m + num_bands > 3*num_bands):
RESTART with current Ritz vectors as new basis
g. Apply kinetic preconditioner:
t_n[G] = r_n[G] / (|k+G|^2 - epsilon_n)
with floor |denom| >= 1e-4 to prevent blowup
h. Orthogonalize t_n against all V (two passes)
i. If ||t_n|| > 1e-10: normalize, add to V, compute H|t_n>
j. If no vectors added: stop (preconditioner too exact)

5.2 Pulay/DIIS Density Mixing

Accelerates SCF convergence by finding the optimal linear combination of previous density/residual pairs. Implemented in src/solver/mixing.cpp.

History depth M = 8, mixing parameter alpha = 0.3

1. First step: simple linear mixing
rho_new = rho_in + alpha * (rho_out - rho_in)

2. Subsequent steps (DIIS):
a. Store (rho_in, R = rho_out - rho_in) in history deque
b. Build overlap matrix: B_ij = <R^(i) | R^(j)> (real dot product)
c. Solve augmented constrained system via Gaussian elimination
with partial pivoting:

| B_11 B_12 ... B_1M 1 | | c_1 | | 0 |
| B_21 B_22 ... B_2M 1 | | c_2 | | 0 |
| ... 1 | | ... | = | 0 |
| B_M1 B_M2 ... B_MM 1 | | c_M | | 0 |
| 1 1 ... 1 0 | | lam | | 1 |

d. Optimal density:
rho_new = sum_i c_i * [rho_in^(i) + alpha * R^(i)]
e. If matrix is singular (pivot < 1e-15): fall back to using
only the most recent entry (avoids destroying convergence)

Kerker Preconditioner (for metals, activated when smearing != None):

Applied to the residual in G-space before Pulay mixing:

R_precond(G) = R(G) * |G|^2 / (|G|^2 + q0^2) q0 = 1.5 bohr^{-1}

This suppresses long-wavelength (small |G|) charge oscillations that cause "charge sloshing" in metallic systems. At G=0, the filter gives 0, eliminating uniform charge shifts entirely.

5.3 Fermi Level Bisection

Finds the Fermi energy such that the total electron count matches the target. Implemented in src/solver/fermi.cpp.

Input: eigenvalues[k][n], k-weights, target_electrons, smearing type/width
Output: Fermi energy E_F, occupation matrix f[k][n]

1. Set energy bounds: e_min = min(all eigenvalues) - 10*degauss
e_max = max(all eigenvalues) + 10*degauss

2. Bisection loop (max 200 steps, tolerance 1e-10 Ry):
a. e_mid = (e_min + e_max) / 2
b. Count electrons:
N(E_F) = sum_k sum_n spin_factor * w_k * f((epsilon_nk - E_F) / degauss)
c. If |N - N_target| < 1e-8: converged, exit
d. If N < N_target: e_min = e_mid
If N > N_target: e_max = e_mid

3. Compute final occupations:
f_nk = spin_factor * f((epsilon_nk - E_F) / degauss)

Supported smearing functions:

TypeFormula f(x)Use case
NoneStep function: 1 if x <= 0, else 0Insulators
Gaussian0.5 * erfc(x)General metals
Marzari-VanderbiltCold smearing (PRL 82, 3296, 1999)Metals (improved)
Fermi-Dirac1 / (1 + exp(x))Finite-temperature metals

5.4 H|psi> Application (Hamiltonian)

The Kohn-Sham Hamiltonian operator applies three terms to a wavefunction. Implemented in src/hamiltonian/hamiltonian.cpp.

H|psi> = T|psi> + V_eff|psi> + V_NL|psi>

1. Kinetic (pointwise in G-space):
(T|psi>)_G = |k+G|^2 * psi_G [Rydberg units, NOT /2]

2. Local effective potential (via FFT):
a. scatter psi_G onto full FFT grid
b. psi(r) = IFFT(psi_G_grid)
c. (V*psi)(r) = V_eff(r) * psi(r) [pointwise real-space multiply]
d. (V*psi)_G_grid = FFT((V*psi)(r))
e. gather (V*psi)_G from grid back to PW basis

3. Nonlocal PP (Kleinman-Bylander):
a. Precompute beta_i(k+G) for this k-point (cached)
b. proj_j = <beta_j|psi> = sum_G conj(beta_j(G)) * psi(G)
c. (V_NL|psi>)_G = sum_{i,j} D_ij * proj_j * beta_i(G)

Per-k masking: The shared PW basis is expanded to cover all k-points, but each k-point only uses G-vectors where |k+G|^2 <= ecutwfc. The get_apply_function method creates a closure that masks inactive components to zero on input and applies a high energy wall (1e4 * psi_G) on output, pushing the Davidson solver to converge inactive components to zero amplitude.

5.5 Ewald Summation

Computes the electrostatic energy of periodic point charges by splitting the Coulomb sum into rapidly convergent parts. Implemented in src/potential/ewald.cpp.

E_ion = E_real + E_recip + E_self + E_charged

E_real = (1/2) sum'_{T} sum_{i,j} Z_i Z_j erfc(eta*|r_ij+T|) / |r_ij+T|

E_recip = (4*pi / Omega) sum_{G!=0} |S(G)|^2 * exp(-|G|^2/(4*eta^2)) / |G|^2
where S(G) = sum_i Z_i * exp(-i G.tau_i)

E_self = -(eta / sqrt(pi)) * sum_i Z_i^2

eta = sqrt(pi) * (N_atoms / V^2)^(1/6)

Forces are the analytical derivatives of each term with respect to atomic positions, yielding separate real-space and reciprocal-space contributions.

5.6 Kleinman-Bylander Nonlocal Pseudopotential

Implemented in src/potential/nonlocal_pp.cpp. Each UPF projector with angular momentum l is expanded into (2l+1) channels for m = -l, ..., +l:

V_NL|psi> = sum_{a,i,j} D_ij^a |beta_i^a> <beta_j^a|psi>

beta_i(k+G) = (4*pi / sqrt(Omega)) * i^l
* integral r^2 beta_i(r) j_l(|k+G|*r) dr
* Y_lm(k+G_hat) * exp(-i(k+G).tau_a)

Projectors are precomputed and cached per k-point via prepare_kpoint() to avoid redundant radial Bessel transforms during the Davidson iteration.