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.xray;
39  
40  import edu.rit.pj.ParallelTeam;
41  import ffx.algorithms.Terminatable;
42  import ffx.crystal.Crystal;
43  import ffx.crystal.HKL;
44  import ffx.crystal.ReflectionList;
45  import ffx.numerics.optimization.LBFGS;
46  import ffx.numerics.optimization.LineSearch.LineSearchResult;
47  import ffx.numerics.optimization.OptimizationListener;
48  import ffx.xray.solvent.SolventModel;
49  
50  import javax.annotation.Nullable;
51  import java.util.logging.Level;
52  import java.util.logging.Logger;
53  
54  import static ffx.numerics.math.ScalarMath.b2u;
55  import static ffx.numerics.math.ScalarMath.u2b;
56  import static ffx.utilities.Constants.NS2SEC;
57  import static java.lang.Double.isNaN;
58  import static java.lang.Math.max;
59  import static java.lang.String.format;
60  import static java.lang.System.arraycopy;
61  import static java.util.Arrays.fill;
62  import static org.apache.commons.math3.util.FastMath.exp;
63  import static org.apache.commons.math3.util.FastMath.log;
64  
65  /**
66   * ScaleBulkMinimize class.
67   *
68   * @author Timothy D. Fenn
69   * @since 1.0
70   */
71  public class ScaleBulkMinimize implements OptimizationListener, Terminatable {
72  
73    private static final Logger logger = Logger.getLogger(ScaleBulkMinimize.class.getName());
74  
75    private final ReflectionList reflectionList;
76    private final DiffractionRefinementData refinementData;
77    private final Crystal crystal;
78    private final CrystalReciprocalSpace crystalReciprocalSpace;
79    private final ScaleBulkEnergy bulkSolventEnergy;
80    private final int solventN;
81    private final int n;
82    private final double[] x;
83    private final double[] grad;
84    private final double[] scaling;
85    private boolean done = false;
86    private boolean terminate = false;
87    private long time;
88    private double grms;
89    private int nSteps;
90  
91    /**
92     * Constructor for ScaleBulkMinimize.
93     *
94     * @param reflectionList         a {@link ffx.crystal.ReflectionList} object.
95     * @param refinementData         a {@link ffx.xray.DiffractionRefinementData} object.
96     * @param crystalReciprocalSpace a {@link ffx.xray.CrystalReciprocalSpace} object.
97     * @param parallelTeam           the ParallelTeam to execute the ScaleBulkMinimize.
98     */
99    public ScaleBulkMinimize(ReflectionList reflectionList,
100                            DiffractionRefinementData refinementData,
101                            CrystalReciprocalSpace crystalReciprocalSpace,
102                            ParallelTeam parallelTeam) {
103     this.reflectionList = reflectionList;
104     this.refinementData = refinementData;
105     this.crystal = reflectionList.crystal;
106     this.crystalReciprocalSpace = crystalReciprocalSpace;
107 
108     if (crystalReciprocalSpace.getSolventModel() == SolventModel.NONE ||
109         refinementData.bulkSolventFixed) {
110       solventN = 1;
111     } else {
112       solventN = 3;
113     }
114     n = solventN + refinementData.nScale;
115     bulkSolventEnergy = new ScaleBulkEnergy(reflectionList, refinementData, n, parallelTeam);
116 
117     x = new double[n];
118     grad = new double[n];
119     scaling = new double[n];
120     fill(scaling, 1.0);
121 
122     // Initialize K overall if its zero.
123     if (refinementData.modelScaleK == 0.0) {
124       x[0] = getInitialKOverall(x);
125       logger.info(format("\n Initial K_Overall (Fc to Fo Scale): %4.2f", exp(0.25 * x[0])));
126     } else {
127       x[0] = refinementData.modelScaleK;
128     }
129 
130     // Refinement of Bulk Solvent Parameters.
131     if (solventN > 1) {
132       x[1] = refinementData.bulkSolventK;
133       x[2] = refinementData.bulkSolventUeq;
134     }
135     for (int i = 0; i < 6; i++) {
136       if (crystal.scaleB[i] >= 0) {
137         x[solventN + crystal.scaleB[i]] = refinementData.modelAnisoB[i];
138       }
139     }
140   }
141 
142   /**
143    * getCoordinates.
144    *
145    * @param x the array to populate with parameters or null to create a new array.
146    * @return an array containing the parameters.
147    */
148   public double[] getCoordinates(@Nullable double[] x) {
149     if (x == null) {
150       x = new double[this.x.length];
151     }
152     arraycopy(this.x, 0, x, 0, this.x.length);
153     return x;
154   }
155 
156   /**
157    * Retrieves the number of variables used in the optimization process.
158    *
159    * @return the number of variables.
160    */
161   public int getNumberOfVariables() {
162     return x.length;
163   }
164 
165   /**
166    * Performs a minimization procedure with the specified convergence criterion (epsilon).
167    *
168    * @param eps the convergence threshold for the minimization process; smaller values indicate tighter convergence criteria.
169    * @return a {@link ScaleBulkEnergy} object resulting from the minimization process.
170    */
171   public ScaleBulkEnergy minimize(double eps) {
172     return minimize(7, eps);
173   }
174 
175   /**
176    * Performs a minimization process to optimize scaling and bulk solvent energy parameters.
177    *
178    * @param m   the number of prior gradient evaluations used by the L-BFGS algorithm;
179    *            larger values can improve convergence for challenging optimizations,
180    *            but may increase memory usage.
181    * @param eps the convergence threshold for the minimization process; smaller values indicate
182    *            tighter convergence criteria.
183    * @return the {@link ScaleBulkEnergy} object representing the refined bulk solvent energy after
184    * the minimization process.
185    */
186   public ScaleBulkEnergy minimize(int m, double eps) {
187     bulkSolventEnergy.setScaling(scaling);
188     double e = bulkSolventEnergy.energyAndGradient(x, grad);
189 
190     long mtime = -System.nanoTime();
191     time = -System.nanoTime();
192     done = false;
193     int status = LBFGS.minimize(n, m, x, e, grad, eps, bulkSolventEnergy, this);
194     done = true;
195     switch (status) {
196       case 0:
197         logger.info(format("\n Optimization achieved convergence criteria: %10.5f\n", grms));
198         break;
199       case 1:
200         logger.info(format("\n Optimization terminated at step %d.\n", nSteps));
201         break;
202       default:
203         logger.warning("\n Optimization failed.\n");
204     }
205     refinementData.modelScaleK = x[0] / scaling[0];
206     if (solventN > 1) {
207       refinementData.bulkSolventK = x[1] / scaling[1];
208       refinementData.bulkSolventUeq = x[2] / scaling[2];
209     }
210     for (int i = 0; i < 6; i++) {
211       int offset = crystal.scaleB[i];
212       if (offset >= 0) {
213         int index = solventN + offset;
214         refinementData.modelAnisoB[i] = x[index] / scaling[index];
215       }
216     }
217 
218     mtime += System.nanoTime();
219     logger.info(format(" Optimization time: %8.3f (sec)\n", mtime * NS2SEC));
220 
221     bulkSolventEnergy.setScaling(null);
222 
223     return bulkSolventEnergy;
224   }
225 
226   /**
227    * {@inheritDoc}
228    */
229   @Override
230   public boolean optimizationUpdate(int iter, int nBFGS, int nfun, double grms, double xrms,
231                                     double f, double df, double angle, LineSearchResult info) {
232     long currentTime = System.nanoTime();
233     Double seconds = (currentTime - time) * NS2SEC;
234     time = currentTime;
235     this.grms = grms;
236     this.nSteps = iter;
237 
238     if (iter == 0) {
239       if (nBFGS > 0) {
240         if (solventN > 1) {
241           logger.info("\n Limited Memory BFGS Quasi-Newton Optimization of Scaling and Solvent Parameters\n");
242         } else {
243           logger.info("\n Limited Memory BFGS Quasi-Newton Optimization of Overall Scaling Parameters\n");
244         }
245       } else {
246         if (solventN > 1) {
247           logger.info("\n Steepest Decent Optimization of Scaling and Solvent Parameters\n");
248         } else {
249           logger.info("\n Steepest Decent Optimization of Overall Scaling Parameters\n");
250         }
251       }
252       logger.info(" Cycle       Energy      G RMS    Delta E   Delta X    Angle  Evals     Time      R  Rfree");
253     }
254     if (info == null) {
255       logger.info(format("%6d %12.5f %10.6f", iter, f, grms));
256     } else {
257       if (info == LineSearchResult.Success) {
258         double R = bulkSolventEnergy.getR();
259         double Rfree = bulkSolventEnergy.getRfree();
260         logger.info(format("%6d %12.5f %10.6f %10.6f %9.5f %8.2f %6d %8.3f %5.3f %5.3f",
261             iter, f, grms, df, xrms, angle, nfun, seconds, R, Rfree));
262       } else {
263         logger.info(format("%6d %12.5f %10.6f %10.6f %9.5f %8.2f %6d %8s",
264             iter, f, grms, df, xrms, angle, nfun, info));
265       }
266     }
267     if (terminate) {
268       logger.info(" The optimization received a termination request.");
269       // Tell the L-BFGS optimizer to terminate.
270       return false;
271     }
272     return true;
273   }
274 
275   /**
276    * {@inheritDoc}
277    */
278   @Override
279   public void terminate() {
280     terminate = true;
281     while (!done) {
282       synchronized (this) {
283         try {
284           wait(1);
285         } catch (Exception e) {
286           logger.log(Level.WARNING, "Exception terminating minimization.\n", e);
287         }
288       }
289     }
290   }
291 
292   /**
293    * getScaleBulkEnergy.
294    *
295    * @return a {@link ffx.xray.ScaleBulkEnergy} object.
296    */
297   ScaleBulkEnergy getScaleBulkEnergy() {
298     return bulkSolventEnergy;
299   }
300 
301   /**
302    * Calculates the initial overall scaling factor (KOverall) for the refinement process
303    * based on the provided parameters.
304    *
305    * @param x an array of double values representing current refinement parameters.
306    * @return a double value representing the initial overall scaling factor.
307    */
308   private double getInitialKOverall(double[] x) {
309     double[][] fc = refinementData.fc;
310     double[][] fSigf = refinementData.fSigF;
311     double[] grad = new double[x.length];
312 
313     bulkSolventEnergy.setScaling(scaling);
314     double e = bulkSolventEnergy.energyAndGradient(x, grad);
315     bulkSolventEnergy.setScaling(null);
316 
317     double sumfofc = 0.0;
318     double sumfc = 0.0;
319     for (HKL ih : reflectionList.hklList) {
320       int i = ih.getIndex();
321       if (isNaN(fc[i][0]) || isNaN(fSigf[i][0]) || fSigf[i][1] <= 0.0) {
322         continue;
323       }
324 
325       double fcTotF = refinementData.fcTotF(i);
326       sumfofc += fSigf[i][0] * fcTotF;
327       sumfc += fcTotF * fcTotF;
328     }
329 
330     // x[0] = log(4.0 * sumfofc / sumfc);
331 
332     // logger.info(" Setting initial scale factor.");
333     // logger.info(format(" Sum FoFc: %16.8f", sumfofc));
334     // logger.info(format(" Sum FcFc: %16.8f", sumfc));
335     // logger.info(format(" 4.0 * Log(Sum FoFc / Sum FcFc): %16.8f", x[0]));
336 
337     return 4.0 * log(sumfofc / sumfc);
338   }
339 
340   /**
341    * Perform a grid optimization of the bulk solvent model to minimize the residual energy and improve
342    * refinement parameters. This method adjusts the bulk solvent parameters (A and B) within a specified
343    * range using a grid search algorithm, evaluates the associated energy, and identifies optimal
344    * parameters based on the minimum residual.
345    */
346   public void gridOptimizeBulkSolventModel() {
347     if (crystalReciprocalSpace == null) {
348       return;
349     }
350 
351     bulkSolventEnergy.setScaling(scaling);
352 
353     // Reset solvent A & B parameters to their default values.
354     crystalReciprocalSpace.setDefaultSolventAB();
355 
356     // Initial target values.
357     crystalReciprocalSpace.computeDensity(refinementData.fs);
358     double initialTarget = bulkSolventEnergy.energy(x);
359     double min = initialTarget;
360     double R = bulkSolventEnergy.getR();
361     double Rfree = bulkSolventEnergy.getRfree();
362     double minR = R;
363     double minRfree = Rfree;
364 
365     double solventA = crystalReciprocalSpace.getSolventA();
366     double solventB = crystalReciprocalSpace.getSolventB();
367     double amin = solventA - 2.0;
368     double amax = solventA + 2.0;
369     double astep = 0.25;
370     double bmin = solventB - 0.2;
371     double bmax = solventB + 0.2;
372     double bstep = 0.05;
373     if (crystalReciprocalSpace.getSolventModel() == SolventModel.BINARY) {
374       amin = solventA - 0.4;
375       amax = solventA + 0.4;
376       astep = 0.05;
377     }
378 
379     logger.info(" Grid Search for Bulk Solvent Model Parameters");
380     logger.info("               A      B    Target      R  Rfree");
381     logger.info(format(" Initial: %6.3f %6.3f %9.6f %6.3f %6.3f ",
382         solventA, solventB, min, R, Rfree));
383 
384     int index = 0;
385     for (double a = amin; a <= amax; a += astep) {
386       for (double b = bmin; b <= bmax; b += bstep) {
387         crystalReciprocalSpace.setSolventAB(a, b);
388         crystalReciprocalSpace.computeDensity(refinementData.fs);
389         double sum = bulkSolventEnergy.energy(x);
390         R = bulkSolventEnergy.getR();
391         Rfree = bulkSolventEnergy.getRfree();
392 
393         if (logger.isLoggable(Level.FINE)) {
394           logger.fine(format(" %8d %6.3f %6.3f %9.6f %6.3f %6.3f ", index, a, b, sum, R, Rfree));
395         } else if (sum < min) {
396           logger.info(format(" %8d %6.3f %6.3f %9.6f %6.3f %6.3f ", index, a, b, sum, R, Rfree));
397         }
398 
399         index++;
400         if (sum < min) {
401           min = sum;
402           minR = R;
403           minRfree = Rfree;
404           solventA = a;
405           solventB = b;
406         }
407       }
408     }
409 
410     logger.info(format(" Minimum: %6.3f %6.3f %9.6f %6.3f %6.3f ", solventA, solventB, min, minR, minRfree));
411     if (min < initialTarget) {
412       crystalReciprocalSpace.setSolventAB(solventA, solventB);
413       crystalReciprocalSpace.computeDensity(refinementData.fs);
414     }
415 
416     bulkSolventEnergy.setScaling(null);
417   }
418 
419   /**
420    * Perform a grid search optimization for bulk solvent and scaling parameters.
421    * The method adjusts the scaling factor and bulk solvent parameters (Ks and Bs)
422    * to minimize the energy function and improve the refinement results.
423    */
424   public void gridOptimizeKsBs() {
425     if (solventN < 3) {
426       return;
427     }
428 
429     bulkSolventEnergy.setScaling(scaling);
430 
431     double bulkSolventK = refinementData.bulkSolventK;
432     double bulkSolventUeq = refinementData.bulkSolventUeq;
433 
434     // Starting point.
435     x[0] = refinementData.modelScaleK;
436     x[1] = bulkSolventK;
437     x[2] = bulkSolventUeq;
438     double initialSum = bulkSolventEnergy.energy(x);
439     double initialR = bulkSolventEnergy.getR();
440     double initialRfree = bulkSolventEnergy.getRfree();
441     double min = initialSum;
442 
443     // Prepare for the grid search.
444     // Make sure Ks is at least 0.3.
445     if (bulkSolventK < 0.3) {
446       bulkSolventK = 0.3;
447     }
448     // Make sure Bs is at least 40.0.
449     if (u2b(bulkSolventUeq) < 40.0) {
450       bulkSolventUeq = b2u(40.0);
451     }
452 
453     double kmin = bulkSolventK - 0.3;
454     double kmax = bulkSolventK + 0.3;
455     double kstep = 0.02;
456     double initialB = u2b(bulkSolventUeq);
457     double umin = b2u(max(0.0, initialB - 40.0));
458     double umax = b2u(initialB + 40.0);
459     double ustep = b2u(2.0);
460 
461     logger.info("\n Grid Search for Bulk Solvent Ks and Bs");
462     logger.info("             Ks       Bs   Target      R  Rfree");
463     logger.info(format(" Initial: %5.3f %8.3f %8.5f %5.3f %5.3f",
464         bulkSolventK, u2b(bulkSolventUeq), initialSum, initialR, initialRfree));
465 
466     int index = 0;
467     for (double ks = kmin; ks <= kmax; ks += kstep) {
468       for (double ku = umin; ku <= umax; ku += ustep) {
469         x[1] = ks;
470         x[2] = ku;
471         double sum = bulkSolventEnergy.energy(x);
472         double R = bulkSolventEnergy.getR();
473         double Rfree = bulkSolventEnergy.getRfree();
474         if (logger.isLoggable(Level.FINE)) {
475           logger.fine(format(" %8d %5.3f %8.3f %8.5f %5.3f %5.3f", index, ks, u2b(ku), sum, R, Rfree));
476         } else if (sum < min) {
477           logger.info(format(" %8d %5.3f %8.3f %8.5f %5.3f %5.3f", index, ks, u2b(ku), sum, R, Rfree));
478         }
479         index++;
480         if (sum < min) {
481           min = sum;
482           bulkSolventK = ks;
483           bulkSolventUeq = ku;
484         }
485       }
486     }
487     x[1] = bulkSolventK;
488     x[2] = bulkSolventUeq;
489     double sum = bulkSolventEnergy.energy(x);
490     double R = bulkSolventEnergy.getR();
491     double Rfree = bulkSolventEnergy.getRfree();
492     logger.info(format(" Minimum: %5.3f %8.3f %8.5f %5.3f %5.3f",
493         bulkSolventK, u2b(bulkSolventUeq), sum, R, Rfree));
494     if (sum < initialSum) {
495       refinementData.bulkSolventK = bulkSolventK;
496       refinementData.bulkSolventUeq = bulkSolventUeq;
497     }
498 
499     bulkSolventEnergy.setScaling(null);
500   }
501 }