2
\$\begingroup\$

I've implemented the 2D ISING model in Python, using NumPy and Numba's JIT:


from timeit import default_timer as timer
import matplotlib.pyplot as plt
import numba as nb
import numpy as np
# TODO for Dict optimization.
# from numba import types
# from numba.typed import Dict
@nb.njit(nogil=True)
def initialstate(N): 
 ''' 
 Generates a random spin configuration for initial condition
 '''
 state = np.empty((N,N),dtype=np.int8)
 for i in range(N):
 for j in range(N):
 state[i,j] = 2*np.random.randint(2)-1
 return state
@nb.njit(nogil=True)
def mcmove(lattice, beta, N):
 '''
 Monte Carlo move using Metropolis algorithm 
 '''
 
 # # TODO* Dict optimization 
 # dict_param = Dict.empty(
 # key_type=types.int64,
 # value_type=types.float64,
 # )
 # dict_param = {cost : np.exp(-cost*beta) for cost in [-8, -4, 0, 4, 8] }
 for _ in range(N):
 for __ in range(N):
 a = np.random.randint(0, N)
 b = np.random.randint(0, N)
 s = lattice[a, b]
 dE = lattice[(a+1)%N,b] + lattice[a,(b+1)%N] + lattice[(a-1)%N,b] + lattice[a,(b-1)%N]
 cost = 2*s*dE
 if cost < 0:
 s *= -1
 
 #TODO* elif np.random.rand() < dict_param[cost]:
 elif np.random.rand() < np.exp(-cost*beta):
 s *= -1
 lattice[a, b] = s
 return lattice
@nb.njit(nogil=True)
def calcEnergy(lattice, N):
 '''
 Energy of a given configuration
 '''
 energy = 0 
 
 for i in range(len(lattice)):
 for j in range(len(lattice)):
 S = lattice[i,j]
 nb = lattice[(i+1)%N, j] + lattice[i,(j+1)%N] + lattice[(i-1)%N, j] + lattice[i,(j-1)%N]
 energy += -nb*S
 return energy/2
@nb.njit(nogil=True)
def calcMag(lattice):
 '''
 Magnetization of a given configuration
 '''
 mag = np.sum(lattice, dtype=np.int32)
 return mag
@nb.njit(nogil=True)
def ISING_model(nT, N, burnin, mcSteps):
 """ 
 nT : Number of temperature points.
 N : Size of the lattice, N x N.
 burnin : Number of MC sweeps for equilibration (Burn-in).
 mcSteps : Number of MC sweeps for calculation.
 """
 T = np.linspace(1.2, 3.8, nT); 
 E,M,C,X = np.zeros(nT), np.zeros(nT), np.zeros(nT), np.zeros(nT)
 n1, n2 = 1.0/(mcSteps*N*N), 1.0/(mcSteps*mcSteps*N*N) 
 for temperature in range(nT):
 lattice = initialstate(N) # initialise
 E1 = M1 = E2 = M2 = 0
 iT = 1/T[temperature]
 iT2= iT*iT
 
 for _ in range(burnin): # equilibrate
 mcmove(lattice, iT, N) # Monte Carlo moves
 for _ in range(mcSteps):
 mcmove(lattice, iT, N) 
 Ene = calcEnergy(lattice, N) # calculate the Energy
 Mag = calcMag(lattice,) # calculate the Magnetisation
 E1 += Ene
 M1 += Mag
 M2 += Mag*Mag 
 E2 += Ene*Ene
 E[temperature] = n1*E1
 M[temperature] = n1*M1
 C[temperature] = (n1*E2 - n2*E1*E1)*iT2
 X[temperature] = (n1*M2 - n2*M1*M1)*iT
 return T,E,M,C,X
