← Back to Work

Research entry

The Ising Model — Metropolis Monte Carlo

2018 · Academic Archive

Monte Carlo simulation of the 2D and 3D Ising model using the Metropolis algorithm — computing energy, magnetisation, heat capacity, and susceptibility across a temperature sweep to find the ferromagnetic phase transition and Curie temperature.

Python Monte Carlo Statistical Physics Metropolis Algorithm NumPy Matplotlib

Open 3D Visualiser →  ·  GitHub →  ·  Full Research Writeup →

Interactive Simulation

Ising Model — Metropolis Algorithm2D Square Lattice
Steps: 0
|⟨M⟩|: 0.000
20×20
2.0
1.0
0.0
Run temperature sweep to see ⟨E⟩ / spin
Run temperature sweep to see |⟨M⟩| / spin
Run temperature sweep to see Heat Capacity Cv
Run temperature sweep to see Susceptibility χ
spin +1spin −1

Hamiltonian: H = −J Σ σᵢσⱼ − h Σ σᵢ

Cv = β²(⟨E²⟩ − ⟨E⟩²)N²  ·  χ = β(⟨M²⟩ − ⟨M⟩²)N²

Square lattice: Tc ≈ 2.27 J/k_B (Onsager exact)  ·  Triangular: Tc ≈ 3.64 J/k_B


Overview

A computational physics assignment from JS TP (Junior Sophister Theoretical Physics) at Trinity College Dublin, 2018. The project implemented the Metropolis algorithm to simulate the Ising model across a range of geometries (2D square, 2D triangular, 3D cubic) and investigate the ferromagnetic phase transition.

The Ising model assigns a spin σᵢ ∈ {−1, +1} to each site of a lattice. The energy of a configuration is given by the Hamiltonian:

H = −J Σ_{⟨i,j⟩} σᵢσⱼ − h Σᵢ σᵢ
  • J > 0: ferromagnetic — neighbouring spins prefer to align
  • J < 0: antiferromagnetic — neighbouring spins prefer to anti-align
  • h: external magnetic field

The Metropolis Algorithm

  1. Initialise a random lattice of spins s₀ = {σ₁, ..., σₙ}
  2. For each spin σᵢ, compute the energy change from flipping it:
    ΔE = 2σᵢ ( J Σⱼ σⱼ + h )
  3. Accept the flip if ΔE < 0. Otherwise accept with probability e^{−βΔE}
  4. Repeat until the lattice converges (|⟨M⟩| → 1 for a ferromagnet)
  5. After warm-up, collect averages of E, M, E², M² over many iterations

Observables

All four observables are computed from a single run (no re-running needed):

ObservableFormula
Average energy per spin⟨E⟩ = (1/N²) Σ (−J σᵢ Σⱼ σⱼ − h σᵢ)
Average magnetisation per spin⟨M⟩ = (1/N²) Σ σᵢ
Specific heat capacityCv = β²(⟨E²⟩ − ⟨E⟩²) N²
Magnetic susceptibilityχ = β(⟨M²⟩ − ⟨M⟩²) N²

Key Results

Phase Transition and Curie Temperature

Below the Curie temperature Tc, the lattice is ferromagnetic (all spins aligned, |⟨M⟩| ≈ 1). Above Tc, thermal fluctuations disorder the lattice (paramagnetic). At Tc itself, both Cv and χ show a sharp spike — the signature of a second-order phase transition.

GeometryNeighboursGround state energyTc (measured)Tc (theory)
2D Square4−2J2.7 ± 0.35 J/k_B2.269 J/k_B (Onsager)
2D Triangular6−3J3.7 ± 0.35 J/k_B3.641 J/k_B
3D Cubic6−3J4.1 ± 0.35 J/k_B~4.511 J/k_B

The ground state energies matched theory exactly. The triangular and 3D cubic lattices share 6 neighbours each, giving the same ground state energy but slightly different Curie temperatures.

Boundary Conditions

Replacing periodic boundary conditions with hard walls on a 10×10 lattice reduced Tc from 2.6 to 2.2 J/k_B. Edge spins only have 2–3 neighbours, weakening the cooperative effect. The relative impact scales as (4N−4) / (N−2)² for boundary vs. interior spins.

External Magnetic Field

With h ≠ 0, the average energy shifts to −2|J| − |h|. A larger field raises the effective Curie temperature — more thermal energy is needed to break the combined field + exchange alignment. Magnetic susceptibility decreases with larger h (the lattice is already saturated).

Hysteresis Loop

Sweeping h from positive to negative and back produces a hysteresis loop — the lattice retains its magnetisation longer than naively expected. Larger J widens the loop. Negative J (antiferromagnetic) collapses the loop to near zero unless |h| is large enough to force full alignment.

