Domain: Math — Global Mathematical Physics

Problem:
Consider a 2D Ising model on an \(N\times N\) lattice with periodic boundary conditions, where each spin \(s_{i}\in\lbrace -1, 1\rbrace\). The system’s Hamiltonian is: \(H = -J\displaystyle\sum_{\langle i, j\rangle}s_is_j \) where the sum is over nearest-neighbor pairs and \(J =  1\). At a given temperature \(T\) the Boltzmann probability of a configuration \(S\) is: \(P(S) = \dfrac{1}{Z}e^{-\tfrac{H(S)}{k_BT}}\), where \(k_B = 1\) and \(Z\) is the (unknown) partition function. You'll use the Metropolis-Hastings algorithm to simulate the system at a chosen temperature \(T\) and estimate the mean magnetization per spin: \(\langle m\rangle = \dfrac{1}{N^2}\left\langle \left|\displaystyle \sum_{i,j}s_is_j\right|\right\rangle_T\). Use \(N = 60, T = 2.3\), random seed for initial spin choice \(2025\), using the method `numpy.random.default_rng(seed)`, total number of individual spin-flip attempts in the simulation \(1.2\times 10^6\),  number of initial spin-flip attempts discarded \(2\times 10^5\), interval (in attempted flips) between consecutive magnetization measurements after thermalization \(10\). To ensure the simulation is perfectly reproducible, the first measurement must be taken precisely at step \(2\times 10^5\) (the first step after thermalization), with subsequent measurements occurring every 10 steps thereafter. The possible energy changes  \(\Delta E\) from flipping a single spin \(\lbrace \pm 8, \pm 4, 0\rbrace\). Implement and run a Python code that solves this question, and show the code in one block. Please, be sure to write down the result of your code. The final output should be a float number, with 6 decimal places.

Answer: 0.149378

Solution code:
import numpy as np
def ising_metropolis_avg_magnetization(N=60, T=2.3, J=1.0, seed=42,
                                       total_updates=1_200_000, thermal_updates=200_000,
                                       sample_interval=10):
    
    rng = np.random.default_rng(seed)
    
    # Initialize spins randomly +/-1
    spins = rng.choice([-1, 1], size=(N, N)).astype(np.int8)
    
    # Precompute Boltzmann factors for possible ΔE values: -8, -4, 0, 4, 8
    possible_dE = np.array([-8, -4, 0, 4, 8])
    boltz = {dE: np.exp(-dE / T) for dE in possible_dE}  # note sign: exp(-ΔE/T)
    
    # Helper to compute neighbor sum of a given site (using periodic boundaries)
    def neighbor_sum(i, j):
        return (spins[(i - 1) % N, j] + spins[(i + 1) % N, j] +
                spins[i, (j - 1) % N] + spins[i, (j + 1) % N])
    
    # Running magnetization absolute sum accumulator and sample count
    mag_accum = 0.0
    samples = 0
    
    # Perform Metropolis updates
    for step in range(total_updates):
        # pick a random spin
        i = rng.integers(N)
        j = rng.integers(N)
        
        s = spins[i, j]
        nbr_sum = neighbor_sum(i, j)
        dE = 2 * J * s * nbr_sum  # energy change if we flip s -> -s
        
        # Metropolis acceptance
        if dE <= 0:
            accept = True
        else:
            # compare with precomputed Boltzmann probability
            # fallback to compute if dE not in keys (shouldn't happen here)
            p = boltz.get(dE, np.exp(-dE / T))
            accept = (rng.random() < p)
        
        if accept:
            spins[i, j] = -s
        
        # After thermalization, sample magnetization every `sample_interval` steps
        if step >= thermal_updates and (step - thermal_updates) % sample_interval == 0:
            m = np.abs(spins.sum()) / (N * N)  # absolute magnetization per spin
            mag_accum += m
            samples += 1
    
    if samples == 0:
        return 0.0
    return mag_accum / samples


# Run the simulation
N = 60
T = 2.3
total_updates = 1_200_000  
thermal_updates = 200_000
sample_interval = 10
seed = 2025

m_avg = ising_metropolis_avg_magnetization(N=N, T=T, J=1.0, seed=seed,
                                           total_updates=total_updates,
                                           thermal_updates=thermal_updates,
                                           sample_interval=sample_interval)

print(f"{m_avg:.6f}")