Computational Physics › Quantum Computational Methods
Evolving wavefunctions in time and finding electronic ground states — the numerical heart of quantum simulation.
Time-Dependent Schrödinger Equation
class QuantumEvolution:
"""Solve time-dependent Schrödinger equation"""
def __init__(self, x_range, n_points=256):
self.x_min, self.x_max = x_range
self.n = n_points
self.dx = (self.x_max - self.x_min) / (n_points - 1)
self.x = np.linspace(self.x_min, self.x_max, n_points)
# Momentum space
self.k = 2 * np.pi * np.fft.fftfreq(n_points, self.dx)
def gaussian_wavepacket(self, x0, k0, sigma):
"""Initialize Gaussian wavepacket"""
psi = np.exp(-(self.x - x0)**2 / (2 * sigma**2))
psi *= np.exp(1j * k0 * self.x)
psi /= (2 * np.pi * sigma**2) ** 0.25
return psi
def split_operator_step(self, psi, V, dt):
"""Split-operator method for time evolution"""
# Half step in position space
psi *= np.exp(-1j * V * dt / 2)
# Full step in momentum space
psi_k = np.fft.fft(psi)
psi_k *= np.exp(-1j * self.k**2 * dt / 2)
psi = np.fft.ifft(psi_k)
# Half step in position space
psi *= np.exp(-1j * V * dt / 2)
return psi
def crank_nicolson_step(self, psi, H, dt):
"""Crank-Nicolson method for time evolution"""
# (1 + i*dt*H/2)𝜓(t+dt) = (1 - i*dt*H/2)𝜓(t)
I = np.eye(self.n)
A = I + 1j * dt * H / 2
B = I - 1j * dt * H / 2
# Solve linear system
b = B @ psi
psi_new = np.linalg.solve(A, b)
return psi_new
def finite_difference_hamiltonian(self, V):
"""Construct Hamiltonian matrix using finite differences"""
H = np.zeros((self.n, self.n), dtype=complex)
# Kinetic energy (second derivative)
for i in range(1, self.n - 1):
H[i, i-1] = -0.5 / self.dx**2
H[i, i] = 1.0 / self.dx**2 + V[i]
H[i, i+1] = -0.5 / self.dx**2
# Boundary conditions
H[0, 0] = 1.0 / self.dx**2 + V[0]
H[0, 1] = -0.5 / self.dx**2
H[-1, -2] = -0.5 / self.dx**2
H[-1, -1] = 1.0 / self.dx**2 + V[-1]
return H
def tunnel_barrier_simulation(self):
"""Quantum tunneling through a barrier"""
# Potential barrier
V = np.zeros_like(self.x)
barrier_width = 2.0
barrier_height = 5.0
V[np.abs(self.x) < barrier_width/2] = barrier_height
# Initial wavepacket
psi = self.gaussian_wavepacket(x0=-5, k0=3, sigma=1)
# Time evolution
dt = 0.01
n_steps = 1000
results = []
for step in range(n_steps):
psi = self.split_operator_step(psi, V, dt)
if step % 10 == 0:
# Calculate transmission and reflection
transmitted = np.sum(np.abs(psi[self.x > barrier_width/2])**2) * self.dx
reflected = np.sum(np.abs(psi[self.x < -barrier_width/2])**2) * self.dx
results.append({
'time': step * dt,
'psi': psi.copy(),
'transmitted': transmitted,
'reflected': reflected
})
return results, V
# Visualize quantum tunneling
qe = QuantumEvolution(x_range=(-10, 10), n_points=512)
results, V = qe.tunnel_barrier_simulation()
# Animation of wavefunction
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
# Wavefunction plot
line1, = ax1.plot(qe.x, np.abs(results[0]['psi'])**2, 'b-', label='|𝜓|²')
line2, = ax1.plot(qe.x, np.real(results[0]['psi']), 'r--', label='Re(𝜓)')
ax1.fill_between(qe.x, 0, V/10, alpha=0.3, label='Potential')
ax1.set_ylabel('Wavefunction')
ax1.legend()
ax1.grid(True)
# Transmission/Reflection plot
times = [r['time'] for r in results]
trans = [r['transmitted'] for r in results]
refl = [r['reflected'] for r in results]
ax2.plot(times, trans, 'g-', label='Transmitted')
ax2.plot(times, refl, 'r-', label='Reflected')
ax2.set_xlabel('Time')
ax2.set_ylabel('Probability')
ax2.legend()
ax2.grid(True)
plt.tight_layout()
plt.show()
Density Functional Theory (DFT)
The snippet below is illustrative, simplified pseudocode meant to show the shape of a self-consistent Kohn-Sham loop — building the effective potential, solving for a density, and mixing. It is not a numerically valid DFT solver: a real implementation diagonalizes a Hamiltonian whose kinetic operator is diagonal in momentum space while the potential is diagonal in position space (so the two are never simply added in the same basis), and it constructs the new density from the resulting eigenfunctions.
class SimpleDFT:
"""Simplified 1D DFT implementation (schematic — see note above)"""
def __init__(self, n_grid=100, box_length=10):
self.n = n_grid
self.L = box_length
self.dx = self.L / n_grid
self.x = np.linspace(-self.L/2, self.L/2, n_grid)
# Kinetic energy operator in momentum space
self.k = 2 * np.pi * np.fft.fftfreq(n_grid, self.dx)
self.T_k = 0.5 * self.k**2
def thomas_fermi_functional(self, n):
"""Thomas-Fermi kinetic energy functional"""
C_TF = (3/10) * (3 * np.pi**2)**(2/3)
return C_TF * np.sum(n**(5/3)) * self.dx
def exchange_functional(self, n):
"""Local density approximation for exchange"""
C_x = -(3/4) * (3/np.pi)**(1/3)
return C_x * np.sum(n**(4/3)) * self.dx
def hartree_potential(self, n):
"""Solve Poisson equation for Hartree potential"""
# Fourier space solution
n_k = np.fft.fft(n)
V_H_k = np.zeros_like(n_k)
# V_H = 4π n / k² (avoiding k=0)
V_H_k[1:] = 4 * np.pi * n_k[1:] / self.k[1:]**2
V_H_k[0] = 0 # Set DC component
return np.real(np.fft.ifft(V_H_k))
def kohn_sham_step(self, n, V_ext):
"""Single Kohn-Sham iteration"""
# Calculate potentials
V_H = self.hartree_potential(n)
# Exchange-correlation potential (LDA)
V_xc = -(3/np.pi)**(1/3) * n**(1/3)
# Total effective potential
V_eff = V_ext + V_H + V_xc
# SCHEMATIC ONLY: a real Kohn-Sham solver diagonalizes a Hamiltonian
# whose kinetic term T_k is diagonal in momentum space and whose
# potential V_eff is diagonal in position space. The two operators
# live in different bases and must NOT be added directly; the line
# below is a placeholder standing in for that diagonalization.
H_diag = self.T_k + np.fft.fft(V_eff) # not physical — schematic placeholder
# Find eigenvalues and eigenvectors
# (Simplified: assuming non-interacting electrons)
energies = np.sort(np.real(H_diag))
# Construct new density (simplified)
# In real DFT, we'd solve for eigenfunctions
n_new = self.initial_density(V_ext) # Placeholder
return n_new, energies
def initial_density(self, V_ext):
"""Initial guess for electron density"""
# Use Thomas-Fermi approximation
mu = 1.0 # Chemical potential (adjust as needed)
n = np.maximum(0, mu - V_ext)**(3/2)
# Normalize to correct number of electrons
N_electrons = 10 # Example
n *= N_electrons / (np.sum(n) * self.dx)
return n
def self_consistent_field(self, V_ext, max_iter=50, tol=1e-6):
"""Self-consistent field iteration"""
n = self.initial_density(V_ext)
for i in range(max_iter):
n_old = n.copy()
# Kohn-Sham step
n, energies = self.kohn_sham_step(n, V_ext)
# Mix old and new density
alpha = 0.3 # Mixing parameter
n = alpha * n + (1 - alpha) * n_old
# Check convergence
error = np.max(np.abs(n - n_old))
if error < tol:
print(f"Converged in {i+1} iterations")
break
return n, energies
Previous: Finite Elements & Fluid Dynamics · Next: Parallel Computing & Machine Learning
See Also
- Quantum Mechanics — the physics behind wavefunction evolution and the Schrödinger equation.
- Condensed Matter Physics — density functional theory and electronic structure in solids.
- Monte Carlo & Molecular Dynamics — variational and quantum Monte Carlo methods.