IMP logo
IMP Reference Guide  develop.4eee3cf66f,2026/08/02
The Integrative Modeling Platform
TuneRex.py
1 #!/usr/bin/env python3
2 
3 
4 __doc__ = """
5 This module provides a few methods to improve the efficiency of a
6 replica-exchange simulation by tuning its parameters.
7 Author: Yannick Spill
8 """
9 
10 import rpy2.robjects as robjects
11 
12 kB = 1.3806503 * 6.0221415 / 4184.0 # Boltzmann constant in kcal/mol/K
13 # here for float comparison. Floats are equal if their difference
14 # is smaller than EPSILON
15 EPSILON = 1e-8
16 debug = False
17 
18 
19 def prdb(arg):
20  if debug:
21  print(arg)
22 
23 
24 # R compatibility functions
25 r = robjects.r
26 robjects.globalenv["kB"] = kB
27 _rinverf = r('invErf <- function(x) {qnorm((1 + x) /2) / sqrt(2)}')
28 _rerf = r('erf <- function(x) {2 * pnorm(x * sqrt(2)) - 1}')
29 _rinvF = r('qf')
30 _rinterp = None
31 
32 
33 def erfinv(x):
34  return _rinverf(x)[0]
35 
36 
37 def erf(x):
38  return _rerf(x)[0]
39 
40 
41 def Finv(x, d1, d2):
42  return _rinvF(x, d1, d2)[0]
43 
44 
45 def spline(xy, mean, method=None):
46  """spline interpolation of (x,y) coordinates. If interpolation goes
47  negative, replace by mean value.
48  """
49  x, y = list(zip(*xy))
50  robjects.globalenv["x"] = robjects.FloatVector(x)
51  robjects.globalenv["y"] = robjects.FloatVector(y)
52  global _rinterp
53  # _rinterp = r.splinefun(x,y)
54  if method is None:
55  r('cvsplinenonbounded <- splinefun(x,y)')
56  else:
57  r('cvsplinenonbounded <- splinefun(x,y,method="%s")' % method)
58  _rinterp = r(
59  'cvspline <- function(x) { tmp = cvsplinenonbounded(x); '
60  'if (tmp>0) {tmp} else {%f}}' %
61  mean)
62 
63  def interpolated(x):
64  global _rinterp # noqa: F824
65  return _rinterp(x)[0]
66  return interpolated
67 
68 
69 def linear_interpolation(xy, mean):
70  """linear interpolation of (x,y) coordinates. No extrapolation possible.
71  """
72  x, y = list(zip(*xy))
73  robjects.globalenv["x"] = robjects.FloatVector(x)
74  robjects.globalenv["y"] = robjects.FloatVector(y)
75  global _rinterp
76  # _rinterp = r.splinefun(x,y)
77  _rinterp = r('cvspline <- approxfun(x,y)')
78 
79  def interpolated(x):
80  global _rinterp # noqa: F824
81  return _rinterp(x)[0]
82  return interpolated
83 
84 
85 # R testing functions
86 
87 def anova(*args):
88  """perform anova using R and return statistic, p-value, between and
89  within variance"""
90  ngroups = len(args) # number of groups
91  # nreps = len(args[0]) #number of repetitions
92  # group = r.gl(ngroups,nreps)
93  reps = r.rep(0, len(args[0]))
94  weight = robjects.FloatVector(args[0])
95  for i in range(1, len(args)):
96  reps += r.rep(i, len(args[i]))
97  weight += robjects.FloatVector(args[i])
98  group = r.factor(reps)
99  robjects.globalenv["weight"] = weight
100  robjects.globalenv["group"] = group
101  lm = r.lm("weight ~ group")
102  aov = r.anova(lm)
103  prdb(aov)
104  # F statistic, p-value, between and within variance
105  anova_result = {'fstat': aov[3][0],
106  'pval': aov[4][0],
107  'between': aov[2][0],
108  'within': aov[2][1],
109  'nsteps': [len(i) for i in args],
110  'nreps': ngroups,
111  'test': 'anova'} # nreps: number of replicas
112  return anova_result
113 
114 
115 def kruskal(*args):
116  """perform kruskal-wallis rank test"""
117  ngroups = len(args)
118  # nreps = len(args[0])
119  # group = r.gl(ngroups,nreps)
120  reps = r.rep(0, len(args[0]))
121  weight = robjects.FloatVector(args[0])
122  for i in range(1, len(args)):
123  reps += r.rep(i, len(args[i]))
124  weight += robjects.FloatVector(args[i])
125  group = r.factor(reps)
126  aov = r('kruskal.test')(group, weight)
127  prdb(aov)
128  kruskal_result = {'fstat': aov[0][0],
129  'pval': aov[2][0],
130  'nsteps': [len(i) for i in args],
131  'nreps': ngroups,
132  'test': 'kruskal'}
133  return kruskal_result # F statistic and p-value
134 
135 
136 def ttest(obs, target):
137  """perform a one-sample two-sided t-test on obs against target mean"""
138  test = r('t.test')(robjects.IntVector(obs), mu=target)
139  return test[0][0], test[2][0] # stat and p-value
140 
141 
142 def binom(obs, target):
143  """perform an exact binomial test on the mean of obs against target"""
144  success = sum(obs)
145  trials = len(obs)
146  test = r('binom.test')(success, trials, p=target)
147  return test[0][0], test[2][0] # stat and p-value
148 
149 
150 def bartlett(*args):
151  """perform bartlett's test on the equality of variances of the
152  observations"""
153  ngroups = len(args)
154  nreps = len(args[0])
155  group = r.gl(ngroups, nreps)
156  weight = robjects.IntVector(args[0])
157  for i in args[1:]:
158  weight += robjects.IntVector(i)
159  robjects.globalenv["weight"] = weight
160  robjects.globalenv["group"] = group
161  var = r('bartlett.test')(weight, group)
162  return var[0][0], var[2][0] # statistic and p-value
163 
164 
165 def fligner(*args):
166  """perform Fligner-Killeen non-parametric test of the variance equality"""
167  ngroups = len(args)
168  nreps = len(args[0])
169  group = r.gl(ngroups, nreps)
170  weight = robjects.IntVector(args[0])
171  for i in args[1:]:
172  weight += robjects.IntVector(i)
173  robjects.globalenv["weight"] = weight
174  robjects.globalenv["group"] = group
175  var = r('fligner.test')(weight, group)
176  return var[0][0], var[2][0] # statistic and p-value
177 
178 
179 def power_test(ar, power=0.8, alpha=0.05):
180  """perform an anova power test and return
181  - the power of the test with this input data
182  - the number of points that would be needed to achieve a default
183  power of 0.8
184  ar: the output of anova()
185  """
186  result = r('power.anova.test')(groups=ar['nreps'], n=min(ar['nsteps']),
187  between=ar['between'], within=ar['within'],
188  sig=alpha)
189  prdb('the power of this anova was: %.3f' % result[5][0])
190  result = r('power.anova.test')(groups=ar['nreps'],
191  between=ar['between'], within=ar['within'],
192  sig=alpha, pow=power)
193  prdb('To have a power of %.3f, there should be at least %d exchange '
194  'attempts.' % (power, result[1][0]))
195  return
196 
197 
198 def minimum_n(ar, alpha=0.05):
199  """This routine tries to return an estimate of the additional number of
200  exchange trials that could lead to a positive result of the anova (e.g.
201  the average ARs are not the same). It is still very crude. It also assumes
202  that a one-way anova was made.
203  ar: the output of anova()
204  alpha: type I error
205  """
206  nreps = ar['nreps']
207  nsteps = ar['nsteps']
208  try:
209  nsteps = min(nsteps)
210  except: # noqa: E722
211  pass
212  fstat = ar['fstat']
213  return nsteps * (numpy.sqrt(Finv(1 - alpha, nreps - 1,
214  nreps * (nsteps - 1)) / fstat) - 1)
215 
216 
217 # Heat capacity class
219 
220  """When created, estimates the heat capacity from the energies or from the
221  indicator functions using the specified method. Two methods are then
222  available to guess the heat capacity at a given parameter value: get()
223  returns a single point, and mean() returns the linear average between two
224  points.
225  """
226 
227  def __init__(self, params, energies=None, indicators=None,
228  method="constant", temps=None, write_cv=False):
229 
230  self.__initialized = False
231  self.__cv = []
232  self.method = method
233 
234  if method == "interpolate":
235  self.estimate_cv_interpolate(params, indicators)
236  self.get = self.get_interp
237  self.mean = self.mean_interp
238  elif method == "constant":
239  self.estimate_cv_constant(params, indicators)
240  self.get = lambda p: self.__cv
241  self.mean = lambda p1, p2: self.__cv
242  self.__cvfun = self.get
243  r('cvspline <- function(x) {%f}' % self.__cv)
244  elif method == "mbar":
245  self.estimate_cv_mbar(params, energies, temps)
246  self.get = self.get_mbar
247  self.mean = self.mean_mbar
248  else:
249  raise NotImplementedError(method)
250 
251  self.__initialized = True
252 
253  # write the heat capacity to a file
254  if write_cv:
255  fl = open('cv', 'w')
256  fl.write("".join(["%f %f\n" % (x, self.get(x))
257  for x in numpy.linspace(params[0] / 2, 2 * params[-1])]))
258  fl.close()
259 
260  def estimate_cv_interpolate(self, params, indicators):
261  """interpolate using previous values, by reversing the approximate
262  overlap function
263  """
264  if self.__initialized:
265  return
266  if len(indicators) != len(params) - 1:
267  raise ValueError(
268  "the length of indicators and params does not match!")
269  if params != tuple(sorted(params)):
270  raise NotImplementedError(
271  "unable to work on parameters that do not change "
272  "monotonically")
273 
274  prdb("storing params and means")
275  self.__params = params
276  self.__pmeans = [(params[i] + params[i + 1]) / 2.
277  for i in range(len(params) - 1)]
278  prdb("computing __cv")
279  for i, ind in enumerate(indicators):
280  mean = sum(ind) / float(len(ind))
281  Y2 = 2 * kB * erfinv(1 - mean) ** 2
282  p1 = params[i]
283  p2 = params[i + 1]
284  self.__cv.append(
285  (self.__pmeans[i],
286  (p1 ** 2 + p2 ** 2) * float(Y2) / (p2 - p1) ** 2))
287  prdb(self.__params)
288  prdb(self.__cv)
289  self.__cvmean = sum([i[1] for i in self.__cv]) / float(len(self.__cv))
290  if self.__cvmean < 0:
291  raise ValueError("Cv mean is negative!")
292  self.__cvfun = spline(self.__cv, self.__cvmean)
293  return
294 
295  def estimate_cv_constant(self, params, indicators):
296  """try to guess which constant cv fits best"""
297  if self.__initialized:
298  return
299  self.estimate_cv_interpolate(params, indicators)
300  self.__cv = self.__cvmean
301  return
302 
303  def needs_init(self):
304  if not self.__initialized:
305  raise RuntimeError("Class was not initialized correctly!")
306 
307  def estimate_cv_mbar(self, params, energies, temps):
308  "use MBAR to get the heat capacity"
309  raise NotImplementedError("estimate_cv_mbar")
310 
311  def _isinbounds(self, p, params):
312  """returns True if p is within params, else false. the params list
313  must be sorted ascendingly."""
314  if p < params[0] - EPSILON or p > params[-1] + EPSILON:
315  # prdb("Warning: value %f is outside of bounds, "
316  # "extrapolating." % p)
317  return False
318  else:
319  return True
320 
321  def _interpolate(self, xval, xlist):
322  """return interpolation of Cv at point xval, and return the average
323  instead if this value is negative.
324  """
325  self._isinbounds(xval, xlist)
326  val = self.__cvfun(xval)
327  if val > 0:
328  return val
329  else:
330  return self.__cvmean
331 
332  def get_interp(self, param):
333  """returns the point estimate of the first derivative of the energy
334  with respect to the replica exchange parameter (usually T or q).
335  This version assumes that the means of cv are given.
336  """
337  self.needs_init()
338  return self._interpolate(param, self.__pmeans)
339 
340  def get_mbar(self, param):
341  """returns the point estimate of the first derivative of the energy
342  with respect to the replica exchange parameter (usually T or q).
343  This version assumes that the values of cv are given.
344  """
345  self.needs_init()
346  return self._interpolate(param, self.__params)
347 
348  def mean_interp(self, pa, pb):
349  """estimate the mean of Cv between two points. Here the means were
350  stored previously
351  """
352  self.needs_init()
353  return self._interpolate((pa + pb) / 2., self.__pmeans)
354 
355  def mean_mbar(self, pa, pb):
356  self.needs_init()
357  return (self.get_mbar(pb) + self.get_mbar(pa)) / 2.
358 
359 # Parameter updating methods
360 
361 
362 def update_good_dumb(newp, oldp, *args, **kwargs):
363  """Here the old parameters are oldp[0] and oldp[1], and the starting point
364  is newp[0]. We should modify newp[1] so that the AR in the following cycle
365  is equal to the targetAR.
366  In the "dumb" method, the Cv and targetAR keywords are ignored.
367  Here the newp[1] parameter is modified because prior changes have set
368  newp[0] to a different value than oldp[0]. Thus, we should move newp[1] by
369  minimizing the effect on the AR since it is supposedly equal to targetAR.
370  In this simple method, the parameter is just translated.
371  """
372  prdb(
373  "newp[0] has moved (%.3f -> %.3f), adjusting the position of newp[1]" %
374  (oldp[0], newp[0]))
375  return oldp[1] - (oldp[0] - newp[0])
376 
377 
378 def update_bad_dumb(newp, oldp, ind, targetAR=0.4, scale=0.1, **kwargs):
379  """Here the old parameters are oldp[0] and oldp[1], and the starting point
380  is newp[0]. We should modify newp[1] so that the AR in the following cycle
381  is equal to the targetAR.
382  In the "dumb" method, the Cv keyword is ignored. Here the newp[1]
383  parameter is modified to follow the possible translation of newp[0] by
384  calling update_good_dumb, and then newp[1] is added or subtracted scale% of
385  (oldp[1] - oldp[0]) to adjust to targetAR.
386  """
387 
388  if newp[0] != oldp[0]:
389  newp[1] = update_good_dumb(newp, oldp)
390  if targetAR > sum(ind) / float(len(ind)):
391  prdb("""target AR is higher than expected, decreasing newp[1]""")
392  newp[1] -= scale * (oldp[1] - oldp[0])
393  else:
394  prdb("""target AR is lower than expected, increasing newp[1]""")
395  newp[1] += scale * (oldp[1] - oldp[0])
396  return newp[1]
397 
398 
399 def update_any_cv_step(newp, oldp, ind, targetAR=0.4, Cv=None, **kwargs):
400  """here we use the average AR formula of two gaussians to get newp[1] as a
401  function of newp[1], knowing the targetAR and estimating the Cv. If
402  targetAR is negative, consider that mean(ind) equals the target AR and
403  skip any calculation in the case that oldp[0] equals newp[0].
404  step: suppose the heat capacity is stepwise constant, i.e. use the heat
405  capacity at position newp[0] as an estimate of the mean of the heat
406  capacity between newp[0] and newp[1]. This does not require any
407  self-consistent loop.
408  """
409 
410  global kB # noqa: F824
411 
412  if abs(oldp[0] - newp[0]) < EPSILON and targetAR < 0:
413  return oldp[1]
414  if targetAR < 0:
415  targetAR = sum(ind) / float(len(ind))
416  cv = Cv.get(newp[0])
417  Y = numpy.sqrt(2 * kB) * float(erfinv(1 - targetAR))
418  if Y ** 2 >= cv:
419  raise ValueError("""targetAR too small for this approximate method, use
420  the full self-consistent method instead.""")
421  return newp[0] * (cv + Y * numpy.sqrt(2 * cv - Y ** 2)) / (cv - Y ** 2)
422 
423 
424 def update_any_cv_sc(newp, oldp, ind, targetAR=0.4, Cv=None,
425  tol=1e-6, maxiter=10000):
426  """self-consistent solver version"""
427 
428  global kB # noqa: F824
429 
430  if abs(oldp[0] - newp[0]) < EPSILON and targetAR < 0:
431  return oldp[1]
432  if targetAR < 0:
433  targetAR = sum(ind) / float(len(ind))
434  cv = Cv.get(newp[0])
435  Y = numpy.sqrt(2 * kB) * float(erfinv(1 - targetAR))
436  if Y ** 2 >= cv:
437  raise ValueError("""targetAR too small for this approximate method, use
438  the full self-consistent method instead.""")
439  targetp = newp[0] * (cv + Y * numpy.sqrt(2 * cv - Y ** 2)) / (cv - Y ** 2)
440  for i in range(maxiter):
441  cv = Cv.mean(newp[0], targetp)
442  (oldtargetp, targetp) = (
443  targetp, newp[0] * (cv + Y * numpy.sqrt(2 * cv - Y ** 2))
444  / (cv - Y ** 2))
445  if abs(targetp - oldtargetp) <= tol:
446  break
447  if numpy.isnan(targetp):
448  if Y ** 2 >= cv:
449  raise ValueError(
450  "targetAR too small for this approximate method, use the "
451  "full self-consistent method instead.")
452  else:
453  raise ValueError("""something unexpected happened""")
454  if i == maxiter - 1:
455  prdb("""Warning: unable to converge the self-consistent after %d
456  iterations and a tolerance of %f. Change the method or decrease the
457  tolerance!""" % (maxiter, tol))
458  prdb("converged after %d iterations and a tolerance of %f for x=%f" %
459  (i, tol, oldp[1]))
460  return targetp
461 
462 
463 def update_any_cv_scfull(newp, oldp, ind, targetAR=0.4, Cv=None,
464  tol=1e-6, maxiter=10000):
465  """self-consistent solver version, on the exact average AR equation"""
466 
467  # create helper functions and overlap function
468  _ = r('u21 <- function(t1,t2) { integrate(Vectorize(cvspline),'
469  't1,t2)$value }')
470  _ = r('b21 <- function(t1,t2) { 1./(kB*t2) - 1./(kB*t1) }')
471  _ = r('sigma2 <- function(t) {cvspline(t)*kB*t**2}')
472  _rovboltz = r('ovboltz <- function(t1,t2) {\
473  1/2*( 1-erf(\
474  u21(t1,t2)/sqrt(2*(sigma2(t1)+sigma2(t2))))\
475  + exp(b21(t1,t2)*(u21(t1,t2)+b21(t1,t2)*(sigma2(t1)+sigma2(t2))/2))\
476  * (1+erf((u21(t1,t2)+b21(t1,t2)*(sigma2(t1)+sigma2(t2)))\
477  /(sqrt(2*(sigma2(t1)+sigma2(t2))))))\
478  )}')
479  _rrootfn = r(
480  'rootfn <- function(t2) {ovboltz(%f,t2)-%f}' %
481  (newp[0], targetAR))
482 
483  # find upper bound for estimation, raise an error if cv is negative
484  if oldp[1] > oldp[0]:
485  tmp = newp[0] * 1.05
486  else:
487  tmp = newp[0] * 0.95
488  nloops = 0
489  while _rrootfn(tmp)[0] >= 0:
490  nloops += 1
491  tmp += (oldp[1] - oldp[0])
492  if Cv.get(tmp) < 0:
493  raise RuntimeError("heat capacity goes negative")
494  if nloops > maxiter:
495  raise RuntimeError('could not find zero of function!')
496 
497  # find root
498  _runiroot = r('uniroot(rootfn,c(%f,%f),f.lower = %f, f.upper = %f, '
499  'tol = %f, maxiter = %d)'
500  % (newp[0], tmp, 1 - targetAR, -targetAR, tol, maxiter))
501  prdb("self-consistent solver converged after %s iterations and an "
502  "estimated precision of %s " % (_runiroot[2][0], _runiroot[3][0]))
503  prdb(
504  ["root:",
505  _runiroot[0][0],
506  "overlap:",
507  _rovboltz(newp[0],
508  _runiroot[0][0])[0]])
509  return _runiroot[0][0]
510 
511 
512 def update_any_cv_nr(newp, oldp, ind, targetAR=0.4, Cv=None, **kwargs):
513  """newton-raphson solver version"""
514 
515  # use nlm
516  raise NotImplementedError
517 
518 # Testing methods
519 
520 
521 def are_equal_to_targetAR(
522  indicators,
523  targetAR=0.4,
524  alpha=0.05,
525  method="binom"):
526  """here, all indicators have same average, we want to know if it is
527  equal to targetAR
528  """
529 
530  # calculate sample mean deviation of each indicator function from targetAR
531  deviations = sorted([(abs(sum(ind) / float(len(ind)) - targetAR), ind)
532  for pos, ind in enumerate(indicators)])
533  deviant = deviations[-1]
534 
535  # perform t-test
536  if method == "ttest":
537  # from statlib.stats import ttest_1samp as ttest
538  our_ttest = ttest
539  elif method == "binom":
540  our_ttest = binom
541  else:
542  raise NotImplementedError
543 
544  try:
545  test, pval = our_ttest(deviant[1], targetAR)
546  except: # noqa: E722
547  if abs(targetAR - sum(deviant[1]) / len(deviant[1])) > EPSILON:
548  pval = 0
549  else:
550  pval = 1
551  if pval < alpha:
552  return False
553  else:
554  return True
555 
556 
557 def are_stationnary(indicators, alpha=0.05, method="anova"):
558  """test on the stationarity of the observations (block analysis). Done
559  so by launching an anova on the difference between the two halves of
560  each observations.
561  """
562 
563  if method == "kruskal":
564  test = kruskal
565  else:
566  test = anova
567 
568  tmp = numpy.array(indicators)
569  blocklen = len(indicators[0]) / 2
570  block = tmp[:, :blocklen] - tmp[:, blocklen:2 * blocklen]
571  if test(*block)['pval'] < alpha:
572  return False
573  else:
574  return True
575 
576 
577 def are_equal(indicators, targetAR=0.4, alpha=0.05,
578  method="anova", varMethod="skip", power=0.8):
579  """Perform a one-way ANOVA or kruskal-wallis test on the indicators set,
580  and return True if one cannot exclude with risk alpha that the indicators
581  AR are different (i.e. True = all means are equal). Also performs a test
582  on the variance (they must be equal).
583  """
584 
585  if min(targetAR, 1 - targetAR) * len(indicators[0]) <= 5 and \
586  (varMethod == "bartlett" or method == "anova"):
587  prdb("Warning: normal approximation to the binomial does not hold!")
588 
589  # test the variances
590  if varMethod == "skip":
591  pass
592  else:
593  if varMethod == "bartlett":
594  pval = bartlett(*indicators)[1]
595  elif varMethod == "fligner":
596  pval = fligner(*indicators)[1]
597  else:
598  raise NotImplementedError(
599  "variance testing method unknown: %s" %
600  varMethod)
601  if pval < alpha:
602  prdb("Warning: performing mean test with unequal variances.")
603 
604  if method == "kruskal":
605  test = kruskal
606  else:
607  test = anova
608 
609  tr = test(*indicators)
610  tr['alpha'] = alpha
611  # p-value < alpha => H0 rejected => result == False
612  tr['result'] = tr['pval'] >= alpha
613 
614  return tr
615 
616 
617 def find_good_ARs(indicators, targetAR=0.4, alpha=0.05, method="binom"):
618  """perform one-sample t-tests on each of the data sets, and return
619  a tuple of bool of size N-1, False if AR i is not equal to targetAR
620  at risk alpha.
621  """
622 
623  # calculate sample means of each indicator function
624  means = sorted([(sum(ind) / float(len(ind)), pos, ind)
625  for pos, ind in enumerate(indicators)])
626 
627  # perform t-test
628  if method == "ttest":
629  # from statlib.stats import ttest_1samp as ttest
630  our_ttest = ttest
631  elif method == "binom":
632  our_ttest = binom
633  else:
634  raise NotImplementedError
635 
636  isGoodTuple = []
637  # start from the lowest means and stop when they are ok
638  prdb("starting left")
639  for (i, (mean, pos, ind)) in enumerate(means):
640  prdb("performing t-test on couple %d having average AR %f, "
641  "position %d" % (pos, mean, i))
642  try:
643  test, pval = our_ttest(ind, targetAR)
644  except: # noqa: E722
645  if abs(targetAR - mean) > EPSILON:
646  pval = 0
647  else:
648  pval = 1
649  if pval < alpha:
650  # means are different
651  isGoodTuple.append((pos, False))
652  else:
653  goodstart = i
654  break
655  # then start from the highest means
656  prdb("starting right")
657  for (i, (mean, pos, ind)) in enumerate(reversed(means)):
658  prdb("performing t-test on couple %d having average AR %f, position %d"
659  % (pos, mean, len(means) - 1 - i))
660  if our_ttest(ind, targetAR)[1] < alpha:
661  # means are different
662  isGoodTuple.append((pos, False))
663  else:
664  goodstop = len(means) - 1 - i
665  break
666 
667  # limiting cases: all different
668  if len(isGoodTuple) > len(indicators):
669  return tuple([False] * len(indicators))
670  # all equal
671  elif len(isGoodTuple) == 0:
672  return tuple([True] * len(indicators))
673  # intermediate
674  else:
675  isGoodTuple.extend([(means[i][1], True) for i in
676  range(goodstart, goodstop + 1)])
677  isGoodTuple.sort()
678  return tuple([tup[1] for tup in isGoodTuple])
679 
680 # Trebst, Katzgraber, Nadler and Hansmann optimum flux stuff
681 
682 
683 def mean_first_passage_times(
684  replicanums_ori,
685  subs=1,
686  start=0,
687  use_avgAR=False):
688  """compute mean first passage times as suggested in
689  Nadler W, Meinke J, Hansmann UHE, Phys Rev E *78* 061905 (2008)
690 
691  use_avgAR : if a list of average ARs is given computes everything from
692  average AR; if it is False, compute by direct counting of events.
693 
694  returns:
695  If use_avgAR == False:
696  tau0, tauN, chose_N, times0, timesN
697  else:
698  tau0, tauN, None, None, None
699  tau0[i]: average time to go from replica 0 to replica i
700  tauN[i]: average time to go from replica N to replica i
701  times0 and timesN are the corresponding lists of single events.
702 
703  """
704 
705  from numpy import array, zeros
706  replicanums = array(replicanums_ori)[:, start::subs]
707  N = len(replicanums)
708  tauN = [0] * N
709  tau0 = [0] * N
710 
711  if use_avgAR:
712  tau0[0] = 0.0
713  tauN[-1] = 0.0
714  for state in range(1, N):
715  tau0[state] = tau0[state - 1] + \
716  state / (float(use_avgAR[state - 1]))
717  for state in reversed(range(1, N)):
718  tauN[state - 1] = tauN[state] \
719  + (N - (state - 1)) / (float(use_avgAR[state - 1]))
720 
721  return tau0, tauN, None, None, None
722 
723  else:
724  # prdb('not using average AR')
725  # the algorithm looks for replicas that start at the lowest temp, and
726  # records the farthest state it went to before returning to zero. Once
727  # back it increments the counter of all concerned replicas. Similar
728  # procedure if starting from N.
729  store0 = zeros((N, N), dtype=bool)
730  last0 = [0 for i in range(N)]
731  already0 = [False for i in range(N)]
732  times0 = [[] for i in range(N)]
733  storeN = zeros((N, N), dtype=bool)
734  lastN = [0 for i in range(N)]
735  alreadyN = [False for i in range(N)]
736  timesN = [[] for i in range(N)]
737 
738  # prdb('looping over replicanums')
739  for time, frame in enumerate(zip(*replicanums)):
740  # case of the replica in state 0
741  if not already0[frame[0]]:
742  last0[frame[0]] = time
743  store0[frame[0], :] = True
744  already0[frame[0]] = True
745  # case of the replica in state N
746  if not alreadyN[frame[-1]]:
747  lastN[frame[-1]] = time
748  storeN[frame[-1], :] = True
749  alreadyN[frame[-1]] = True
750  # set already flags to False when in state 1 or N-1
751  already0[frame[1]] = False
752  alreadyN[frame[-2]] = False
753  # loop over all states
754  for state, rep in enumerate(frame):
755  if store0[rep, state]:
756  # reached a state for the first time since 0
757  store0[rep, state] = False
758  # store time since this replica left state 0
759  times0[state].append(time - last0[rep])
760  if storeN[rep, state]:
761  # reached a state for the first time since N
762  storeN[rep, state] = False
763  # store time since this replica left state N
764  timesN[state].append(time - lastN[rep])
765  # prdb([replicanums.shape, len(storeN), len(last0)])
766  # times = [[] for i in range(N)]
767  chose_N = [len(timesN[state]) > len(times0[state]) for state in
768  range(N)]
769  for state in range(N):
770  tauN[state] = sum(timesN[state]) / float(len(timesN[state]))
771  tau0[state] = sum(times0[state]) / float(len(times0[state]))
772  # prdb(len(chose_N))
773 
774  return tau0, tauN, chose_N, times0, timesN
775 
776 
777 def compute_effective_fraction(tau0, tauN, chose_N):
778  """input: tau0, tauN, chose_N
779  output: effective fraction f(T) (P_up(n)) as introduced in
780  Trebst S, Troyer M, Hansmann UHE, J Chem Phys *124* 174903 (2006).
781  formalized in
782  Nadler W, Hansmann UHE, Phys Rev E *75* 026109 (2007)
783  and whose calculation is enhanced in
784  Nadler W, Meinke J, Hansmann UHE, Phys Rev E *78* 061905 (2008)
785  the ideal value of f(n) should be 1 - n/N
786  """
787 
788  # nstar is the index of the last state where tau0 should be used.
789  N = len(tau0)
790  if chose_N is None:
791  nstar = N / 2
792  else:
793  nstar = N - sum([int(a) for a in chose_N]) - 1
794 
795  prdb("n* = %d" % nstar)
796  # compute helper functions h
797  h0 = [0] * N
798  h0[1] = tau0[1]
799  for state in range(2, nstar + 1):
800  h0[state] = h0[state - 1] + \
801  (tau0[state] - tau0[state - 1]) / float(state)
802 
803  hN = [0] * N
804  hN[-2] = tauN[-2]
805  for state in reversed(range(nstar, N - 1)):
806  hN[state] = hN[state + 1] + \
807  (tauN[state] - tauN[state + 1]) / float(N - state)
808 
809  # compute flow probabilities
810  pup = [0] * N
811  pup[0] = 1
812  for n in range(1, nstar + 1):
813  pup[n] = 1 - h0[n] / (h0[nstar] + hN[nstar])
814  for n in range(nstar + 1, N):
815  pup[n] = hN[n] / (h0[nstar] + hN[nstar])
816 
817  return pup
818 
819 
820 def spline_diffusivity(pup, params):
821  """spline interpolation of diffusivity: D = 1/(df/dT * heta)
822  """
823  from numpy import linspace
824  robjects.globalenv["hetay"] = \
825  robjects.FloatVector(linspace(0, 1, num=len(params)).tolist())
826  robjects.globalenv["hetax"] = robjects.FloatVector(params)
827  robjects.globalenv["pupx"] = robjects.FloatVector(params)
828  robjects.globalenv["pupy"] = robjects.FloatVector(pup)
829  _ = r('heta <- splinefun(hetax,hetay,method="monoH.FC")')
830  _ = r('eff <- splinefun(pupx,pupy,method="monoH.FC")')
831  diff = r('diff <- function(x) {-1/(heta(x,deriv=1)*eff(x,deriv=1))}')
832  return lambda x: diff(x)[0]
833 
834 
835 # Misc
836 def compute_indicators(replicanums, subs=1, start=0):
837  """input: replicanums : a list of N lists of size M, where N is the number
838  of states and M is the length of the simulation. Each element is an
839  integer, and corresponds to the label of a replica.
840  output: an indicator function of exchanges (size (N-1)x(M-1)), 1 if
841  exchange and 0 if not.
842  """
843  def exchange(n, m):
844  if replicanums[n][m] == replicanums[n + 1][m + 1] \
845  and replicanums[n][m + 1] == replicanums[n + 1][m]:
846  return 1
847  else:
848  return 0
849 
850  indicators = []
851  for n in range(len(replicanums) - 1):
852  indicators.append(
853  [exchange(n, m)
854  for m in range(len(replicanums[n]) - 1)][start::subs])
855  return indicators
856 
857 # Main routines
858 
859 
860 def update_params_nonergodic(pup, params, write_g=False, num=False):
861 
862  from numpy import linspace
863  # g = spline(zip(pup,params),0,method='monoH.FC')
864  g = linear_interpolation(list(zip(pup, params)), 0)
865  if write_g:
866  d = spline_diffusivity(pup, params)
867  fl = open('g', 'w')
868  fl.write("".join(["%f %f\n" % (x, g(x))
869  for x in linspace(0, 1, num=100)]))
870  fl.close()
871  fl = open('diffusivity', 'w')
872  fl.write(''.join(["%f %f\n" % (x, d(x)) for x in
873  linspace(params[0], params[-1], num=100)]))
874  fl.close()
875  fl = open('pup', 'w')
876  fl.write("".join(["%f %f\n" % (i, j) for (i, j) in zip(params, pup)]))
877  fl.close()
878 
879  if num is False:
880  newparams = [g(i) for i in reversed(linspace(0, 1, num=len(params)))]
881  else:
882  newparams = [g(i) for i in reversed(linspace(0, 1, num=num))]
883  # for numerical issues
884  newparams[0] = params[0]
885  newparams[-1] = params[-1]
886 
887  return newparams
888 
889 
890 def update_params(
891  indicators, params, isGood, targetAR=0.4, immobilePoint=1,
892  Cv=None, badMethod="dumb", goodMethod="dumb", dumb_scale=0.1):
893  """update the parameters according to the isGood tuple and using the
894  specified methods"""
895 
896  newparams = list(params) # make a copy
897 
898  if immobilePoint != 1:
899  raise NotImplementedError
900 
901  if Cv is None and (badMethod != "dumb" or goodMethod != "dumb"):
902  raise RuntimeError("""Cv needs to be estimated if using other methods
903  than 'dumb' for updating!""")
904 
905  if goodMethod == "dumb":
906  update_good = update_good_dumb
907  elif goodMethod == "step":
908  update_good = update_any_cv_step
909  elif goodMethod == "sc":
910  update_good = update_any_cv_sc
911  elif goodMethod == "scfull":
912  update_good = update_any_cv_scfull
913  elif goodMethod == "nr":
914  update_good = update_any_cv_nr
915  else:
916  raise NotImplementedError(goodMethod)
917  if badMethod == "dumb":
918  update_bad = update_bad_dumb
919  elif badMethod == "step":
920  update_bad = update_any_cv_step
921  elif badMethod == "sc":
922  update_bad = update_any_cv_sc
923  elif badMethod == "scfull":
924  update_bad = update_any_cv_scfull
925  elif badMethod == "nr":
926  update_bad = update_any_cv_nr
927  else:
928  raise NotImplementedError(badMethod)
929 
930  # scan each position starting from the immobilePoint
931  for pos in range(len(params) - 1):
932  if isGood[pos]:
933  newparams[pos + 1] = update_good(
934  newparams[pos:pos + 2], params[pos:pos + 2],
935  indicators[pos], targetAR=targetAR, Cv=Cv)
936  else:
937  newparams[pos + 1] = update_bad(
938  newparams[pos:pos + 2], params[pos:pos + 2],
939  indicators[pos], targetAR=targetAR, Cv=Cv, scale=dumb_scale)
940 
941  return tuple(newparams)
942 
943 
944 def tune_params_flux(replicanums, params, subs=1, start=0, alpha=0.05,
945  testMethod='anova', meanMethod='binom', use_avgAR=False,
946  power=0.8, num=False):
947  # num is here if you want to add some more temperatures. indicate total
948  # number of replicas
949 
950  # TODO: do case where one estimates all based on target AR.
951  if use_avgAR is not False:
952  raise NotImplementedError
953 
954  prdb("computing mean first passage times")
955  tau0, tauN, chose_N, times0, timesN = mean_first_passage_times(
956  replicanums, subs=subs, start=start, use_avgAR=use_avgAR)
957 
958  prdb("average round trip time: %.2f (%d+%d events)" %
959  (tau0[-1] + tauN[0], len(times0[-1]), len(timesN[0])))
960  prdb("checking if the parameterset needs to be improved")
961  N = len(replicanums)
962  if chose_N is None:
963  nstar = N / 2
964  else:
965  nstar = N - sum([int(a) for a in chose_N]) - 1
966 
967  reduced = []
968  # no need to check for times?[0] or times?[N]
969  for n in range(1, N - 1):
970  if n > nstar:
971  reduced.append([i * 2.0 / ((N - n) * (N - n + 1))
972  for i in timesN[n]])
973  else:
974  reduced.append([i * 2.0 / (n * (n + 1)) for i in times0[n]])
975 
976  anova_result = are_equal(reduced, alpha=alpha, method=testMethod,
977  power=power)
978  if (anova_result['result']): # TODO test if equal to targetAR
979  prdb("flux is constant, nothing to do!")
980  min_n = minimum_n(anova_result, alpha)
981  prdb('Try to rerun this test with at least %d more samples.' %
982  numpy.ceil(min_n))
983  return (False, min_n)
984 
985  # the flux is not constant so the parameters need improvement.
986  # calculate the estimate of the effective fraction
987  prdb("parameterset not optimal, computing effective fraction")
988  pup = compute_effective_fraction(tau0, tauN, chose_N)
989 
990  # improve parameterset
991  prdb("returning new parameterset")
992  params = update_params_nonergodic(pup, params, num=num)
993 
994  return (True, params)
995 
996 
997 def tune_params_ar(indicators, params, targetAR=0.4, alpha=0.05,
998  immobilePoint=1, CvMethod="skip", badMethod="dumb",
999  goodMethod="dumb", varMethod="skip", testMethod="anova",
1000  meanMethod="binom", energies=None, temps=None, power=0.8,
1001  dumb_scale=0.1):
1002  """Tune the replica-exchange parameters and return a new set.
1003 
1004  Arguments:
1005  indicators -- an (N-1)x(M-1) table, where entry at position (j,i)
1006  is True if replica j and j+1 performed an exchange between
1007  times i and i+1.
1008  params -- the current set of N parameters used in the simulation.
1009 
1010  Keyword arguments:
1011  targetAR -- the target AR which is wanted for the simulation
1012  (default: 0.4)
1013  alpha -- the type 1 error on the one-way ANOVA and subsequent
1014  t-tests (default: 5%)
1015  immobilePoint -- which replica should keep it's parameter fixed
1016  (default: 1st replica, e.g. 1)
1017  CvMethod -- the heat capacity estimation method (default:
1018  "skip", other options: "mbar", "spline", "constant")
1019  badMethod -- how to correct the (j+1)th parameter if the
1020  acceptance ratio between replicas j and j+1 is off the
1021  target value (default: "dumb", options: "step", "sc",
1022  "scfull", "nr")
1023  goodMethod -- how to update the value of the (j+1)th parameter
1024  in the case of a correctly exchanging couple, but if the jth
1025  parameter has been modified (default: "dumb",options: "step",
1026  "sc" self-consistent, "scfull" self-consistent using the exact
1027  equation, "nr" newton-raphson solver for the exact equation)
1028  dumb_scale -- (0.0-1.0) in the "dumb" method, scale wrong temperature
1029  intervals by this amount. (default: 0.1)
1030  testMethod -- how to test for the difference of the means,
1031  either "anova" for a one-way anova, or "kruskal" for a
1032  Kruskal-Wallis one-way non-parametric anova.
1033  meanMethod -- "ttest" for a two-sided one-sample t-test,
1034  "binom" for an exact binomial test of the probability.
1035  varMethod -- how to test for the equality of variances.
1036  "fligner" for the Fligner-Killeen non-parametric test,
1037  "bartlett" for Bartlett's test, "skip" to pass.
1038  energies -- if CvMethod is set to "mbar", the energies of each
1039  state as a function of time are used to estimate the heat capacity.
1040  temps -- the temperatures of the simulations, if estimating
1041  with "mbar".
1042 
1043  Return Value:
1044  returns a tuple: (bool, params). bool is True if params have
1045  changed, and params is the new set.
1046 
1047  """
1048 
1049  # perform ANOVA
1050  prdb("performing ANOVA")
1051  anova_result = are_equal(indicators, targetAR, alpha, method=testMethod,
1052  varMethod=varMethod, power=power)
1053  if (anova_result['result']
1054  and are_equal_to_targetAR(indicators, targetAR, alpha,
1055  method=meanMethod)):
1056  prdb("all means are equal to target AR, nothing to do!")
1057  min_n = minimum_n(anova_result, alpha)
1058  prdb(
1059  'Try to rerun this test with at least %d more samples.' %
1060  numpy.ceil(min_n))
1061  return (False, min_n)
1062  prdb("some means are different, performing t-tests")
1063 
1064  # perform two-by-two t-tests
1065  isGood = find_good_ARs(indicators, targetAR, alpha, method=meanMethod)
1066  if not (False in isGood):
1067  prdb("""Bad luck: ANOVA says means are not identical, but t-tests
1068  can't find the bad rates...""")
1069  return (False, params)
1070  prdb(isGood)
1071 
1072  # check if data is stationnary by doing a block analysis
1073  prdb("performing stationarity test")
1074  if not are_stationnary(indicators, alpha):
1075  prdb("Warning: Some simulations are not stationary!")
1076 
1077  # interpolate the heat capacity from the data
1078  # TODO: use all previous data, not just from this run.
1079  prdb("launching Cv estimation or skipping it")
1080  if CvMethod == "skip":
1081  Cv = None
1082  elif CvMethod == "interpolate" or CvMethod == "constant":
1083  Cv = CvEstimator(params, indicators=indicators, method=CvMethod)
1084  elif CvMethod == "mbar":
1085  Cv = CvEstimator(
1086  params,
1087  energies=energies,
1088  temps=temps,
1089  method=CvMethod)
1090  else:
1091  raise NotImplementedError(CvMethod)
1092 
1093  # update parameters
1094  prdb('updating params')
1095  # update the current parameter set to match the target AR
1096  params = update_params(indicators, params, isGood, targetAR=targetAR,
1097  immobilePoint=immobilePoint, Cv=Cv,
1098  badMethod=badMethod, goodMethod=goodMethod,
1099  dumb_scale=dumb_scale)
1100 
1101  prdb('Done')
1102  return (True, params)
1103 
1104 
1105 if __name__ == '__main__':
1106  import numpy
1107  replicanums = []
1108  for i in range(1, 8):
1109  replicanums.append(
1110  tuple(numpy.fromfile('data/replica-indices/%d.rep' % i,
1111  dtype=int, sep='\n')))
1112 
1113  prdb("replicanums: %dx%d" % (len(replicanums), len(replicanums[0])))
1114  params = tuple(numpy.fromfile('data/temperatures', sep=' '))
1115 
1116  prdb(params)
1117  indicators = compute_indicators(replicanums, subs=1, start=0)
1118  prdb("indicators: %dx%d" % (len(indicators), len(indicators[0])))
1119  prdb("Exchange rate:")
1120  prdb(numpy.array([sum(ind) / float(len(ind)) for ind in indicators]))
1121  numpy.array([sum(ind) / float(len(ind))
1122  for ind in indicators]).tofile('xchgs', sep='\n')
1123  changed, newparams = tune_params_ar(
1124  indicators, params, targetAR=0.25,
1125  badMethod="dumb", goodMethod="dumb", CvMethod="skip",
1126  testMethod="anova", alpha=0.05)
1127  if not changed:
1128  print("Parameter set seems optimal.")
1129  else:
1130  if True not in [abs(newparams[i + 1] - newparams[i]) < 1e-3
1131  for i in range(len(newparams) - 1)]:
1132  numpy.array(newparams).tofile('data/temperatures', sep=' ')
1133  else:
1134  print("PROBLEM IN NEW PARAMETERSET -> not saved")
1135  print("params :", params)
1136  print("new params:", newparams)
def estimate_cv_constant
try to guess which constant cv fits best
Definition: TuneRex.py:295
def estimate_cv_interpolate
interpolate using previous values, by reversing the approximate overlap function
Definition: TuneRex.py:260
When created, estimates the heat capacity from the energies or from the indicator functions using the...
Definition: TuneRex.py:218
def mean_interp
estimate the mean of Cv between two points.
Definition: TuneRex.py:348
def estimate_cv_mbar
use MBAR to get the heat capacity
Definition: TuneRex.py:307
def get_mbar
returns the point estimate of the first derivative of the energy with respect to the replica exchange...
Definition: TuneRex.py:340
def get_interp
returns the point estimate of the first derivative of the energy with respect to the replica exchange...
Definition: TuneRex.py:332