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-2025.
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 ffx.numerics.math.RunningStatistics;
41  import ffx.numerics.math.SummaryStatistics;
42  
43  import java.util.Random;
44  import java.util.concurrent.ThreadLocalRandom;
45  import java.util.logging.Logger;
46  
47  import static java.lang.String.format;
48  import static java.util.Arrays.stream;
49  import static org.apache.commons.math3.util.FastMath.sqrt;
50  
51  /**
52   * Bootstrap Free Energy Estimate.
53   */
54  public class EstimateBootstrapper {
55  
56    private static final Logger logger = Logger.getLogger(EstimateBootstrapper.class.getName());
57    private static final int DEFAULT_LOG_INTERVAL = 100;
58  
59    private final BootstrappableEstimator estimate;
60    private final int nWindows;
61    private final SummaryStatistics[] freeEnergyDifferenceResults;
62    private final SummaryStatistics[] enthalpyResults;
63  
64    /**
65     * Constructor.
66     *
67     * @param estimator Estimator to bootstrap.
68     */
69    public EstimateBootstrapper(BootstrappableEstimator estimator) {
70      this.estimate = estimator;
71      nWindows = estimate.getNumberOfBins();
72      freeEnergyDifferenceResults = new SummaryStatistics[nWindows];
73      enthalpyResults = new SummaryStatistics[nWindows];
74    }
75  
76    /**
77     * Gets randomized bootstrap indices; ensures there are at least two distinct indices.
78     *
79     * @param length Number of random indices to generate in range [0,length)
80     * @return Randomized indices.
81     */
82    public static int[] getBootstrapIndices(int length) {
83      return getBootstrapIndices(length, ThreadLocalRandom.current());
84    }
85  
86    /**
87     * Gets randomized bootstrap indices; ensures there are at least two distinct indices.
88     *
89     * @param length Number of random indices to generate in range [0,length)
90     * @param random Source of randomness.
91     * @return Randomized indices.
92     */
93    public static int[] getBootstrapIndices(int length, Random random) {
94      return getBootstrapIndices(length, random, Math.min(2, length));
95    }
96  
97    /**
98     * Gets randomized bootstrap indices; ensures there are at least a few distinct indices.
99     *
100    * @param length      Number of random indices to generate in range [0,length)
101    * @param random      Source of randomness.
102    * @param minDistinct Minimum number of distinct indices.
103    * @return Randomized indices.
104    */
105   public static int[] getBootstrapIndices(int length, Random random, int minDistinct) {
106     // Handle extremely short lengths with special-case handling.
107     switch (length) {
108       case 0 -> {
109         return new int[0];
110       }
111       case 1 -> {
112         return new int[]{0};
113       }
114       case 2 -> {
115         int[] indices = new int[2];
116         indices[0] = random.nextBoolean() ? 0 : 1;
117         indices[1] = random.nextBoolean() ? 0 : 1;
118         return indices;
119       }
120       // Default: leave switch and handle general case.
121     }
122 
123     // General case.
124     int[] indices = random.ints(length, 0, length).toArray();
125     long distinctVal = stream(indices).distinct().count();
126     int ctr = 0;
127     while (distinctVal <= minDistinct) {
128       logger.info(
129           format(" Regenerating array (iteration %d): only %d distinct values found for length %d.",
130               ++ctr, distinctVal, length));
131       indices = random.ints(length, 0, length).toArray();
132       distinctVal = stream(indices).distinct().count();
133     }
134     return indices;
135   }
136 
137   /**
138    * Get bootstrap Enthalpy results for each window.
139    *
140    * @return Bootstrap Enthalpy results for each window.
141    */
142   public SummaryStatistics[] getEnthalpyResults() {
143     return enthalpyResults;
144   }
145 
146   /**
147    * Perform bootstrap analysis.
148    *
149    * @param trials Number of trials.
150    */
151   public void bootstrap(long trials) {
152     bootstrap(trials, DEFAULT_LOG_INTERVAL);
153   }
154 
155   /**
156    * Perform bootstrap analysis.
157    *
158    * @param trials      Number of trials.
159    * @param logInterval Interval between logging statements.
160    */
161   public void bootstrap(long trials, long logInterval) {
162     RunningStatistics[] windows = new RunningStatistics[nWindows];
163     RunningStatistics[] enthalpyWindows = new RunningStatistics[nWindows];
164     for (int i = 0; i < nWindows; i++) {
165       windows[i] = new RunningStatistics();
166       enthalpyWindows[i] = new RunningStatistics();
167     }
168 
169     for (long i = 0; i < trials; i++) {
170       if ((i + 1) % logInterval == 0) {
171         logger.fine(format(" Bootstrap Trial %d", i + 1));
172       }
173 
174       estimate.estimateDG(true);
175 
176       double[] fe = estimate.getFreeEnergyDifferences();
177       double[] enthalpy = estimate.getEnthalpyDifferences();
178       for (int j = 0; j < nWindows; j++) {
179         windows[j].addValue(fe[j]);
180         enthalpyWindows[j].addValue(enthalpy[j]);
181       }
182     }
183 
184     for (int i = 0; i < nWindows; i++) {
185       freeEnergyDifferenceResults[i] = new SummaryStatistics(windows[i]);
186       enthalpyResults[i] = new SummaryStatistics(enthalpyWindows[i]);
187     }
188   }
189 
190   /**
191    * Get the total free energy difference estimate from bootstrap analysis.
192    *
193    * @return The total free energy difference estimate.
194    */
195   public double getTotalFreeEnergyDifference() {
196     return getTotalFreeEnergyDifference(getFreeEnergyDifferences());
197   }
198 
199   /**
200    * Get the total enthalpy estimate from bootstrap analysis.
201    *
202    * @return The total enthalpy estimate.
203    */
204   public double getTotalEnthalpyChange() {
205     return getTotalEnthalpyChange(getEnthalpyChanges());
206   }
207 
208   /**
209    * Get the total entropy change (-TdS) from bootstrap analysis.
210    *
211    * @return The total entropy change (-TdS).
212    */
213   public double getTotalEntropyChange() {
214     // dG = dH - TdS
215     double dG = getTotalFreeEnergyDifference();
216     double dH = getTotalEnthalpyChange();
217     return dG - dH;
218   }
219 
220   /**
221    * Get bootstrap free energy estimate for each window.
222    * <p>
223    * getFreeEnergyDifferences
224    *
225    * @return Return the bootstrap free energy difference estimate for each window.
226    */
227   public double[] getFreeEnergyDifferences() {
228     return stream(freeEnergyDifferenceResults).mapToDouble(SummaryStatistics::getMean).toArray();
229   }
230 
231   /**
232    * Get bootstrap enthalpy estimate for each window.
233    *
234    * @return Return the bootstrap enthalpy estimate for each window.
235    */
236   public double[] getEnthalpyChanges() {
237     return stream(enthalpyResults).mapToDouble(SummaryStatistics::getMean).toArray();
238   }
239 
240   /**
241    * Get bootstrap entropy estimate for each window (-TdS).
242    *
243    * @return Return the bootstrap entropy estimate (-TdS) for each window.
244    */
245   public double[] getEntropyChanges() {
246     double[] dG = getFreeEnergyDifferences();
247     double[] dH = getEnthalpyChanges();
248     double[] dS = new double[nWindows];
249     for (int i = 0; i < nWindows; i++) {
250       dS[i] = dG[i] - dH[i];
251     }
252     return dS;
253   }
254 
255   /**
256    * Get the total free energy difference uncertainty estimate from bootstrap analysis.
257    *
258    * @return The total free energy difference uncertainty estimate.
259    */
260   public double getTotalFEDifferenceUncertainty() {
261     return getTotalFEDifferenceUncertainty(getFEDifferenceVariances());
262   }
263 
264   /**
265    * Get the total enthalpy uncertainty estimate from bootstrap analysis.
266    *
267    * @return The total enthalpy uncertainty estimate.
268    */
269   public double getTotalEnthalpyUncertainty() {
270     return getTotalEnthalpyUncertainty(getEnthalpyVariances());
271   }
272 
273   /**
274    * Get the total entropy uncertainty estimate from bootstrap analysis.
275    * This is computed as the sqrt of the sum of the free energy and enthalpy variances.
276    *
277    * @return The total enthalpy uncertainty estimate.
278    */
279   public double getTotalEntropyUncertainty() {
280     double dG = getTotalFEDifferenceUncertainty();
281     double dH = getTotalEnthalpyUncertainty();
282     return sqrt(dG * dG + dH * dH);
283   }
284 
285   /**
286    * Get the free energy difference uncertainties (standard deviations) from bootstrap analysis for each
287    * window.
288    *
289    * @return Returns free energy difference standard deviation estimate for each window.
290    */
291   public double[] getFEDifferenceStdDevs() {
292     return stream(freeEnergyDifferenceResults).mapToDouble(SummaryStatistics::getSd).toArray();
293   }
294 
295   /**
296    * Get the enthalpy standard deviation estimate from bootstrap analysis for each window.
297    *
298    * @return Returns enthalpy standard deviation estimate for each window.
299    */
300   public double[] getEnthalpyStdDevs() {
301     return stream(enthalpyResults).mapToDouble(SummaryStatistics::getSd).toArray();
302   }
303 
304   /**
305    * Get the entropy standard deviation estimate from bootstrap analysis for each window.
306    * <p>
307    * This is computed as the sqrt of the sum of the free energy and enthalpy variances for each window.
308    *
309    * @return Returns enthalpy standard deviation estimate for each window.
310    */
311   public double[] getEntropyStdDevs() {
312     double[] dG = getFEDifferenceStdDevs();
313     double[] dH = getEnthalpyStdDevs();
314     double[] dS = new double[nWindows];
315     for (int i = 0; i < nWindows; i++) {
316       dS[i] = sqrt(dG[i] * dG[i] + dH[i] * dH[i]);
317     }
318     return dS;
319   }
320 
321   /**
322    * Get the total free energy difference estimate from per window bootstrap analysis.
323    *
324    * @param freeEnergyDifferences The free energy difference estimate for each window.
325    * @return The total free energy difference estimate from bootstrap analysis.
326    */
327   public double getTotalFreeEnergyDifference(double[] freeEnergyDifferences) {
328     return estimate.getTotalFreeEnergyDifference(freeEnergyDifferences);
329   }
330 
331   /**
332    * Get the free energy difference variance estimate from bootstrap analysis for each window.
333    *
334    * @return Returns free energy difference variance estimate for each window.
335    */
336   public double[] getFEDifferenceVariances() {
337     return stream(freeEnergyDifferenceResults).mapToDouble(SummaryStatistics::getVar).toArray();
338   }
339 
340   /**
341    * Get the total free energy difference uncertainty estimate from per window bootstrap analysis.
342    *
343    * @param variances The free energy difference variance estimate (not uncertainty) for each window.
344    * @return The total free energy difference uncertainty from bootstrap analysis.
345    */
346   public double getTotalFEDifferenceUncertainty(double[] variances) {
347     return estimate.getTotalFEDifferenceUncertainty(variances);
348   }
349 
350   /**
351    * Get the total enthalpy estimate from per window bootstrap analysis.
352    *
353    * @param enthalpyChanges The enthalpy estimate for each window.
354    * @return The total enthalpy difference estimate from bootstrap analysis.
355    */
356   public double getTotalEnthalpyChange(double[] enthalpyChanges) {
357     return estimate.getTotalFreeEnergyDifference(enthalpyChanges);
358   }
359 
360   /**
361    * Get the total enthalpy uncertainty estimate from per window bootstrap analysis.
362    *
363    * @param variances The enthalpy variance estimate (not uncertainty) for each window.
364    * @return The total enthalpy uncertainty estimate from bootstrap analysis.
365    */
366   public double getTotalEnthalpyUncertainty(double[] variances) {
367     return estimate.getTotalEnthalpyUncertainty(variances);
368   }
369 
370   /**
371    * Get the enthalpy variance estimate from bootstrap analysis for each window.
372    *
373    * @return Returns enthalpy variance estimate for each window.
374    */
375   public double[] getEnthalpyVariances() {
376     return stream(enthalpyResults).mapToDouble(SummaryStatistics::getVar).toArray();
377   }
378 }