In this guide, we generate finite-size scaling dataset for the two-dimensional ferromagnetic Ising model on a square lattice from scratch using vectorized Metropolis Monte Carlo and Jackknife error estimation.
By the end of this tutorial, you will have a clear understanding of:
The 2D Ising Hamiltonian and the Binder parameter .
Vectorized checkerboard (sublattice) Metropolis sampling in NumPy.
Jackknife resampling for evaluating statistical errors of non-linear ratios.
How to format and save the simulation dataset for analysis with
jaxfss.
1. Theoretical Background¶
The standard 2D ferromagnetic Ising model on an square lattice with periodic boundary conditions is described by the Hamiltonian:
where denotes nearest-neighbor pairs, and we set .
Exact Critical Temperature¶
Onsager’s exact solution provides the critical temperature (or inverse critical temperature ):
The Binder Parameter (Binder Cumulant)¶
The fourth-order Binder parameter is defined as:
where is the total magnetization. In the thermodynamic limit:
(ordered phase):
(disordered phase):
At : is scale-invariant (all finite-size curves cross at ).
The finite-size scaling relation for has anomalous dimension :
with exact 2D exponent .
import numpy as np
import matplotlib.pyplot as plt
import time
import jaxfss2. Vectorized Checkerboard Metropolis Monte Carlo¶
In a square lattice, sites can be partitioned into two independent sublattices (like black and white squares of a chessboard: and ).
Because nearest neighbors of any “even” site are strictly “odd” sites, all even sites can be updated simultaneously in parallel without race conditions, followed by updating all odd sites.
def simulate_ising_chain(
L: int,
inv_T: float,
n_thermalization: int = 500,
n_measurements: int = 2000,
sample_interval: int = 5,
seed: int = 0,
) -> np.ndarray:
"""Sample magnetization M = sum(sigma) using vectorized checkerboard Metropolis."""
rng = np.random.default_rng(seed)
spins = rng.choice([-1, 1], size=(L, L)).astype(np.int8)
# Sublattice masks
i, j = np.meshgrid(np.arange(L), np.arange(L), indexing="ij")
mask_even = (i + j) % 2 == 0
mask_odd = ~mask_even
def sweep():
for mask in (mask_even, mask_odd):
# Sum of 4 nearest neighbors with periodic boundary conditions
nn_sum = (
np.roll(spins, 1, axis=0)
+ np.roll(spins, -1, axis=0)
+ np.roll(spins, 1, axis=1)
+ np.roll(spins, -1, axis=1)
)
dE = 2 * spins * nn_sum
# Acceptance probability for dE > 0
prob = np.exp(-inv_T * dE)
accept = (dE <= 0) | (rng.random((L, L)) < prob)
spins[mask & accept] *= -1
# Thermalization sweeps
for _ in range(n_thermalization):
sweep()
# Measurement sweeps
magnetizations = []
for step in range(n_measurements * sample_interval):
sweep()
if step % sample_interval == 0:
magnetizations.append(float(np.sum(spins)))
return np.array(magnetizations)3. Jackknife Error Estimation for Binder Parameter¶
The Binder cumulant is a ratio of non-linear averages. Standard propagation of error is biased, so we use the Jackknife resampling method.
def compute_binder_jackknife(magnetizations: np.ndarray, n_bins: int = 20) -> tuple[float, float]:
"""Compute Binder parameter and statistical error using Jackknife resampling with binning."""
# Group into n_bins blocks to eliminate autocorrelation
n_total = len(magnetizations)
bin_size = n_total // n_bins
m_binned = magnetizations[: n_bins * bin_size].reshape(n_bins, bin_size)
# Bin averages of M^2 and M^4
m2_bins = np.mean(m_binned**2, axis=1)
m4_bins = np.mean(m_binned**4, axis=1)
# Overall averages
m2_mean = np.mean(m2_bins)
m4_mean = np.mean(m4_bins)
u_full = 1.0 - m4_mean / (3.0 * (m2_mean**2))
# Jackknife pseudo-values (leaving out one bin at a time)
u_jack = []
for b in range(n_bins):
m2_sub = (np.sum(m2_bins) - m2_bins[b]) / (n_bins - 1)
m4_sub = (np.sum(m4_bins) - m4_bins[b]) / (n_bins - 1)
u_jack.append(1.0 - m4_sub / (3.0 * (m2_sub**2)))
u_jack = np.array(u_jack)
# Jackknife standard error formula
u_err = np.sqrt((n_bins - 1) * np.mean((u_jack - np.mean(u_jack)) ** 2))
return float(u_full), float(u_err)4. Multi-Size Simulation and Binder Crossing¶
We simulate three lattice sizes over a grid of inverse temperatures around .
L_list = [16, 32, 64]
inv_T_values = np.linspace(0.42, 0.46, 17)
results = {}
print("Running 2D Ising simulation...")
t_start = time.time()
for L in L_list:
u_vals, u_errs = [], []
for k_idx, inv_T in enumerate(inv_T_values):
m_chain = simulate_ising_chain(
L=L,
inv_T=inv_T,
n_thermalization=400,
n_measurements=1500,
sample_interval=3,
seed=100 * L + k_idx,
)
u, u_err = compute_binder_jackknife(m_chain, n_bins=15)
u_vals.append(u)
u_errs.append(u_err)
results[L] = (np.array(u_vals), np.array(u_errs))
print(f" L = {L:2d} completed in {time.time() - t_start:.2f}s")
print(f"Total simulation completed in {time.time() - t_start:.2f}s.")Running 2D Ising simulation...
L = 16 completed in 4.07s
L = 32 completed in 9.95s
L = 64 completed in 21.97s
Total simulation completed in 21.97s.
Plot the Binder ratio curves. All curves cross at the critical coupling .
plt.figure(figsize=(7.5, 4.8))
markers = ["o", "s", "^"]
K_c_exact = 0.5 * np.log(1 + np.sqrt(2))
for (L, (u_vals, u_errs)), m in zip(results.items(), markers):
plt.errorbar(inv_T_values, u_vals, yerr=u_errs, fmt=f"{m}-", label=f"$L = {L}$", capsize=3, lw=1.5)
plt.axvline(K_c_exact, color="black", linestyle=":", label=f"Exact $K_{{\mathrm{{c}}}} = {K_c_exact:.5f}$")
plt.xlabel(r"Inverse Temperature $K = J / k_{\mathrm{B}} T$", fontsize=12)
plt.ylabel(r"Binder Cumulant $U(K, L)$", fontsize=12)
plt.title("2D Ising Model: Binder Cumulant Crossing", fontsize=14)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()<>:8: SyntaxWarning: invalid escape sequence '\m'
<>:8: SyntaxWarning: invalid escape sequence '\m'
/var/folders/g6/3yjn6zvx14s3q4k28w8d78fw0000gn/T/ipykernel_2412/1662117723.py:8: SyntaxWarning: invalid escape sequence '\m'
plt.axvline(K_c_exact, color="black", linestyle=":", label=f"Exact $K_{{\mathrm{{c}}}} = {K_c_exact:.5f}$")

5. Exporting Dataset for jaxfss¶
We save our simulated Binder cumulant data to ising_custom.txt in the standard tabular format:
L K U U_erroutput_rows = []
for L, (u_vals, u_errs) in results.items():
for k_val, u, err in zip(inv_T_values, u_vals, u_errs):
output_rows.append([L, k_val, u, err])
output_data = np.array(output_rows)
output_path = "ising_custom.txt"
np.savetxt(
output_path,
output_data,
fmt=["%d", "%.8f", "%.8f", "%.8f"],
header="L K U U_err"
)
print(f"Saved {len(output_data)} rows to {output_path}.")
# Verify loading with jaxfss
loaded_dataset = jaxfss.CriticalData.from_file(output_path)
print(f"Successfully loaded into jaxfss: {loaded_dataset}")
print(f"System sizes in dataset: {np.unique(loaded_dataset.Ls)}")Saved 51 rows to ising_custom.txt.
Successfully loaded into jaxfss: jaxfss.CriticalData(n_data=51)
System sizes in dataset: [16. 32. 64.]
Summary¶
We have implemented a fast, vectorized 2D Ising Metropolis Monte Carlo simulation, computed Binder cumulants with Jackknife error estimates, verified the universal curve crossing at , and exported the data ready for jaxfss Finite-Size Scaling analysis.