Simulating an Ising Model
Contents
Simulating an Ising Model#
In this page, you will write Monte Carlo code to sample configurations from the Ising model with the correct probability.
Minimal background#
The Ising model#
The Ising model is one of the drosophila of physics. At first look, it’s a simple model of crystalline materials which attributes magnetism to the orientation of magnetic moments on a lattice, summarized by the Hamiltonian or energy
The variables
Ising’s original formulation (often referred to as the Ising model) considers moments with two orientations,
A configuration of an Ising model is a specification of every moment’s orientation — an assignment of values to the
Statistical Mechanics#
There are a couple facts we need to know about statistical mechanics.
(1) We can specify configurations of our system
(2) Each configuration
(3) For classical systems in contact with an energy reservoir of temperature
where
Markov Chains#
We have just learned that statistical mechanics tells us that if we look at the system, each configuration
One such algorithm is markov chain Monte Carlo.
A Markov chain is a process where you are at some state
As long as you do a (non-pathalogical) random walk in a memoryless way, you’re guaranteed that, after walking around enough, that the probability you are in configuration
It is aperiodic; it doesn’t cycle between configurations in a subset of the system’s full configuration space.
It is connected; given two configurations, there is a path with non-zero probability that the chain could (but not necessarily will) follow to get from one to the other.
To simulate the Ising model, we wish to build a markov chain which has the stationary distribution
The Metropolis-Hastings algorithm#
If you know your desired stationary distribution, the Metropolis-Hastings algorithm provides a canonical way to generate a Markov chain that produces it. The fact that you can do this is amazing! The steps of the algorithm are:
Start with some configuration
Propose a move to a new trial configuration
with a transition probabilityAccept the move to
with probability
The first step is straightforward, but the latter two deserve some explanation.
How
Question to think about: How do equiprobable forward and reverse moves simplify the Metropolis algorithm?
Acceptance condition: To construct a Markov chain which asymptotes to the desired distribution
Notice how the first factor in
Question to think about: Why are moves with
accepted with probability , rather than outright rejected?
States and parameters#
For our simulation, we are going to work on a two-dimensional lattice, have no magnetic field (
where
For this project we will be working on a
We are going to represent the state of our spins by a np.random.choice([1,-1], (L,L))
in python. In C++, there are two approaches to storing the two dimensional array. One approach is to use the STL: vector<vector<int> > spins
. Another option is to use Eigen (see here) where you will use MatrixXi
.
This approach will work for square grids (and is what you want to do for this project!). In some cases, you want a more complicated lattice (triangular, pyrochlore) and have different parameters for
Storing your configuration as a graph.
A more general way to store your configuration is to put your spins (and magnetic field) on nodes of a graph and the coupling constants
A graph has a bunch of nodes (each node
Now there are two questions: how do you represent this inside your code and how do you get the code to know about the graph that you want.
Representing your spins: Here is what I did in my code. I wrote an Ising class which has two vectors which represent the state of the nodes and their magnetic fields (i.e. vector<int> spins
and vector<double> h
). Then, I stored a neighbor vector. The i’th element in the neighbor vector stores a list (or vector) of neighbors with their respective coupling constants.
In other words, for the graph above, you want to be storing the following inside your computer:
Node: (neighbor, J) (neighbor, J) (neighbor, J) ...
1: (2, 1) (3,-0.5)
2: (1,1) (4,-1.1)
3: (1,-0.5) (4,1.1)
4: (2,-1.1) (3,1.1) (5,0.6)
5: (4,0.6)
I stored this as vector<vector<pair<int, double> > > neighbors
where the pair<int,double>
stores the node you are a neighbor to and the weight.
Setting up your graph: Setting up your Ising model this way gives you a lot of flexibility. But now you need some way to tell your code what particular graph you want (maybe the one above or a grid, etc).
I recommend doing this by reading two files. A “spins.txt” which tells you the value of
For the first graph above: bonds.txt
1 2 1.0
1 3 -0.5
3 4 1.1
2 4 -1.1
4 5 0.6
spin.txt
1 0.2
2 -1.1
3 0.2
4 1.2
5 0.1
Then just write some sort of Init() function for your Ising model to read in the graph. Also, go ahead and write yourself a python code that generates the relevant graph for a two-dimensional Ising model on a square grid. Now, any time you want to run a different Ising model, you just have to change these files.
Computing the Energy#
The next step in putting together your Ising model is learning how to compute the energy def Energy(spins)
) which allows you to do this.
In addition to computing the energy for a single configuration, it’s going to be critical to be able to compute the energy difference between two configurations that differ by a single spin flip (i.e. def deltaE(spin_to_flip)
)
Why is this? Because the acceptance criterion we are going to use is the ratio of
Go ahead and make this function. Make sure that the function takes time Energy(spins)
twice).
Comment about computational complexity: One of the themes of this class is to think about the scaling of your algorithms (like we did thinking about various options for quantum computing). Let’s do this for computing an energy difference. One thing you can do is compute the energy, flip the spin, compute the energy, and subtract. How long does this take? It takes
At this point you should be able to store a configuration of spins and compute energy and energy differences on them.
Testing
Set up a
grid and compute the energy of some spin configuration both by hand and with your code to make sure it’s the sameCompute the energy difference of flipping a single spin both by
using your
Energy()
function before and after a spin flip.using your
deltaE()
function.
Both approaches for computing the energy should be the same. Another reasonable test is to compute the energy of a number of configurations that you can evaluate by hand (all spin-up, etc) and verify that it works.
Writing MCMC#
We now want to put together the Monte Carlo. The basic operation, which we call a step is to do the following (many times):
Choose a new configuration
with some probability given the current configuration . The simplest method is to choose a random spin and flip it. In this case both (where is the number of spins). Therefore the ratio of cancel in the acceptance probability ).Evaluate the difference in energy
If
then accept the new configuration . Otherwise leave the old configuration
We will call a sweep doing
You will typically need to do a certain number (maybe 20) sweeps before you should start collecting data. This is called the equilibration time and is required because you need the Markov Chain to forget about your initial condition.
Then you can take a measurement every sweep. There is no need to measure more often then this (and doing so will just slow down the simulation).
This is going to look something like this (in c++)
for (int sweep=0;sweep<10000;sweep++){
for (int i=0;i<N;i++){
//Pick a random spin and try to flip it
}
//measure your histogram or energy, etc.
}
or (in python)
for sweep in range(0,10000):
for step in range(0,N):
#flip spins
# measure
Grading
For your
Then, compare this histogram against the theoretical values - i.e you can compute the energy of all
You should have both graphs on top of each other on the same plot (maybe one with points and one with lines) to see that they agree. There is going to be some error bar on your Monte Carlo data so they don’t have to match exactly.
Note, that this is a great way to test your code. It’s only going to work with a small number (i.e. 9) of spins. If you tried to do this with 25 spins, you would need much too large a histogram.
How often should you take snapshots (or more generically compute observables)
The promise of MCMC is that after a “long enough time”
Q: What happens if you take snapshots too infrequently? You end up just wasting some time. The samples are still independent.
Q: What happens if you take snapshots too frequently? That’s a little more subtle. If you take snapshots too soon then you are in trouble. This means that you should wait a fair amount of time before you start measuring snapshots.
If you wait enough time for the first snapshot and then take measurements more frequently then you are still ok. It’s just you have to be careful with your errorbars because subsequent snapshots may be correlated.
Later on we will see how to estimate
If you are using C++, your code will be very efficient (although make sure you are compiling with -O3
or it will be much too slow!)
In python unfortunately, this will turn out to be very slow especially if you are using for
loops.
The code will be correct but it will take a very long time (this doesn’t matter and won’t necessarily show up for the
Speeding Up Your Code (especially python)#
Extra Credit (5 points)
There is 5 points to get your code working quickly. You can earn this by having your code do 1000 sweeps of an
To speed up your python code, what you want to do is avoid having a for-loop over steps; instead you should work with many of the spins at once.
Heat Bath#
Before we do this, though, we need to make a minor change to the Monte Carlo move we are making (otherwise we accidentally get a pathological Markov Chain)
Our current move is
pick a random spin
consider flipping it
We are going to switch to another move - the heat bath move.
Pick a random spin (say spin
)Consider
making it spin-up with probability
making it spin-down with probability
Notice that this move doesn’t actually care what the current spin was! This means sometimes you are going to replace a configuration with the same configuration. (If you think for a minute you can convince yourself that if you’re considering doing this, you accept it with probability one).
When we do this we need to be a little careful with the acceptance probability. When we were flipping a spin,
In the new move, let’s assume we are moving from
our current configuration
toour new configuration
Then we find that
(since this is the probability of choosing spin-up) and .
Therefore, the ratio
But our ratio of Boltzmann factors gives us
$
Therefore the total acceptance ratio (the product of these two terms)
Therefore, we accept 100% of the moves that we consider (although this isn’t as good as it sounds because some of that time we are accepting a move that doesn’t do anything).
If you do a little bit of algebra, you can rewrite this entire move so that the probability you flip your spin (i.e. the probability you compare to a random number) is
Many Spins At Once#
Let’s consider the
We can consider flipping all of the red squares at once, because the red squares aren’t neighbors of any other red squares (even with the periodic boundary conditions) and so if we flip them all at one time, there isn’t going to be any conflicts. The same is true of the blue, yellow, and green colors (the yellow and green exist to deal with the boundary conditions and the odd-size lattice).
In python, we can access all the colors by doing
# This is for a L x L lattice
x,y=np.indices((L,L))
red=np.logical_and((x+y) % 2==0 , np.logical_and(x<L-1,y<L-1))
red[L-1,L-1]=True
blue=np.logical_and((x+y) % 2==1 , np.logical_and(x<L-1,y<L-1))
green=np.logical_and((x+y) % 2==0 , np.logical_or(x==L-1,y==L-1))
green[L-1,L-1]=False
yellow=np.logical_and((x+y) % 2==1 , np.logical_or(x==L-1,y==L-1))
For each color (say red as an example)
write a function that computes the
for all the red spins without using any for-loops. Two things you might find useful here are thatspins[red]
will access all the red spins. In addition,np.roll(spins,1,axis=0)
will return an array where all the spins are rolled over to the right.After calling this function, write one line of python that returns an array which says which red spins should be flipped.
Write one line of python which flips just those spins.
Just to give a sense, this is how long my version of the naive python code and the efficient python code take for 1000 sweeps:
Slow Code Wall time: 1min 20s
Efficient Code Wall time: 1.06 s
Even Faster Code#
At this point the primary bottleneck of your code is that the Markov chain has a large autocrrelation time near the critical point. This phenomena is called critical slowing down.
Extra Credit (1 point)
Plot the autocorrelation time of your simulation as a function of
To help resolve this problem, essentially you have to move to more sophisticated Monte Carlo techiques. The most general of these is parallel tempering.
For this particular model, though, you have a lot of other choices. The most famous of these choices is the Wolff algorithm. The Wolff algorithm is a cluster-move which flips an entire cluster instead of a single spin at a time.
Another alternative is the worm algorithm. The worm algorithm rewrites the Ising model using a totally different set of variables.
Extra Credit (5 point)
Implement the Wolff algorthim and worm algorithm and measure the autocorrelation times as a function of
Coupling From the Past#
Extension and Extra Credit (10 points)
By writing a Markov Chain Monte Carlo, you’ve produced a code which samples from the Boltzmann distribution for an Ising model - at least if we look away for long enough. Unfortunately, we don’t know how long that necessarily needs to be. In practice, this is rarely a problem. Nonetheless, it would be satisfying if there was an algorithm that exactly sampled from the Boltzmann distribution of the Ising model with no heurestics or additional approximations. There is such an algorithm: coupling from the past. This algorithm is on my list of 10 algorithms I didn’t believe could accomplish what it was claiming when I first heard it (automatic differentiation is also on that list).