View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.estimator;
39  
40  import static ffx.numerics.estimator.EstimateBootstrapper.getBootstrapIndices;
41  import static ffx.numerics.estimator.Zwanzig.Directionality.BACKWARDS;
42  import static ffx.numerics.estimator.Zwanzig.Directionality.FORWARDS;
43  import static ffx.numerics.math.ScalarMath.fermiFunction;
44  import static java.lang.String.format;
45  import static java.util.Arrays.copyOf;
46  import static java.util.Arrays.fill;
47  import static java.util.Arrays.stream;
48  import static org.apache.commons.math3.util.FastMath.abs;
49  import static org.apache.commons.math3.util.FastMath.log;
50  import static org.apache.commons.math3.util.FastMath.sqrt;
51  
52  import ffx.numerics.math.SummaryStatistics;
53  import ffx.utilities.Constants;
54  
55  import java.util.Random;
56  import java.util.logging.Level;
57  import java.util.logging.Logger;
58  
59  /**
60   * The Bennett Acceptance Ratio class implements the Bennett Acceptance Ratio (BAR) statistical
61   * estimator, based on the Tinker implementation.
62   *
63   * <p>Literature References (from Tinker): C. H. Bennett, "Efficient Estimation of Free Energy
64   * Differences from Monte Carlo Data", Journal of Computational Physics, 22, 245-268 (1976)
65   *
66   * <p>M. A. Wyczalkowski, A. Vitalis and R. V. Pappu, "New Estimators for Calculating Solvation
67   * Entropy and Enthalpy and Comparative Assessments of Their Accuracy and Precision, Journal of
68   * Physical Chemistry, 114, 8166-8180 (2010) [modified BAR algorithm, non-implemented
69   * entropy/enthalpy]
70   *
71   * <p>K. B. Daly, J. B. Benziger, P. G. Debenedetti and A. Z. Panagiotopoulos, "Massively Parallel
72   * Chemical Potential Calculation on Graphics Processing Units", Computer Physics Communications,
73   * 183, 2054-2062 (2012) [non-implemented NPT modification]
74   *
75   * @author Michael J. Schnieders
76   * @author Jacob M. Litman
77   * @since 1.0
78   */
79  public class BennettAcceptanceRatio extends SequentialEstimator implements BootstrappableEstimator {
80  
81    private static final Logger logger = Logger.getLogger(BennettAcceptanceRatio.class.getName());
82  
83    /**
84     * Default BAR convergence tolerance.
85     */
86    private static final double DEFAULT_TOLERANCE = 1.0E-7;
87    /**
88     * Maximum number of BAR iterations.
89     */
90    private static final int MAX_ITERS = 100;
91    /**
92     * Number of simulation windows.
93     */
94    private final int nWindows;
95    /**
96     * Forward Zwanzig free-energy difference estimates.
97     */
98    private final double[] forwardZwanzig;
99    /**
100    * Backward Zwanzig free-energy difference estimates.
101    */
102   private final double[] backwardZwanzig;
103   /**
104    * BAR free-energy difference estimates.
105    */
106   private final double[] barEstimates;
107   /**
108    * BAR free-energy difference uncertainties.
109    */
110   private final double[] barUncertainties;
111   /**
112    * BAR convergence tolerance.
113    */
114   private final double tolerance;
115   /**
116    * Forward Zwanzig instance.
117    */
118   private final Zwanzig forwardsFEP;
119   /**
120    * Backward Zwanzig instance.
121    */
122   private final Zwanzig backwardsFEP;
123   private final Random random;
124   /**
125    * Total BAR free-energy difference estimate.
126    */
127   private double totalBAREstimate;
128   /**
129    * Total BAR free-energy difference uncertainty.
130    */
131   private double totalBARUncertainty;
132   /**
133    * BAR Enthalpy estimates
134    */
135   private final double[] barEnthalpy;
136   /**
137    * Alpha for BAR Enthalpy calculations
138    */
139   private double alpha;
140   /**
141    * sum for BAR Enthalpy Calculations
142    */
143   private double fbsum;
144 
145   /**
146    * Constructs a BAR estimator and obtains an initial free energy estimate.
147    *
148    * @param lambdaValues Values of lambda used.
149    * @param energiesLow  Energies of trajectory i at lambda (i-1).
150    * @param energiesAt   Energies of trajectory i at lambda i.
151    * @param energiesHigh Energies of trajectory i at lambda (i+1).
152    * @param temperature  Temperature of each trajectory.
153    */
154   public BennettAcceptanceRatio(double[] lambdaValues, double[][] energiesLow, double[][] energiesAt,
155                                 double[][] energiesHigh, double[] temperature) {
156     this(lambdaValues, energiesLow, energiesAt, energiesHigh, temperature, DEFAULT_TOLERANCE);
157   }
158 
159   /**
160    * Constructs a BAR estimator and obtains an initial free energy estimate.
161    *
162    * @param lambdaValues Values of lambda used.
163    * @param energiesLow  Energies of trajectory i at lambda (i-1).
164    * @param energiesAt   Energies of trajectory i at lambda i.
165    * @param energiesHigh Energies of trajectory i at lambda (i+1).
166    * @param temperature  Temperature of each trajectory.
167    * @param tolerance    Convergence criterion in kcal/mol for BAR iteration.
168    */
169   public BennettAcceptanceRatio(double[] lambdaValues, double[][] energiesLow, double[][] energiesAt,
170                                 double[][] energiesHigh, double[] temperature, double tolerance) {
171 
172     super(lambdaValues, energiesLow, energiesAt, energiesHigh, temperature);
173 
174     // Used to seed an initial guess.
175     forwardsFEP = new Zwanzig(lambdaValues, energiesLow, energiesAt, energiesHigh, temperature, FORWARDS);
176     backwardsFEP = new Zwanzig(lambdaValues, energiesLow, energiesAt, energiesHigh, temperature, BACKWARDS);
177 
178     nWindows = nTrajectories - 1;
179     forwardZwanzig = forwardsFEP.getBinEnergies();
180     backwardZwanzig = backwardsFEP.getBinEnergies();
181 
182     barEstimates = new double[nWindows];
183     barUncertainties = new double[nWindows];
184     barEnthalpy = new double[nWindows];
185     this.tolerance = tolerance;
186     random = new Random();
187 
188     estimateDG();
189   }
190 
191   /**
192    * Calculates the Fermi function for the differences used in estimating c.
193    *
194    * <p>f(x) = 1 / (1 + exp(x)) x = (e1 - e0 + c) * invRT
195    *
196    * @param e0         Perturbed energy (to be added; evaluated at L +/- dL).
197    * @param e1         Unperturbed energy (to be subtracted; evaluated at L).
198    * @param fermiDiffs Array to be filled with Fermi differences.
199    * @param len        Number of energies.
200    * @param c          Prior best estimate of the BAR offset/free energy.
201    * @param invRT      1.0 / ideal gas constant * temperature.
202    */
203   private static void fermiDiffIterative(double[] e0, double[] e1, double[] fermiDiffs, int len,
204                                          double c, double invRT) {
205     for (int i = 0; i < len; i++) {
206       fermiDiffs[i] = fermiFunction(invRT * (e0[i] - e1[i] + c));
207     }
208   }
209 
210   /**
211    * Calculates forward alpha and fbsum for BAR Enthalpy calculations
212    *
213    * @param e0    Perturbed energy (to be added; evaluated at L +/- dL).
214    * @param e1    Unperturbed energy (to be subtracted; evaluated at L).
215    * @param len   Number of energies.
216    * @param c     Prior best estimate of the BAR offset/free energy.
217    * @param invRT 1.0 / ideal gas constant * temperature.
218    */
219   private void calcAlphaForward(double[] e0, double[] e1, int len, double c, double invRT) {
220 
221     double fsum = 0;
222     double fvsum = 0;
223     double fbvsum = 0;
224     double vsum = 0;
225     fbsum = 0;
226     for (int i = 0; i < len; i++) {
227       double fore = fermiFunction(invRT * (e1[i] - e0[i] - c));
228       double back = fermiFunction(invRT * (e0[i] - e1[i] + c));
229       fsum += fore;
230       fvsum += fore * e0[i];
231       fbvsum += fore * back * (e1[i] - e0[i]);
232       vsum += e0[i];
233       fbsum += fore * back;
234     }
235     alpha = fvsum - (fsum * (vsum / len)) + fbvsum;
236   }
237 
238   /**
239    * Calculates backward alpha and fbsum for  BAR Enthalpy calculations
240    *
241    * @param e0    Perturbed energy (to be added; evaluated at L +/- dL).
242    * @param e1    Unperturbed energy (to be subtracted; evaluated at L).
243    * @param len   Number of energies.
244    * @param c     Prior best estimate of the BAR offset/free energy.
245    * @param invRT 1.0 / ideal gas constant * temperature.
246    */
247   private void calcAlphaBackward(double[] e0, double[] e1, int len, double c, double invRT) {
248 
249     double bsum = 0;
250     double bvsum = 0;
251     double fbvsum = 0;
252     double vsum = 0;
253     fbsum = 0;
254     for (int i = 0; i < len; i++) {
255       double fore = fermiFunction(invRT * (e1[i] - e0[i] - c));
256       double back = fermiFunction(invRT * (e0[i] - e1[i] + c));
257       bsum += back;
258       bvsum += back * e1[i];
259       fbvsum += fore * back * (e1[i] - e0[i]);
260       vsum += e1[i];
261       fbsum += fore * back;
262     }
263     alpha = bvsum - (bsum * (vsum / len)) - fbvsum;
264   }
265 
266 
267   /**
268    * Calculates the Fermi function for the differences used in estimating c, using bootstrap sampling
269    * (choosing random indices with replacement rather than scanning through them all).
270    *
271    * <p>f(x) = 1 / (1 + exp(x)) x = (e1 - e0 + c) * invRT
272    *
273    * @param e0         Perturbed energy (to be added; evaluated at L +/- dL).
274    * @param e1         Unperturbed energy (to be subtracted; evaluated at L).
275    * @param fermiDiffs Array to be filled with Fermi differences.
276    * @param len        Number of energies.
277    * @param c          Prior best estimate of the BAR offset/free energy.
278    * @param invRT      1.0 / ideal gas constant * temperature.
279    */
280   private static void fermiDiffBootstrap(double[] e0, double[] e1, double[] fermiDiffs,
281                                          int len, double c, double invRT, int[] bootstrapSamples) {
282     for (int indexI = 0; indexI < len; indexI++) {
283       int i = bootstrapSamples[indexI];
284       fermiDiffs[indexI] = fermiFunction(invRT * (e0[i] - e1[i] + c));
285     }
286   }
287 
288 
289   /**
290    * Computes one half of the BAR variance.
291    *
292    * @param meanFermi   Mean Fermi value for either state 0 or state 1.
293    * @param meanSqFermi Mean squared Fermi value for either state 0 or state 1.
294    * @param len         Number of values.
295    * @return One half of BAR variance.
296    */
297   private static double uncertaintyCalculation(double meanFermi, double meanSqFermi, int len) {
298     double sqMeanFermi = meanFermi * meanFermi;
299     return ((meanSqFermi - sqMeanFermi) / len) / sqMeanFermi;
300   }
301 
302   /**
303    * Returns the backwards Zwanzig estimator used to seed BAR.
304    *
305    * @return A backwards Zwanzig estimator.
306    */
307   public Zwanzig getInitialBackwardsGuess() {
308     return backwardsFEP;
309   }
310 
311   /**
312    * Main driver for estimation of delta-G. Based on Tinker implementation, which uses the
313    * substitution proposed in Wyczalkowski, Vitalis and Pappu 2010.
314    */
315   @Override
316   public final void estimateDG() {
317     estimateDG(false);
318   }
319 
320   /**
321    * Returns the forwards Zwanzig estimator used to seed BAR.
322    *
323    * @return A forwards Zwanzig estimator.
324    */
325   public Zwanzig getInitialForwardsGuess() {
326     return forwardsFEP;
327   }
328 
329   /**
330    * {@inheritDoc}
331    */
332   @Override
333   public BennettAcceptanceRatio copyEstimator() {
334     return new BennettAcceptanceRatio(lamValues, eLow, eAt, eHigh, temperatures, tolerance);
335   }
336 
337   /**
338    * Main driver for estimation of delta-G. Based on Tinker implementation, which uses the
339    * substitution proposed in Wyczalkowski, Vitalis and Pappu 2010.
340    *
341    * @param randomSamples Whether to use random sampling (for bootstrap analysis).
342    */
343   @Override
344   public final void estimateDG(final boolean randomSamples) {
345     double cumDG = 0;
346     fill(barEstimates, 0);
347     fill(barUncertainties, 0);
348     fill(barEnthalpy, 0);
349 
350     // Avoid duplicate warnings when bootstrapping.
351     Level warningLevel = randomSamples ? Level.FINE : Level.WARNING;
352 
353     for (int i = 0; i < nWindows; i++) {
354       // Free energy estimate/shift constant.
355       double c = 0.5 * (forwardZwanzig[i] + backwardZwanzig[i]);
356 
357       if (!randomSamples) {
358         logger.fine(format(" BAR Iteration   %2d: %12.4f Kcal/mol", 0, c));
359       }
360 
361       double cold = c;
362       int len0 = eAt[i].length;
363       int len1 = eAt[i + 1].length;
364 
365       if (len0 == 0 || len1 == 0) {
366         barEstimates[i] = c;
367         logger.log(warningLevel, format(" Window %d has no snapshots at one end (%d, %d)!", i, len0, len1));
368         continue;
369       }
370 
371       // Ratio of the number of samples: Tinker equivalent: rfrm
372       double sampleRatio = ((double) len0) / ((double) len1);
373 
374       // Fermi differences.
375       double[] fermi0 = new double[len0];
376       double[] fermi1 = new double[len1];
377 
378       // Ideal gas constant * temperature, or its inverse.
379       double rta = Constants.R * temperatures[i];
380       double rtb = Constants.R * temperatures[i + 1];
381       double rtMean = 0.5 * (rta + rtb);
382       double invRTA = 1.0 / rta;
383       double invRTB = 1.0 / rtb;
384 
385       // Summary statistics for Fermi differences for the upper half.
386       SummaryStatistics s1 = null;
387       // Summary statistics for Fermi differences for the lower half.
388       SummaryStatistics s0 = null;
389 
390       // Each BAR convergence cycle needs to operate on the same set of indices.
391       int[] bootstrapSamples0 = null;
392       int[] bootstrapSamples1 = null;
393 
394       if (randomSamples) {
395         bootstrapSamples0 = getBootstrapIndices(len0, random);
396         bootstrapSamples1 = getBootstrapIndices(len1, random);
397       }
398 
399       int cycleCounter = 0;
400       boolean converged = false;
401       while (!converged) {
402         if (randomSamples) {
403           fermiDiffBootstrap(eHigh[i], eAt[i], fermi0, len0, -c, invRTA, bootstrapSamples0);
404           fermiDiffBootstrap(eLow[i + 1], eAt[i + 1], fermi1, len1, c, invRTB, bootstrapSamples1);
405         } else {
406           fermiDiffIterative(eHigh[i], eAt[i], fermi0, len0, -c, invRTA);
407           fermiDiffIterative(eLow[i + 1], eAt[i + 1], fermi1, len1, c, invRTB);
408         }
409 
410         s0 = new SummaryStatistics(fermi0);
411         s1 = new SummaryStatistics(fermi1);
412         double ratio = stream(fermi1).sum() / stream(fermi0).sum();
413 
414         c += rtMean * log(sampleRatio * ratio);
415 
416         converged = (abs(c - cold) < tolerance);
417         cold = c;
418 
419         if (++cycleCounter > MAX_ITERS) {
420           throw new IllegalArgumentException(
421               format(" BAR required too many iterations (%d) to converge!", cycleCounter));
422         }
423 
424         if (!randomSamples) {
425           logger.fine(format(" BAR Iteration   %2d: %12.4f Kcal/mol", cycleCounter, c));
426         }
427       }
428 
429       barEstimates[i] = c;
430       cumDG += c;
431       double sqFermiMean0 = new SummaryStatistics(
432           stream(fermi0).map((double d) -> d * d).toArray()).mean;
433       double sqFermiMean1 = new SummaryStatistics(
434           stream(fermi1).map((double d) -> d * d).toArray()).mean;
435       barUncertainties[i] = sqrt(uncertaintyCalculation(s0.mean, sqFermiMean0, len0)
436           + uncertaintyCalculation(s1.mean, sqFermiMean1, len1));
437 
438       calcAlphaForward(eAt[i], eHigh[i], len0, c, invRTA);
439       double alpha0 = alpha;
440       double fbsum0 = fbsum;
441 
442       calcAlphaBackward(eLow[i + 1], eAt[i + 1], len1, c, invRTB);
443       double alpha1 = alpha;
444       double fbsum1 = fbsum;
445 
446       double hBar = (alpha0 - alpha1) / (fbsum0 + fbsum1);
447       barEnthalpy[i] = hBar;
448     }
449 
450     totalBAREstimate = cumDG;
451     totalBARUncertainty = sqrt(stream(barUncertainties).map((double d) -> d * d).sum());
452   }
453 
454   /**
455    * {@inheritDoc}
456    */
457   @Override
458   public double[] getBinEnergies() {
459     return copyOf(barEstimates, nWindows);
460   }
461 
462   /**
463    * {@inheritDoc}
464    */
465   @Override
466   public double[] getBinUncertainties() {
467     return copyOf(barUncertainties, nWindows);
468   }
469 
470   /**
471    * {@inheritDoc}
472    */
473   @Override
474   public double getFreeEnergy() {
475     return totalBAREstimate;
476   }
477 
478   /**
479    * {@inheritDoc}
480    */
481   @Override
482   public double getUncertainty() {
483     return totalBARUncertainty;
484   }
485 
486   /**
487    * {@inheritDoc}
488    */
489   @Override
490   public int numberOfBins() {
491     return nWindows;
492   }
493 
494   /**
495    * {@inheritDoc}
496    */
497   @Override
498   public double[] getBinEnthalpies() {
499     return barEnthalpy;
500   }
501 }