A Python implementation of the two-dimensional ferromagnetic Ising model, simulated with the Metropolis Monte Carlo algorithm and accelerated with JAX (JIT-compiled via XLA). The code reproduces the paramagnetic-to-ferromagnetic phase transition and lets you measure thermodynamic observables (energy, magnetisation, specific heat, susceptibility) as functions of temperature.
The Ising model is one of the most important exactly solvable models in statistical mechanics. Despite its simplicity — binary spins on a lattice interacting only with their nearest neighbours — it captures the essential physics of second-order phase transitions, spontaneous symmetry breaking, and universality.
This project provides:
ising_model.py— a self-contained Python module using JAX (jax.numpyarrays and@jax.jit-compiled update kernels) with a single functionrun_ising_model(...)that runs the simulation and returns results as apandas.DataFrame.Ising_simulation.ipynb— a Jupyter notebook that runs the simulation and produces publication-quality plots of all thermodynamic observables, including a finite-size scaling comparison.
Consider a 2D square lattice of
where:
- The sum runs over all nearest-neighbour pairs
$\langle i,j \rangle$ . -
$J > 0$ is the exchange coupling constant (ferromagnetic: aligned spins lower the energy). - Periodic boundary conditions are applied in both directions, so every spin has exactly four neighbours and there are no edge effects.
We work in units where
Ground state (
High-temperature limit (
In thermal equilibrium at temperature
| Observable | Formula | Physical meaning |
|---|---|---|
| Mean energy per spin | Average energy density | |
| Magnetisation per spin | Strength of ferromagnetic order (order parameter) | |
| Specific heat per spin | Energy fluctuations; diverges at |
|
| Susceptibility per spin | Response to external field; diverges at |
We use
The fluctuation-dissipation relations connect
The 2D Ising model undergoes a continuous (second-order) phase transition at the Onsager critical temperature, derived analytically by Lars Onsager in 1944:
This is one of the very few exact results in statistical mechanics for an interacting many-body system.
Below
-
$\langle |M| \rangle / N \to 1$ as$T \to 0$ - Long-range correlations between spins
Above
$\langle |M| \rangle / N \to 0$ - Exponential decay of spin-spin correlations
At
- Both
$C_v$ and$\chi$ diverge (logarithmically and as a power law, respectively) - Power-law decay of correlations — the system is scale-invariant
- Critical exponents:
$\alpha = 0$ (log divergence of$C_v$ ),$\beta = 1/8$ ,$\gamma = 7/4$ ,$\nu = 1$
A simulation can only access a finite lattice of size
-
Rounded transition: the divergences of
$C_v$ and$\chi$ are replaced by finite peaks. -
Shifted pseudo-critical temperature: the peak of
$\chi$ occurs at a pseudo-critical temperature$T_c(L)$ that shifts toward$T_c$ as:$$T_c(L) \approx T_c + a, L^{-1/\nu} = T_c + a, L^{-1}$$ -
Finite peak heights: the peak heights scale with
$L$ as:$$C_v^{\max} \sim \ln L, \quad \chi^{\max} \sim L^{\gamma/\nu} = L^{7/4}$$
Studying how these peaks evolve with
The Metropolis algorithm generates a Markov chain of spin configurations that samples the Boltzmann distribution
- Select a spin
$s_i$ at random. - Compute the local field energy:
$$\Delta E_{\text{flip}} = 2, s_i \sum_{j \in \text{nn}(i)} s_j$$ (the energy change if$s_i$ is flipped, derived from$H = -J \sum s_i s_j$ ). - Accept the flip with probability:
$$P_{\text{accept}} = \min!\left(1,; e^{-\Delta E / T}\right)$$ - If
$\Delta E \leq 0$ (energy decreases): always accept. - If
$\Delta E > 0$ (energy increases): accept with probability$e^{-\Delta E/T}$ , which is large at high$T$ and small at low$T$ .
- If
- Update
$E$ and$M$ accordingly.
One Monte Carlo sweep (MCS) consists of
Rather than selecting spins one at a time (which would require a Python loop over N spins per sweep), we use checkerboard (sublattice) updates combined with JAX JIT compilation for maximum throughput:
The lattice is divided into two sublattices — like the black and white squares of a chessboard:
-
Sublattice A: sites
$(i,j)$ with$(i+j)$ even -
Sublattice B: sites
$(i,j)$ with$(i+j)$ odd
Crucially, all nearest neighbours of an A-site are B-sites and vice versa. This means all sites within one sublattice can be updated simultaneously: their acceptance decisions are independent because they do not interact with each other.
This allows all sublattice updates to be expressed as fully vectorised jax.numpy operations and compiled once to XLA bytecode via @jax.jit, making each sweep orders of magnitude faster than a Python spin-by-spin loop while producing statistically equivalent results.
One sweep = one simultaneous update of sublattice A + one simultaneous update of sublattice B.
The checkerboard update kernel is decorated with @jax.jit, which triggers
XLA compilation on the first call. Subsequent calls skip Python overhead
entirely and execute compiled machine code, typically yielding a further
5–20× speedup over an equivalent NumPy implementation for large lattices.
JAX also uses an explicit, functional PRNG system. Rather than a global random state, each stochastic step receives an explicit key:
key, subkey = jax.random.split(key)
r = jax.random.uniform(subkey, shape=sublattice.shape)This makes the simulation fully reproducible and compatible with JAX's
functional transformation model (jax.vmap, jax.grad for future extensions).
Rather than re-initialising the lattice at each temperature, we carry the spin configuration over from one temperature step to the next. Starting from a fully ordered state at
Ising_model/
├── ising_model.py # Python module: run_ising_model() function
├── Ising_simulation.ipynb # Jupyter notebook: runs simulation and plots results
├── pyproject.toml # Project dependencies (uv / pip)
└── README.md # This file
- Python 3.12+
jax(>=0.10.0, withjaxlib— installs automatically viauv sync)numpypandasmatplotlibjupyter/notebook
# Clone or enter the project directory
cd Ising_model
# Install all dependencies into a virtual environment
uv sync
# Activate the environment
source .venv/bin/activatefrom ising_model import run_ising_model
# Run with default parameters (L=36, MCS=1000)
df = run_ising_model(L=36, MCS=1000, T_start=4.0, T_end=0.5, dT=0.05, seed=42)
# df is a pandas DataFrame with columns: T, M, E, Cv, chi
print(df.head())Function signature:
run_ising_model(
L = 36, # Lattice size (L x L spins)
MCS = 1000, # Monte Carlo sweeps per temperature
T_start = 4.0, # Starting temperature
T_end = 0.5, # Ending temperature
dT = 0.05, # Temperature step
seed = None, # Random seed (None = non-deterministic)
) -> pd.DataFrameIncreasing MCS gives smoother, more accurate curves at the cost of longer runtime. Values of 2000–5000 are recommended for publication-quality results.
# Launch Jupyter
jupyter notebook Ising_simulation.ipynb
# or
jupyter lab Ising_simulation.ipynbRun all cells from top to bottom. The notebook will:
- Run a single simulation for
$L = 36$ , MCS = 1000. - Plot magnetisation, energy, specific heat, and susceptibility vs temperature.
- Run simulations for
$L \in \{4, 12, 36, 100\}$ and produce a finite-size scaling comparison. - Print a table of pseudo-critical temperatures and peak heights.
Saved plots (observables_L36.png, finite_size_scaling.png) will appear in the project directory.
- Onsager, L. (1944). "Crystal Statistics. I. A Two-Dimensional Model with an Order-Disorder Transition." Physical Review, 65(3–4), 117–149.
- Metropolis, N., Rosenbluth, A. W., Rosenbluth, M. N., Teller, A. H., & Teller, E. (1953). "Equation of State Calculations by Fast Computing Machines." Journal of Chemical Physics, 21(6), 1087.
- Newman, M. E. J., & Barkema, G. T. (1999). Monte Carlo Methods in Statistical Physics. Oxford University Press.
- Landau, D. P., & Binder, K. (2014). A Guide to Monte Carlo Simulations in Statistical Physics (4th ed.). Cambridge University Press.