Coupling Constant J

The relationship between J and Tc is approximately linear — stronger spin-spin interactions require higher temperatures to destroy order. Ground state energies follow −2|J| regardless of the sign of J (antiferromagnets anti-align perfectly, cancelling the sign).

Original Python Code (2018)

import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import gaussian_filter1d

class IsingSolver:
    def __init__(self, N, J=1.0, h=0.0, geometry='square'):
        self.N = N
        self.J = J
        self.h = h
        self.geometry = geometry
        self.grid = np.random.choice([-1, 1], size=(N, N))

    def get_neighbours(self, i, j):
        N = self.N
        neighbours = [
            ((i-1) % N, j), ((i+1) % N, j),
            (i, (j-1) % N), (i, (j+1) % N)
        ]
        if self.geometry == 'triangular':
            if i % 2 == 0:
                neighbours += [((i+1) % N, (j-1) % N), ((i-1) % N, (j-1) % N)]
            else:
                neighbours += [((i+1) % N, (j+1) % N), ((i-1) % N, (j+1) % N)]
        return neighbours

    def metropolis_step(self, T):
        beta = 1.0 / T
        for _ in range(self.N**2):
            i, j = np.random.randint(0, self.N, size=2)
            spin = self.grid[i, j]
            neighbour_sum = sum(self.grid[ni, nj] for ni, nj in self.get_neighbours(i, j))
            dE = 2 * spin * (self.J * neighbour_sum + self.h)
            if dE < 0 or np.random.rand() < np.exp(-beta * dE):
                self.grid[i, j] *= -1

    def avg_magnetisation(self):
        return np.mean(self.grid)

    def run(self, T_range, warmup=100, samples=50):
        results = {'T': [], 'E': [], 'M': [], 'Cv': [], 'chi': []}
        for T in T_range:
            for _ in range(warmup):
                self.metropolis_step(T)
            E_list, M_list = [], []
            for _ in range(samples):
                self.metropolis_step(T)
                E = self._total_energy() / self.N**2
                M = abs(self.avg_magnetisation())
                E_list.append(E); M_list.append(M)
            avgE = np.mean(E_list); avgM = np.mean(M_list)
            Cv = (1/T**2) * (np.mean(np.array(E_list)**2) - avgE**2) * self.N**2
            chi = (1/T) * (np.mean(np.array(M_list)**2) - avgM**2) * self.N**2
            results['T'].append(T); results['E'].append(avgE)
            results['M'].append(avgM); results['Cv'].append(Cv)
            results['chi'].append(chi)
        return results

    def _total_energy(self):
        E = 0
        for i in range(self.N):
            for j in range(self.N):
                spin = self.grid[i, j]
                neighbour_sum = sum(self.grid[ni, nj] for ni, nj in self.get_neighbours(i, j))
                E += -self.J * spin * neighbour_sum / 2 - self.h * spin
        return E

    def find_curie_temperature(self, results):
        M_smooth = gaussian_filter1d(results['M'], sigma=2)
        dM = np.gradient(M_smooth, results['T'])
        return results['T'][np.argmin(dM)]

# Example usage
solver = IsingSolver(N=10, J=1.0)
T_range = np.arange(0.5, 5.0, 0.1)
results = solver.run(T_range)
Tc = solver.find_curie_temperature(results)
print(f"Curie temperature: Tc ≈ {Tc:.2f} J/k_B")

fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0,0].plot(results['T'], results['E'])
axes[0,0].set_title('⟨E⟩ vs T'); axes[0,0].set_xlabel('T'); axes[0,0].set_ylabel('⟨E⟩')
axes[0,1].plot(results['T'], results['M'], color='green')
axes[0,1].axvline(Tc, color='red', linestyle='--', label=f'Tc={Tc:.2f}')
axes[0,1].set_title('|⟨M⟩| vs T'); axes[0,1].legend()
axes[1,0].plot(results['T'], results['Cv'], color='orange')
axes[1,0].set_title('Cv vs T')
axes[1,1].plot(results['T'], results['chi'], color='purple')
axes[1,1].set_title('χ vs T')
plt.tight_layout()
plt.show()

What the Interactive Simulation Shows

Use the controls above to explore the physics:

  • Run “Live Simulation” and watch the lattice spontaneously order at low T (all blue or all red), and disorder at high T (mixed)
  • Run “Temp Sweep” to generate all four observable plots and read off Tc from the |⟨M⟩| inflection point and the Cv/χ spikes
  • Switch to Triangular geometry to see the higher Tc (~3.7 vs ~2.7) from 6 neighbours vs 4
  • Set J < 0 for antiferromagnetic behaviour — the lattice forms a checkerboard pattern
  • Set h ≠ 0 to see the external field bias the magnetisation and shift the effective Tc