def main():
 
 N = 32
 start_time = timer()
 T,E,M,C,X = ISING_model(nT = 64, N = N, burnin = 8 * 10**4, mcSteps = 8 * 10**4)
 end_time = timer()
 print("Elapsed time: %g seconds" % (end_time - start_time))
 f = plt.figure(figsize=(18, 10)); # 
 # figure title
 f.suptitle(f"Ising Model: 2D Lattice\nSize: {N}x{N}", fontsize=20)
 _ = f.add_subplot(2, 2, 1 )
 plt.plot(T, E, '-o', color='Blue') 
 plt.xlabel("Temperature (T)", fontsize=20)
 plt.ylabel("Energy ", fontsize=20)
 plt.axis('tight')
 _ = f.add_subplot(2, 2, 2 )
 plt.plot(T, abs(M), '-o', color='Red')
 plt.xlabel("Temperature (T)", fontsize=20)
 plt.ylabel("Magnetization ", fontsize=20)
 plt.axis('tight')
 _ = f.add_subplot(2, 2, 3 )
 plt.plot(T, C, '-o', color='Green')
 plt.xlabel("Temperature (T)", fontsize=20)
 plt.ylabel("Specific Heat ", fontsize=20)
 plt.axis('tight')
 _ = f.add_subplot(2, 2, 4 )
 plt.plot(T, X, '-o', color='Black')
 plt.xlabel("Temperature (T)", fontsize=20)
 plt.ylabel("Susceptibility", fontsize=20)
 plt.axis('tight')
 plt.show()
if __name__ == '__main__':
 main()

Which of course, works:

enter image description here

I have two main questions:

  1. Is there anything left to optimize? I knew ISING model is hard to simulate, but looking at the following table, it seems like I'm missing something...
 lattice size : 32x32 
 burnin = 8 * 10**4
 mcSteps = 8 * 10**4
 Simulation time = 365.98 seconds
 lattice size : 64x64 
 burnin = 10**5
 mcSteps = 10**5
 Simulation time = 1869.58 seconds
  1. I tried implementing another optimization based on not calculating the exponential over and over again using a dictionary, yet on my tests, it seems like its slower. What am I doing wrong?
asked Dec 2, 2022 at 18:51
\$\endgroup\$
2
  • \$\begingroup\$ There's no saving this. You need to throw away the works and replace it with something better like C/C++. Numpy will not ever perform well on iterative algorithms no matter how much JIT you attempt. \$\endgroup\$ Commented Dec 4, 2022 at 1:04
  • \$\begingroup\$ using np.random in jitted numba may cause it inefficient. \$\endgroup\$ Commented Jan 1, 2023 at 8:41

1 Answer 1

2
\$\begingroup\$

Rename functions like initialstate to initial_state by PEP8.

Add PEP484 type hints to your function signatures.

Consider adding a stateful RNG (default_rng) instance so that you can choose initial conditions and have them repeatable. This is important for scientific work.

Replace this loop:

state = np.empty((N,N),dtype=np.int8)
for i in range(N):
 for j in range(N):
 state[i,j] = 2*np.random.randint(2)-1
return state

with a vectorised version:

return np.random.randint(2, size=(N, N), dtype=np.int8)*2 - 1

Replace the nested loop having a __ with a single loop over N*N elements.

Reformat your dE assignment to be over multiple lines for legibility.

Replace your s *= -1 multiplication with the assignment lattice[a, b] = -s.

Combine the cost comparison ifs into one using an or, since their effect is the same.

Vectorise calcEnergy to a sum of a product of lattice with four rolled versions of itself.

n2 is just n1/mc_steps.

Your _ = is non-effectual so delete it.

mc_move mutates lattice, so there's no point in returning it.

Suggested

All of this is window dressing and won't in any significant way improve your performance; for that you need to move to a better language.

from timeit import default_timer as timer
import matplotlib.pyplot as plt
import numpy as np
def initial_state(N: int) -> np.ndarray:
 """
 Generates a random spin configuration for initial condition
 """
 return np.random.randint(2, size=(N, N), dtype=np.int8)*2 - 1
