In [8]:
%matplotlib inline
import numpy as np
from matplotlib import pyplot as plt
plt.style.use('bmh')
from numba import jit

In [23]:
# The functions below can be used to initialise a 2D
# grid of spins, and then let it evolve towards a more
# likely configuration. See further details in
# http://web.phys.ntnu.no/~ingves/Teaching/TFY4235/Download/TFY4235_Slides_2015.pdf
# under Metropolis Monte-Carlo algorithm.

def initialise(N):
    # This function initialises a grid of size N x N
    # where each number is either 1 or -1
    # (corresponding to spin up or down)
    spins = np.ones((N, N), dtype = np.int32)
    for i in range(N):
        for j in range(N):
            r = np.random.random()
            if r < 0.5:
                spins[i,j] = -1
    return spins


@jit(nopython = True)
def dH(i, j, spins):
    # This function calculates the change in energy
    # from flipping a spin
    Nx = spins.shape[0]
    Ny = spins.shape[1]
    J = 1
    s = -spins[i,j]
    return -J * s * (
        spins[i-1, j] + spins[(i+1) % Nx, j] +
        spins[i, j-1] + spins[i, (j+1) % Ny] )


# This function receives a considerable speedup
# from just-in-time compilation with numba, as
# it contains a for-loop. The effect of jit-compiling
# the function dH above isn't as large, but it is
# required as one cannot call a non-compiled function
# from within a compiled function.
@jit(nopython = True)
def sweep(spins, T):
    # This function carries out a "sweep" over the grid,
    # in the sense that it tries to flip each spin once
    # on average, rejecting the flip with a certain probability
    # which depends on temperature and change in energy
    for i in range(spins.size):
        i = np.random.randint(0, spins.shape[0])
        j = np.random.randint(0, spins.shape[1])
        r = np.random.random()
        w = np.exp(-dH(i,j,spins)/T)
        if w > r:
            spins[i,j] = -spins[i,j]
    return spins


def average(spins):
    return np.sum(spins) / spins.size


def run(Nspins, Nsweeps, T):
    # This function carries out one simulation:
    # Initialise a random grid
    # Carry out a number of sweeps, until the system
    # can be assumed to have reached equilibrium
    # Then return the average spin
    # (which is proportional to the bulk magnetisation)
    spins = initialise(Nspins)
    for i in range(Nsweeps):
        spins = sweep(spins, T)
    return average(spins)

In [24]:
# Set up grid size, then run a bunch of simulations
# for increasing temperature
Nspins  = 40
Nsweeps = 500
Temps   = np.linspace(0.1, 2, 250)
Moments = []

iout = 0
for i,T in enumerate(Temps):
    # Primitive progress bar
    if i > iout:
        print('=', end = '')
        iout += len(Temps) / 50
    Moments.append(run(Nspins, Nsweeps, T))


==================================================

In [22]:
# Plot of average spin agains temperature
# reveals a pitchfork bifurcation.
fig = plt.figure(figsize = (9,5))
plt.scatter(Temps, Moments, c = 'k', s = 5)
plt.xlabel('Temperature')
plt.ylabel('Average spin')
plt.tight_layout()



In [ ]: