Files
2026-05-29 10:26:30 +05:30

192 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Self-Consistent Field (SCF) driver for plane-wave LDA-DFT.
Uses FFT-based Davidson iterative diagonalization — H is never built explicitly.
Cost per SCF step: O(n_occ × N_G × log N_G) for H applications.
KS Hamiltonian: H = T + V_KS(r)
T|ψ⟩_G = (|k+G|²/2) ψ(G) [diagonal in G-space]
V|ψ⟩(G) = FFT[ V_KS(r) · IFFT[ψ](r) ] [FFT convolution]
"""
import numpy as np
from .cell import Cell
from .xc import xc_energy, xc_potential
from .pseudo import build_ionic_potential, n_valence_electrons
from .diag import davidson
class SCFResult:
def __init__(self):
self.converged = False
self.e_total = 0.0
self.e_kinetic = 0.0
self.e_hartree = 0.0
self.e_xc = 0.0
self.e_ion_elec = 0.0
self.e_ion_ion = 0.0
self.eigenvalues = None
self.rho_r = None
self.n_iter = 0
def __repr__(self):
lines = [
f"SCF {'converged' if self.converged else 'NOT converged'} "
f"in {self.n_iter} iterations",
f" E_total = {self.e_total:+.8f} Ha",
f" E_kinetic = {self.e_kinetic:+.8f} Ha",
f" E_hartree = {self.e_hartree:+.8f} Ha",
f" E_xc = {self.e_xc:+.8f} Ha",
f" E_ion-elec = {self.e_ion_elec:+.8f} Ha",
f" E_ion-ion = {self.e_ion_ion:+.8f} Ha",
]
if self.eigenvalues is not None:
eig_str = " ".join(f"{e:.6f}" for e in self.eigenvalues)
lines.append(f" Eigenvalues (Ha): {eig_str}")
return "\n".join(lines)
def run_scf(
cell: Cell,
mol,
offset=None,
n_iter_max: int = 100,
tol: float = 1e-6,
mix: float = 0.4,
diis_start: int = 4,
diis_size: int = 8,
diag_tol: float = 1e-7,
verbose: bool = True,
) -> SCFResult:
"""
Plane-wave LDA-DFT SCF (closed shell, Γ-point).
Parameters
----------
cell : Cell
mol : Molecule
offset : (3,) shift added to atomic coords (from Cell.from_molecule).
n_iter_max : Maximum SCF iterations.
tol : Density convergence: √(∫|Δρ|² dV) < tol.
mix : Linear mixing fraction.
diis_start : Iteration to begin Pulay DIIS.
diis_size : DIIS history depth.
diag_tol : Residual tolerance for the Davidson diagonaliser.
verbose : Print iteration table.
"""
n_elec = n_valence_electrons(mol)
# Restricted calculation: pair electrons, last orbital singly occupied if odd
n_occ = (n_elec + 1) // 2
occ = np.full(n_occ, 2.0)
if n_elec % 2 == 1:
occ[-1] = 1.0
V_ion = build_ionic_potential(mol, cell, offset) # (N_r,) fixed
# Initial density: uniform
rho = np.full(cell.N_r, n_elec / cell.volume)
result = SCFResult()
result.e_ion_ion = mol.nuclear_repulsion
diis_rho: list = []
diis_err: list = []
guess = None # warm-start Davidson between SCF iterations
if verbose:
print(f"{'Iter':>4} {'E_total':>16} {'ΔE':>12} {'|Δρ|':>12}")
print("-" * 56)
e_prev = 0.0
for iteration in range(1, n_iter_max + 1):
# ----- KS potential -----
V_H = cell.hartree_potential(rho)
v_xc = xc_potential(rho)
V_KS = V_ion + V_H + v_xc # (N_r,)
# ----- Diagonalise -----
evals, evecs = davidson(
cell, V_KS, n_occ,
n_extra=max(4, n_occ),
tol=diag_tol,
guess=guess,
)
guess = evecs.copy() # warm start next iteration
# ----- New density -----
orbs_G = evecs # (n_occ, N_G)
rho_new = cell.density_from_orbitals(orbs_G, occ)
# ----- Energies -----
# E_kin = Σ_n f_n Σ_G KE_G |ψ̂_n(G)|² / V
e_kin = float(np.einsum('n,nG,G->', occ, np.abs(orbs_G)**2, cell.KE_G) / cell.volume)
e_hartree = 0.5 * float(np.dot(V_H, rho_new) * cell.dV)
e_xc = xc_energy(rho_new, cell.dV)
e_ion_elec = float(np.dot(V_ion, rho_new) * cell.dV)
e_total = e_kin + e_hartree + e_xc + e_ion_elec + result.e_ion_ion
delta_E = e_total - e_prev
e_prev = e_total
# ----- Convergence -----
delta_rho = rho_new - rho
conv = float(np.sqrt(np.dot(delta_rho, delta_rho) * cell.dV))
if verbose:
print(f"{iteration:>4} {e_total:>16.8f} {delta_E:>+12.2e} {conv:>12.2e}")
# ----- Density mixing -----
diis_rho.append(rho_new.copy())
diis_err.append(delta_rho.copy())
if len(diis_rho) > diis_size:
diis_rho.pop(0)
diis_err.pop(0)
if iteration >= diis_start and len(diis_err) >= 2:
rho = _pulay_mix(diis_rho, diis_err)
else:
rho = (1 - mix) * rho + mix * rho_new
# Enforce ρ ≥ 0 and particle conservation
rho = np.maximum(rho, 0.0)
rho *= n_elec / (np.sum(rho) * cell.dV)
if conv < tol and iteration > 2:
result.converged = True
break
result.n_iter = iteration
result.e_total = e_total
result.e_kinetic = e_kin
result.e_hartree = e_hartree
result.e_xc = e_xc
result.e_ion_elec = e_ion_elec
result.eigenvalues = evals
result.rho_r = rho_new
if verbose:
print()
print(result)
return result
def _pulay_mix(rho_hist: list, err_hist: list) -> np.ndarray:
"""Pulay DIIS: optimal linear combination of stored densities."""
m = len(err_hist)
B = np.zeros((m + 1, m + 1))
for i in range(m):
for j in range(m):
B[i, j] = np.dot(err_hist[i], err_hist[j])
B[m, :] = -1.0
B[:, m] = -1.0
B[m, m] = 0.0
rhs = np.zeros(m + 1)
rhs[m] = -1.0
try:
c = np.linalg.solve(B, rhs)[:m]
except np.linalg.LinAlgError:
c = np.zeros(m); c[-1] = 1.0
return sum(ci * ri for ci, ri in zip(c, rho_hist))