I would like to code a Metropolis Hasting algorithm and thought I would get the general scheme of things going with a toy example. I think I got it running but my Mathematica code runs significantly slower than a quick Python version I sketched, with just basic vectorised operations. I would really like to hear your feedback on what could be done better. I am sure the difference would be minimal for properly written Mathematica code.
For the toy example, I am simulating the motion of a particle in a double well $ H = -x^2 + x^4$. moving by discrete steps of length eps. The particle starts at the left well $x = -1 / \sqrt{2}$.I am interested in the time it takes the particle to get over the position $x=0$. I am simulating two different temperature cases, and I would like to get many different realisations (defined in the variable nrealis below).
The code is as follows. I add some comments, would be glad to add more of course.
(* auxiliary variables *)
nrealis = 100;
ntemp = 2;
tem = List [0.05, 0.06];
eps = 0.1;
(* energy of the particle *)
energy[x_] := -x^2 + x^4;
(*get a random integer in {-1,0,1} to define next candidate step*)
candMov[] := RandomInteger[ {-1, 1}]*eps;
(*function adding current displacement to picked step*)
candPos[x_, y_] := x + y;
(* calculating Metropolis Hasting transition probability, based on the energy of current and candidate particle position *)
alfa[T_, xOld_, xNew_] :=
Min[1, Exp[-(1/T)*(energy[xNew] - energy[xOld])]]
(* decide if the step is to be made, by comparing MH transition probability to a value picked from a uniform distribution in (0,1) *)
newPos[beta_, alfa_, xNew_, xOld_] := If[beta < alfa, xNew, xOld];
(*I am interested in the number of iterations for the particle to reach $x=0$ for the first time. The following function applies to the current position, and if it exceeds 0 for the first time it sets the function result to the time counter in the main body of the code. *)
timings[pos_, meta_, time_] :=
If[pos > 0, If[meta != 0, meta, time], meta]
(* initialising vector of current positions. I try to calculate all the realisations, of which there are ntemp * nrealis, in one go *)
xOld = Table[-1 / Sqrt[2], {i, 1, ntemp*nrealis}];
(*initialising iteration counter*)
time = Table[0, {i, 1, ntemp*nrealis}];
(* initialising results vector, i.e. time for first x >0 excursion *)
metaTime = Table[0, {i, 1, ntemp*nrealis}];
(*create a vector of temperatures to use with MapThread, the first nrealis elements get assigned the first temperature, the second half, the second temperature value*)
tempvec =
Table[ tem[[Quotient[i, nrealis] + 1]], {i, 0, ntemp *nrealis - 1}];
(* Main Body
calculate new candidate positions
calculate MH transition probabilities
calculate uniform probabilites
decide if to perform transition
update current positions vector
update results vector
continue until all particles have hit 0
*)
Timing[While[MemberQ[metaTime, 0],
xNew = MapThread[
candPos, {Table[candMov[], {i, 1, ntemp*nrealis}], xOld}];
alfas = MapThread [alfa, {tempvec, xOld, xNew}];
betas = RandomReal[{0, 1}, ntemp*nrealis];
xOld = MapThread[newPos, {betas, alfas, xNew, xOld}];
metaTime = MapThread[timings, {xOld, metaTime, time}]; time++];]
The Python code (which I would gladly share of course) runs the job with the current temperature values in 1.04 seconds, Mathematica needed 62 seconds.
I would really be grateful for any hints or suggestions, thanks in advance.
ADDENDUM: Python reference code
Please find enclosed the Python code I mentioned and used for the comparison. I did not attempt a one-to-one translation. this code is for sure sub-optimal, that is why I was surprised to be a factor 100 slower in Mathematica, in my first attempt. Thanks to the excellent answer a lot of improvements were made, but I am still not matching it (I do not have a C compiler so I cannot use that option in the Compile function, I guess some improvements are to be reaped there as well).
import numpy as np
import random
from random import randrange
def energy (x):
x = np.asarray(x)
energy = -np.power(x,2) + np.power(x,4)
return energy
## creating the vector named tempvec in Mathematica ##
def tempInit(nres, T):
temp = np.zeros(nres * len(T))
for i in range (len(T)):
for j in range(nres):
a = j + nres*(i)
temp [a] = T[i]
return temp
## Initial position vector
def PosInit (nres, ntemp):
initpos = np.zeros(nres*ntemp) - 1/ np.sqrt(2)
return initpos
## candidate position ##
def newPos (x,eps, nres, ntemp):
x = x + np.random.randint(-1,2, size=nres*ntemp) * eps
return x
## equivalent of NewPos function in Mathematica code (with the alfa and beta vectors calculated internally, differently from Mathematica)
def acceptance(xOld, xNew, T, nres, ntemp):
energy_old = energy(xOld)
energy_new = energy (xNew)
alfa = np.minimum(1, np.exp(-(1/T)*(energy_new-energy_old) ) )
beta = np.random.uniform(0,1, nres * ntemp )
xOld = np.where(beta < alfa, xNew, xOld)
return xOld
## Main body
def main (T, nrealis, eps):
ntemp = len(T)
poszero = np.zeros(nrealis * ntemp)
time = np.zeros(nrealis * ntemp)
metatime = np.zeros(nrealis * ntemp)
actual = PosInit(nrealis, ntemp)
tempe = tempInit (nrealis, T)
while np.any(metatime == 0):
candidate = newPos(actual,eps, nrealis, ntemp)
actual = acceptance(actual, candidate, tempe, nrealis, ntemp)
metatime = np.where(actual > poszero , np.where(metatime !=0, metatime, time), metatime )
time = time + 1
## Temperature Values ##
temparr = [0.05,0.06]
## Launching run ##
res = main(temparr, 100,0.1)
EDIT
The final version by HenrikSchumacher is amazingly fast, but I am not entirely sure it does the same computation my naive Python and Mathematica codes, as results are quite different. Just for completeness I will add a couple of lines to postprocess the results and show the difference. In short, the escape time over an energetic barrier is simulated at two temperatures, for an ensemble of particles. The barrier height equals the energy difference between $x = -1 / \sqrt{2}$ and $x=0$ and equals $\Delta E = (1/ \sqrt{2})^2 -(1/ \sqrt{2})^4 = 1/4$. On theoretical grounds the expected escape time scales as $\exp {-\Delta E / T}$, for sufficiently low temperatures. This can be checked by computing the mean of the logarithm of the result vector metaTime for each temperature, and checking their gradient versus $1/T$. For example with
TLowRes = metaTime[[1 ;; nrealis]];
THighRes = metaTime[[nrealis + 1 ;; 2*nrealis]];
LogTLowRes = Log[TLowRes];
LogTHighRes = Log[THighRes];
(Mean[LogTLowRes] - Mean[LogTHighRes]) / (1/tem[[1]] - 1/tem[[2]])
I get values reasonably close to 0.25 for both the naive code versions in my post, while I get much lower values for the code in the answer. I am not implying it is wrong of course, mine might well be, I just cannot pinpoint where the difference is, as the general logic seems the same. Also, the running time should increase with the exponential of the inverse temperature, while it does not seem the case at all.
numba
's@njit
in python, which may make py more quickly. $\endgroup$