def mc_move(lattice: np.ndarray, beta: float, N: int) -> None:
 """
 Monte Carlo move using Metropolis algorithm
 """
 for a, b in np.random.randint(0, N, size=(N*N, 2)):
 s = lattice[a, b]
 dE = (
 lattice[(a + 1) % N, b] +
 lattice[(a - 1) % N, b] +
 lattice[a, (b + 1) % N] +
 lattice[a, (b - 1) % N]
 )
 cost = 2*s*dE
 if cost < 0 or np.random.rand() < np.exp(-cost * beta):
 lattice[a, b] = -s
def calc_energy(lattice: np.ndarray) -> int:
 """
 Energy of a given configuration
 """
 return (
 -lattice * (
 np.roll(lattice, axis=0, shift=+1) +
 np.roll(lattice, axis=0, shift=-1) +
 np.roll(lattice, axis=1, shift=+1) +
 np.roll(lattice, axis=1, shift=-1)
 )
 ).sum() / 2
def calc_mag(lattice: np.ndarray) -> int:
 """
 Magnetization of a given configuration
 """
 return np.sum(lattice, dtype=np.int32)
def ising_model(nT: int, N: int, burnin: int, mc_steps: int) -> tuple[
 np.ndarray, # T
 np.ndarray, # E
 np.ndarray, # M
 np.ndarray, # C
 np.ndarray, # X
]:
 """
 nT : Number of temperature points.
 N : Size of the lattice, N x N.
 burnin : Number of MC sweeps for equilibration (Burn-in).
 mcSteps : Number of MC sweeps for calculation.
 """
 T = np.linspace(1.2, 3.8, nT)
 E, M, C, X = np.zeros((4, nT))
 n1 = 1/mc_steps/N/N
 n2 = n1/mc_steps
 for temperature in range(nT):
 lattice = initial_state(N) # initialise
 E1 = M1 = E2 = M2 = 0
 iT = 1 / T[temperature]
 iT2 = iT * iT
 for _ in range(burnin): # equilibrate
 mc_move(lattice, iT, N) # Monte Carlo moves
 for _ in range(mc_steps):
 mc_move(lattice, iT, N)
 Ene = calc_energy(lattice) # calculate the Energy
 Mag = calc_mag(lattice) # calculate the Magnetisation
 E1 += Ene
 M1 += Mag
 E2 += Ene * Ene
 M2 += Mag * Mag
 E[temperature] = n1*E1
 M[temperature] = n1*M1
 C[temperature] = (n1*E2 - n2*E1*E1)*iT2
 X[temperature] = (n1*M2 - n2*M1*M1)*iT
 return T, E, M, C, X
def main() -> None:
 N = 32
 start_time = timer()
 T, E, M, C, X = ising_model(nT=8, N=N, burnin=8, mc_steps=32)
 end_time = timer()
 print(f'Elapsed time: {end_time - start_time:g} seconds')
 f = plt.figure()
 # figure title
 f.suptitle(f'Ising Model: 2D Lattice\nSize: {N}x{N}')
 f.add_subplot(2, 2, 1)
 plt.plot(T, E, '-o', color='Blue')
 plt.xlabel('Temperature (T)')
 plt.ylabel('Energy ')
 plt.axis('tight')
 f.add_subplot(2, 2, 2)
 plt.plot(T, abs(M), '-o', color='Red')
 plt.xlabel('Temperature (T)')
 plt.ylabel('Magnetization ')
 plt.axis('tight')
 f.add_subplot(2, 2, 3)
 plt.plot(T, C, '-o', color='Green')
 plt.xlabel('Temperature (T)')
 plt.ylabel('Specific Heat ')
 plt.axis('tight')
 f.add_subplot(2, 2, 4)
 plt.plot(T, X, '-o', color='Black')
 plt.xlabel('Temperature (T)')
 plt.ylabel('Susceptibility')
 plt.axis('tight')
 plt.show()
if __name__ == '__main__':
 main()
answered Dec 4, 2022 at 1:46
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.