2D Ising Model
Spin system on an \(L \times L\) lattice:
\[
H = -J \sum_{\langle i,j \rangle} s_i s_j
\]
Periodic boundary conditions are applied. Sample the canonical distribution using the Metropolis algorithm.
Physical Quantities Measured: The magnetization is \(m = \frac{1}{N}\sum_i s_i\), the energy is \(E = -J \sum_{\langle i,j \rangle} s_i s_j\), the specific heat is \(C = \beta^2 (\langle E^2 \rangle - \langle E \rangle^2)\), and the magnetic susceptibility is \(\chi = \beta N (\langle m^2 \rangle - \langle m \rangle^2)\).
# Requirements:
# - Python 3.9+
# - matplotlib>=3.7.0
# - numpy>=1.24.0, <2.0.0
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
class Ising2D:
def __init__(self, L, T, J=1.0):
"""
2D Ising Model
L: Lattice size (L x L)
T: Temperature
J: Interaction strength
"""
self.L = L
self.N = L * L
self.T = T
self.J = J
self.beta = 1.0 / T if T > 0 else np.inf
# Random initial configuration
self.spins = np.random.choice([-1, 1], size=(L, L))
# Statistics
self.energy_history = []
self.magnetization_history = []
def energy(self):
"""Total energy of the system"""
E = 0
for i in range(self.L):
for j in range(self.L):
# Interaction with nearest neighbors (periodic boundary conditions)
S = self.spins[i, j]
neighbors = (
self.spins[(i+1) % self.L, j] +
self.spins[i, (j+1) % self.L]
)
E += -self.J * S * neighbors
return E
def delta_energy(self, i, j):
"""Energy change when flipping spin (i,j)"""
S = self.spins[i, j]
neighbors = (
self.spins[(i+1) % self.L, j] +
self.spins[(i-1) % self.L, j] +
self.spins[i, (j+1) % self.L] +
self.spins[i, (j-1) % self.L]
)
return 2 * self.J * S * neighbors
def magnetization(self):
"""Magnetization"""
return np.abs(np.sum(self.spins)) / self.N
def metropolis_step(self):
"""One Metropolis step (attempt each spin once)"""
for _ in range(self.N):
# Randomly select a spin
i, j = np.random.randint(0, self.L, size=2)
# Energy change
dE = self.delta_energy(i, j)
# Metropolis criterion
if dE < 0 or np.random.random() < np.exp(-self.beta * dE):
self.spins[i, j] *= -1 # Flip spin
def simulate(self, steps, equilibration=1000):
"""Execute simulation"""
# Equilibration
for _ in range(equilibration):
self.metropolis_step()
# Measurement
for step in range(steps):
self.metropolis_step()
if step % 10 == 0: # Record every 10 steps
self.energy_history.append(self.energy())
self.magnetization_history.append(self.magnetization())
def statistics(self):
"""Calculate statistics"""
E_mean = np.mean(self.energy_history)
E_std = np.std(self.energy_history)
M_mean = np.mean(self.magnetization_history)
M_std = np.std(self.magnetization_history)
# Specific heat and susceptibility
E_array = np.array(self.energy_history)
M_array = np.array(self.magnetization_history)
C = self.beta**2 * (np.mean(E_array**2) - np.mean(E_array)**2) / self.N
chi = self.beta * self.N * (np.mean(M_array**2) - np.mean(M_array)**2)
return {
'E_mean': E_mean / self.N,
'E_std': E_std / self.N,
'M_mean': M_mean,
'M_std': M_std,
'C': C,
'chi': chi
}
# Simulate at different temperatures
L = 20
T_range = np.linspace(1.0, 4.0, 16)
J = 1.0
T_c_onsager = 2 * J / np.log(1 + np.sqrt(2)) # Onsager analytical solution
results = []
for T in T_range:
print(f"Temperature T = {T:.2f}...")
ising = Ising2D(L, T, J)
ising.simulate(steps=5000, equilibration=1000)
stats = ising.statistics()
stats['T'] = T
results.append(stats)
# Visualization
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Energy
ax1 = axes[0, 0]
T_vals = [r['T'] for r in results]
E_vals = [r['E_mean'] for r in results]
E_err = [r['E_std'] for r in results]
ax1.errorbar(T_vals, E_vals, yerr=E_err, fmt='o-', linewidth=2, markersize=6)
ax1.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax1.set_xlabel('Temperature T')
ax1.set_ylabel('Energy per spin ⟨E⟩/N')
ax1.set_title('Temperature Dependence of Energy')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Magnetization
ax2 = axes[0, 1]
M_vals = [r['M_mean'] for r in results]
M_err = [r['M_std'] for r in results]
ax2.errorbar(T_vals, M_vals, yerr=M_err, fmt='o-', linewidth=2, markersize=6, color='blue')
ax2.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax2.set_xlabel('Temperature T')
ax2.set_ylabel('Magnetization ⟨|m|⟩')
ax2.set_title('Temperature Dependence of Magnetization')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Specific heat
ax3 = axes[1, 0]
C_vals = [r['C'] for r in results]
ax3.plot(T_vals, C_vals, 'o-', linewidth=2, markersize=6, color='green')
ax3.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax3.set_xlabel('Temperature T')
ax3.set_ylabel('Specific heat C')
ax3.set_title('Specific Heat (Peak at T_c)')
ax3.legend()
ax3.grid(True, alpha=0.3)
# Susceptibility
ax4 = axes[1, 1]
chi_vals = [r['chi'] for r in results]
ax4.plot(T_vals, chi_vals, 'o-', linewidth=2, markersize=6, color='purple')
ax4.axvline(T_c_onsager, color='r', linestyle='--', linewidth=2, label=f'T_c = {T_c_onsager:.2f}')
ax4.set_xlabel('Temperature T')
ax4.set_ylabel('Susceptibility χ')
ax4.set_title('Susceptibility (Peak at T_c)')
ax4.legend()
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stat_mech_ising_mc_simulation.png', dpi=300, bbox_inches='tight')
plt.show()
# Numerical results
print("\n=== 2D Ising Model Monte Carlo Simulation ===\n")
print(f"Lattice size: {L} × {L}")
print(f"Onsager critical temperature: T_c = {T_c_onsager:.4f}\n")
print("Temperature dependence:")
for r in results:
print(f"T = {r['T']:.2f}: E/N = {r['E_mean']:.3f}, M = {r['M_mean']:.3f}, C = {r['C']:.3f}, χ = {r['chi']:.2f}")