In 1920-1930, Schrödinger proposed the famous equation to calculate the wavefunction
-
$\hat{H}$ is the Hamiltonian operator, including kinetic and potential energies. -
$\Psi$ is the many-electron wavefunction, depending on the positions of all electrons. -
$E$ is the total energy of the system.
The Hamiltonian for such a system is:
where
- The first term represents the kinetic energy of each electron (often denoted as
$\hat{T}$ ) -
$V_{\text{external}}$ represents the external potential. - The second summation accounts for electron-electron Coulomb interactions.
Let's first review how to obtain a numerical solution to the most simple case, i.e., a single electron under a given external potential, i.e.,
A typical solution involves the following steps:
- Define the discete grids to describe the wavefunction spanning.
-
Define the external potential (
$V_\textrm{external}$ ) on the given spatial grids. -
Build up the Hamiltonian matrix (
$H$ ) on the given spatial grids based on the$V_\textrm{external}$ and kinetic energy operator. -
Solve the eigenvalue and eigenvectors for the given
$H$
The kinetic energy operator (
To solve this equation numerically, we approximate the second derivative using the finite difference method. Consider a function
The expression tells us how to approximate the curvature of the function at each point using values of the function at neighboring points.
To apply this approximation to a system with (-2, +1, +1)
that multiply the values
-
np.eye(N)
: an identity matrix of$N \times N$ . -
np.eye(N, k=1)}
: a matrix of$N \times N$ with ones on the first upper diagonal. -
np.eye(N, k=-1)
: a matrix of$N \times N$ with ones on the first lower diagonal.
Thus, the combination of these matrices gives us a matrix representation of the finite difference approximation of the second derivative operator across the entire grid. The division by
For a 5-point grid, the matrix
This matrix will act on a vector representing the function
If we limit ourselves to a 1D space, the follow code gives an example to obtain the numerical solution. One can try to adjust the
# Spatial grid parameters
x_min, x_max = -10.0, 10.0
N = 1000 # Number of grid points
x = np.linspace(x_min, x_max, N)
dx = x[1] - x[0]
# Definite the external potential
V_ext = 0.5 * x**2 # a harmonic oscillator
#V_ext = -1 / np.sqrt((x**2+0.5)) # eletric field under a positive charge
# Define the kinetic energy operator
T = -0.5 * (-2 * np.eye(N) + np.eye(N, k=1) + np.eye(N, k=-1)) / dx**2
# Total Hamiltonian
H = T + np.diag(V_ext)
# Solve eigenvalue problem
energies, wavefunctions = np.linalg.eigh(H)
# Get electron density
psi = wavefunctions[:, 0]
psi /= np.sqrt(np.sum(np.abs(psi)**2) * dx)
rho = np.abs(psi)**2
# Print the energies
T_s = np.sum(psi * T.dot(psi)) * dx
E_ext = np.sum(rho * V_ext) * dx
print(f"Kinetic Energy (T) = {T_s:.6f} Hartree")
print(f"External Energy (E_ext) = {E_ext:.6f} Hartree")
print(f"Total Energy (E_total) = {energies[0]:.6f} Hartree")
After the results are obtained, one can also visualize the wavefunction or electron density for its ground states or excited states.
# Plot the effective potential
fig, axs = plt.subplots(1, 2, figsize=(12, 4))
axs[0].plot(x, V_ext, label='$V_{ext}$', linestyle='--')
axs[0].set_xlabel('Position $x$')
axs[0].set_ylabel('Potential')
for i in range(3):
axs[0].axhline(energies[i])
axs[0].legend()
# Plot the electron density
for i in range(3):
rho = np.abs(wavefunctions[:, i])**2 # Ground state
rho /= (rho.sum() * dx)
axs[1].plot(x, rho, label=f'E{i}={energies[i]:.1f} Ha')
axs[1].plot(x, rho)
axs[1].set_xlabel('Position $x$')
axs[1].set_ylabel('Density')
axs[1].legend()
plt.tight_layout()
plt.show()
For a more practical application, we often deal with the electrons at a 3D space, below is a script that solves the single hydrogen atom system.
import numpy as np
from scipy.sparse import kron, eye
from scipy.sparse import diags, csr_matrix
from scipy.sparse.linalg import eigsh # Sparse eigenvalue solver for faster performance
import matplotlib.pyplot as plt
# Define 3D grid parameters
N = 60 # Grid size along each dimension
L = 10 # Simulation box size in Bohr
dx = L / N
x = np.linspace(-L/2, L/2, N)
y = np.linspace(-L/2, L/2, N)
z = np.linspace(-L/2, L/2, N)
X, Y, Z = np.meshgrid(x, y, z, indexing="ij")
# Define the external potential to mimic the hydrogen proton
num_electrons = 1.0
R1 = np.array([0, 0, 0])
softening = 0.02 # To prevent singularity at nuclei
scaling_factor = 1.0 # Scale down the external potential
V_ext = scaling_factor * (-1 / np.sqrt((X - R1[0])**2 + (Y - R1[1])**2 + (Z - R1[2])**2 + softening**2))
def kinetic_energy_operator(N, dx):
# Define 1D kinetic energy finite difference operator
main_diag = -2 * np.ones(N)
side_diag = np.ones(N - 1)
T_1D = diags([main_diag, side_diag, side_diag], [0, -1, 1], shape=(N, N)) / dx**2
# Build 3D kinetic energy operator using Kronecker products
I = eye(N, format='csr') # Identity matrix for each dimension
T = kron(kron(T_1D, I), I) + kron(kron(I, T_1D), I) + kron(kron(I, I), T_1D)
return -0.5 * T # Scale by -0.5 for the kinetic energy operator
# Kinetic energy operator (sparse)
T = kinetic_energy_operator(N, dx)
V_ext_flat = V_ext.flatten()
H = T + diags(V_ext_flat, 0, shape=(N**3, N**3))
# Solve the single-electron equation with a faster solver
energies, orbitals = eigsh(H, k=5, which='SA')
print("Energies from the solver", energies)
# Normalize wavefunction to ensure it represents one electron
psi = orbitals[:, 0].reshape((N, N, N)) # Ground state orbital
psi /= np.sqrt(np.sum(np.abs(psi)**2) * dx**3)
rho = np.abs(psi)**2 # Electron density
# Compute energies
T_s = np.sum(psi.flatten() * T.dot(psi.flatten())) * dx**3 # Kinetic energy with psi
E_ext = np.sum(rho * V_ext) * dx**3
print(f"Kinetic Energy (T) = {T_s:.6f} Hartree")
print(f"External Energy (E_ext). = {E_ext:.6f} Hartree")
print(f"Total Energy (E_total)= {T_s + E_ext:.6f} Hartree\n")
# plot electron density for the first few solutions
for i in range(5):
psi = orbitals[:, i].reshape((N, N, N))
psi /= np.sqrt(np.sum(np.abs(psi)**2) * dx**3)
rho = np.abs(psi)**2
rho1 = rho[N//2, :, :]
print(i, np.sum(rho)*dx**3, np.sum(rho1)*dx**3, rho.max())
plt.imshow(rho1,
origin='lower',
vmin=1e-5,
vmax=5e-3)
plt.xlabel('x (Bohr)')
plt.ylabel('y (Bohr)')
plt.title(f'Single H atom in level {i}: {energies[i]:.2f} Ha')
plt.colorbar(label='Electron Density')
plt.show()
Note that in the above codes, we also compute the kinetic and external energy to double check if the implemented code works properly.
The numerical expressions are
To check if your implmentation is correct, one should compare the results with the analytical solutions for a single H atom. The energy levels for a hydrogen atom in Hartree units (where 1 Hartree = 27.2 eV) are given by the formula:
At the ground state, the system has
Additionally, one should plot the first few hydrogen's atomic orbitals (e.g.,
One can find more analysis and instruction in the notebook.
While it is straightforward to deal with single electron equation, solving the Schrödinger equation for systems with more than one electron becomes intractable due to the electron-electron interactions. Hartree and Hartree-Fock Methods were developed to apply the mean-field approximations to simplify the many-body problem but still faced limitations, particularly in accounting for electron correlation.
It is not easy to solve this equation due to the following issues,
- The wavefunction
$\Psi$ depends on$3N$ spatial coordinates, making it computationally infeasible for large$N$ . - Computational resources required grow exponentially with the number of electrons.
- Properly accounting for interactions and correlations between electrons is challenging.
- Methods like Hartree-Fock simplify the problem but neglect electron correlation, leading to inaccuracies.
Thanks to Walter Kohn and many other pioneers, Density Functional Theory (DFT) has become an indispensable tool in physics, chemistry, and materials science for studying the electronic structure of matter. We will examine some basics in this section.
In 1964, Pierre Hohenberg and Walter Kohn established Density Functional Theory (DFT) that the ground-state properties of a many-electron system are uniquely determined by its electron density
-
The ground-state electron density
$\rho_0(\mathbf{r})$ uniquely determines the external potential$V_{\text{external}}(\mathbf{r})$ , up to a constant, and hence all properties of the system. -
There exists a universal functional
$E[\rho]$ such that the ground-state energy$E_0$ can be obtained variationally:
where
From these two theorems, we can find that
- All observables are functionals of the electron density
$\rho(\mathbf{r})$ . - One can reduce the problem from dealing with a many-body wavefunction to a function of three spatial variables.
- The ground-state density
$\rho_0(\mathbf{r})$ minimizes the energy functional$E[\rho]$ .
In 1965, Walter Kohn and Lu Jeu Sham applied Variational Principle to develope a method to make DFT practical by introducing a system of non-interacting electrons that produce the same ground-state density as the interacting system.
Following Kohn-Sham Formalism, the total Energy Functional is expressed as:
-
$T_s[\rho]$ : Kinetic energy of non-interacting electrons. -
$E_{\text{external}}[\rho]$ : Interaction with external potential. -
$E_{\text{Hartree}}[\rho]$ : Hartree energy (classical electron-electron repulsion). -
$E_{\text{XC}}[\rho]$ : Exchange-correlation energy, encompassing all many-body effects.
Thus, the Schrödinger equation can be decomposed into a series of Kohn-Sham Equations:
where
-
$\phi_i(\mathbf{r})$ : Kohn-Sham orbitals. -
$\epsilon_i$ : Orbital energies. -
$V_{\text{eff}}(\mathbf{r})$ : Effective potential, including$V_{\text{external}}(\mathbf{r}) + V_{\text{Hartree}}[\rho] + V_{\text{XC}}(\mathbf{r})$ .
Technically, we can solve the single electron equation as long as the Effective potential
First, for a system with nuclei located at positions
Second, the Hartree potential
In practice, the integral is a long-range term and it needs to be computed using techniques such as Fourier transforms or Poisson solvers to handle the long-range nature of the Coulomb interaction.
The Hartree energy is given by:
This integral computes the repulsion between all pairs of infinitesimal density elements at positions
Finally, the Exchange-Correlation Functional is a tricky term that represents the difference between the true kinetic and electron-electron interaction energies and those of the non-interacting reference system. The exact form is unknown. Hence approximations are necessary.
where
In the original paper, it was found that when the electron density does not vary significantly (e.g., uniform electron gas, nearly free electrons in a metal). It can be estimated by the local density approxiamation (LDA). For the exchange energy in the LDA, the commonly used expression (for spin-unpolarized systems) is:
The correlation energy
For spin-unpolarized systems, the correlation energy per electron can be expressed as:
This part can be integrated in a similar way:
After choosing the form of
One can then use the updated
In short, DFT proposed two important concepts to solve the many electron problems
- The simplification from many electron equations to a set of single electron KS equations. To enable this transition, one needs to seek an optimal effective potential for the single KS equations. This requires the introduction of Hartree potential (
$V_{\text{Hartree}}$ ) and Exchange-Correlation potential ($V_{\text{XC}}$ ) on top of the traditional single electron problems based on ($T + V_{\text{external}}$ ) only. - When solving the KS equations, we use the electron density (
$\rho$ ) to construct the$\hat{H}$ . Thus all potential terms needs to be expressed as the function of$\rho$ .
After knowing
flowchart TD
InitDensity["Initial Guess of ρ(r)"]
ComputeVeff["Compute V_eff(r)"]
SolveKS["Solve KS Equations"]
RecomputeDensity["Recompute ρ(r)"]
CheckConvergence{"Is ρ(r) Converged?"}
Terminate["Terminate"]
InitDensity --> ComputeVeff
ComputeVeff --> SolveKS
SolveKS --> RecomputeDensity
RecomputeDensity --> CheckConvergence
CheckConvergence -- Yes --> Terminate
CheckConvergence -- No --> ComputeVeff
- Different Potentials: (e.g., a double-well potential)
- Modify the code to include additional occupied orbitals.
- Implement GGA or other exchange-correlation approximations.