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 }