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.algorithms.mc;
39  
40  import static ffx.utilities.Constants.R;
41  import static java.lang.String.format;
42  import static java.lang.System.arraycopy;
43  
44  import ffx.numerics.Potential;
45  import ffx.potential.ForceFieldEnergy;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.bonded.AminoAcidUtils;
48  import ffx.potential.bonded.Angle;
49  import ffx.potential.bonded.Atom;
50  import ffx.potential.bonded.Bond;
51  import ffx.potential.bonded.Residue;
52  import ffx.potential.bonded.AminoAcidUtils.AminoAcid3;
53  import ffx.potential.bonded.ResidueState;
54  import ffx.potential.bonded.Rotamer;
55  import ffx.potential.bonded.RotamerLibrary;
56  import ffx.potential.bonded.Torsion;
57  import ffx.potential.parameters.AtomType;
58  import ffx.potential.parsers.PDBFilter;
59  import java.io.BufferedWriter;
60  import java.io.File;
61  import java.io.FileWriter;
62  import java.io.IOException;
63  import java.util.ArrayList;
64  import java.util.HashMap;
65  import java.util.List;
66  import java.util.concurrent.ThreadLocalRandom;
67  import java.util.logging.Logger;
68  import org.apache.commons.configuration2.CompositeConfiguration;
69  import org.apache.commons.io.FilenameUtils;
70  import org.apache.commons.math3.util.FastMath;
71  
72  /**
73   * Represents a Boltzmann-drawn spin of all residue torsions for use with RosenbluthCBMC
74   * (Configurational-Bias Monte Carlo).
75   *
76   * <p>Biases each torsion by drawing a test set from a Boltzmann distribution on torsion energy.
77   * Selects from amongst each test set on Boltzmann weight of remaining energy.
78   *
79   * <p>Calculates Rosenbluth factors along the way; acceptance criterion = Wn/Wo.
80   *
81   * <p>Note: Contains much of the infrastructure necessary to generalize toward full
82   * polymer-construction-type CBMC (i.e. changing angles and bonds as well). This implementation
83   * leaves bonds/angles fixed and considers only torsion energy as dependent (Ubond in Frenkel/Smit
84   * 13.2).
85   *
86   * @author Stephen D. LuCore
87   */
88  public class RosenbluthChiAllMove implements MCMove {
89  
90    private static final Logger logger = Logger.getLogger(RosenbluthChiAllMove.class.getName());
91    private static int numAccepted = 0;
92    /** MolecularAssembly to operate on. */
93    private final MolecularAssembly molecularAssembly;
94    /** Target residue. */
95    private final Residue target;
96    /** Original residue state. */
97    private final ResidueState origState;
98    /** Force field energy to use. */
99    private final ForceFieldEnergy forceFieldEnergy;
100 
101   private final double beta;
102   private final int testSetSize;
103   private final ThreadLocalRandom rand = ThreadLocalRandom.current();
104   private final StringBuilder report = new StringBuilder();
105   private final boolean[] doChi = new boolean[4];
106   private final MODE mode;
107   private final boolean torsionSampling;
108   private final double CATASTROPHE_THRESHOLD = -10000;
109   /** Convert nanoseconds to milliseconds. */
110   private final double NS_TO_MS = 0.000001;
111   /** Final energy. */
112   double finalEnergy = 0.0;
113   /** Proposed rotamer move. */
114   private Rotamer proposedMove;
115   /** PDBFilter to write out results. */
116   private PDBFilter writer;
117   /** Write out snapshots. */
118   private SnapshotWriter snapshotWriter = null;
119 
120   private double Wn = 0.0;
121   private double Wo = 0.0;
122   private final int moveNumber;
123   private double[] proposedChis = new double[4];
124   private boolean accepted = false;
125   /** Original energy. */
126   private final double origEnergy;
127   /** Start time. */
128   private final long startTime;
129   /** End time. */
130   private long endTime;
131 
132   private final boolean verbose;
133   private final boolean randInts;
134   private final boolean noSnaps;
135   private final boolean printTestSets;
136   private final boolean logTimings;
137   private final boolean verboseEnergies;
138 
139   /**
140    * Constructor for RosenbluthChiAllMove.
141    *
142    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
143    * @param target a {@link ffx.potential.bonded.Residue} object.
144    * @param testSetSize a int.
145    * @param forceFieldEnergy a {@link ffx.potential.ForceFieldEnergy} object.
146    * @param temperature a double.
147    * @param writeSnapshots a boolean.
148    * @param moveNumber a int.
149    * @param verbose a boolean.
150    */
151   RosenbluthChiAllMove(MolecularAssembly molecularAssembly, Residue target, int testSetSize,
152       ForceFieldEnergy forceFieldEnergy, double temperature, boolean writeSnapshots, int moveNumber,
153       boolean verbose) {
154 
155     CompositeConfiguration properties = molecularAssembly.getProperties();
156     if (properties.containsKey("cbmc-type")) {
157       mode = MODE.valueOf(properties.getString("cbmc-type"));
158     } else {
159       logger.severe("CBMC: must specify a bias type.");
160       mode = MODE.CHEAP;
161     }
162 
163     this.startTime = System.nanoTime();
164     this.molecularAssembly = molecularAssembly;
165     this.target = target;
166     this.testSetSize = testSetSize;
167     this.forceFieldEnergy = forceFieldEnergy;
168     this.beta = 1 / (R * temperature);
169     this.moveNumber = moveNumber;
170     this.verbose = verbose;
171     origState = target.storeState();
172 
173     randInts = properties.getBoolean("cbmc-randInts", false);
174     noSnaps = properties.getBoolean("cbmc-noSnaps", false);
175     printTestSets = properties.getBoolean("cbmc-printTestSets", false);
176     logTimings = properties.getBoolean("cbmc-logTimings", false);
177     verboseEnergies = properties.getBoolean("cbmc-verboseEnergies", false);
178 
179     if (writeSnapshots) {
180       snapshotWriter = new SnapshotWriter(molecularAssembly, false);
181     }
182 
183     doChi[0] = properties.getBoolean("cbmc-doChi0", false);
184     doChi[1] = properties.getBoolean("cbmc-doChi1", false);
185     doChi[2] = properties.getBoolean("cbmc-doChi2", false);
186     doChi[3] = properties.getBoolean("cbmc-doChi3", false);
187     if (!doChi[0] && !doChi[1] && !doChi[2] && !doChi[3]) {
188       doChi[0] = true;
189       doChi[1] = true;
190       doChi[2] = true;
191       doChi[3] = true;
192     }
193     updateAll();
194     origEnergy = totalEnergy();
195 
196     torsionSampling = properties.getBoolean("cbmc-torsionSampler", false);
197     if (torsionSampling) {
198       logger.info(" Torsion Sampler engaged!");
199       HashMap<Integer, BackBondedList> map = createBackBondedMap(
200           AminoAcidUtils.AminoAcid3.valueOf(target.getName()));
201       List<Torsion> allTors = new ArrayList<>();
202       for (int i = 0; i < map.size(); i++) {
203         Torsion tors = map.get(i).torsion;
204         allTors.add(tors);
205       }
206       torsionSampler(allTors);
207       System.exit(0);
208     }
209     try {
210       switch (mode) {
211         case EXPENSIVE -> engageExpensive();
212         case CHEAP -> engageCheap();
213         case CTRL_ALL -> engageControlAll();
214         default -> logger.severe("CBMC: Unknown biasing type requested.");
215       }
216     } catch (ArithmeticException ex) {
217       target.revertState(origState);
218       accepted = false;
219     }
220   }
221 
222   /**
223    * Getter for the field <code>mode</code>.
224    *
225    * @return a {@link ffx.algorithms.mc.RosenbluthChiAllMove.MODE} object.
226    */
227   public MODE getMode() {
228     return mode;
229   }
230 
231   /**
232    * getWn.
233    *
234    * @return a double.
235    */
236   public double getWn() {
237     return Wn;
238   }
239 
240   /**
241    * {@inheritDoc}
242    *
243    * <p>Performs the move associated with this MCMove. Also updates chi values in associated Torsion
244    * objects.
245    */
246   @Override
247   public void move() {
248     RotamerLibrary.applyRotamer(target, proposedMove);
249     updateAll();
250   }
251 
252   /**
253    * {@inheritDoc}
254    *
255    * <p>Reverts the last applied move() call.
256    */
257   @Override
258   public void revertMove() {
259     target.revertState(origState);
260     updateAll();
261   }
262 
263   /** {@inheritDoc} */
264   @Override
265   public String toString() {
266     return format("Rosenbluth Rotamer Move:\n   Res:   %s\n   Rota: %s", target.toString(),
267         proposedMove.toString());
268   }
269 
270   private void write() {
271     if (noSnaps) {
272       return;
273     }
274     if (writer == null) {
275       writer = new PDBFilter(molecularAssembly.getFile(), molecularAssembly, null, null);
276     }
277     String filename = molecularAssembly.getFile().getAbsolutePath();
278     if (!filename.contains("_mc")) {
279       filename = FilenameUtils.removeExtension(filename) + "_mc.pdb";
280     }
281     File file = new File(filename);
282     writer.writeFile(file, false);
283   }
284 
285   /**
286    * getWo.
287    *
288    * @return a double.
289    */
290   double getWo() {
291     return Wo;
292   }
293 
294   /**
295    * This version foregoes doing a full energy eval (uExt) on each member of each chi test set.
296    * Instead, each member of the test set is a full set of chi angles, the COMBINATION of which is
297    * drawn from the Boltzmann.
298    */
299   private TrialSet cheapTorsionSet(List<Torsion> allTors, int setSize, String snapSuffix) {
300     if (printTestSets) {
301       report.append("    TrialSet_Cheap (uDep uExt)\n");
302     }
303     TrialSet trialSet = new TrialSet(setSize);
304     double[] origChi = RotamerLibrary.measureRotamer(target, false);
305     int i = 0;
306     while (i < setSize) {
307       double[] newChi = new double[origChi.length];
308       arraycopy(origChi, 0, newChi, 0, origChi.length);
309       for (int k = 0; k < origChi.length; k++) {
310         if (doChi[k]) {
311           if (randInts) {
312             newChi[k] = rand.nextInt(360) - 180;
313           } else {
314             newChi[k] = rand.nextDouble(360.0) - 180;
315           }
316         }
317       }
318       Rotamer newState = createRotamer(target, newChi);
319       RotamerLibrary.applyRotamer(target, newState);
320       double uTors = 0;
321       for (int j = 0; j < allTors.size(); j++) {
322         if (doChi[j]) {
323           uTors += allTors.get(j).energy(false);
324           double offset = 0;
325           try {
326             offset = TORSION_OFFSET_AMPRO13.valueOf(target.getName() + j).offset;
327           } catch (IllegalArgumentException ex) {
328             logger.warning(ex.getMessage());
329           }
330           uTors += offset;
331         }
332       }
333       double criterion = FastMath.exp(-beta * uTors);
334       double rng = rand.nextDouble();
335       if (rng < criterion) {
336         trialSet.theta[i] = 0.0; // this cheap version does all thetas at once
337         trialSet.rotamer[i] = newState;
338         trialSet.uDep[i] = uTors;
339         trialSet.uExt[i] = totalEnergy() - uTors;
340         i++;
341         writeSnapshot(snapSuffix, true);
342         if (printTestSets) {
343           if (i < 5 || i > setSize - 1) {
344             report.append(
345                 format("       %3s %d:      %5.2f\t%5.2f\n", snapSuffix, i, trialSet.uDep[i - 1],
346                     trialSet.uExt[i - 1]));
347           } else if (i == 5) {
348             report.append("       ...\n");
349           }
350         }
351       }
352     }
353     target.revertState(origState);
354     updateAll();
355     return trialSet;
356   }
357 
358   /**
359    * Uses the accept-reject method (F/S Algorithm46) to draw new chi values for the given torsion.
360    */
361   private TrialSet expensiveTorsionSet(Torsion tors, int chiIndex, int setSize, String snapSuffix) {
362     report.append(format("    TrialSet for Chi%d\t\t(Theta uDep uExt)\n", chiIndex));
363     TrialSet trialSet = new TrialSet(setSize);
364     double[] origChi = RotamerLibrary.measureRotamer(target, false);
365     int i = 0;
366     while (i < setSize) {
367       double theta = rand.nextDouble(360.0) - 180;
368       double[] newChi = new double[origChi.length];
369       arraycopy(origChi, 0, newChi, 0, origChi.length);
370       newChi[chiIndex] = theta;
371       Rotamer newState = createRotamer(target, newChi);
372       RotamerLibrary.applyRotamer(target, newState);
373       double uTors = tors.energy(false);
374       double offset = 0;
375       try {
376         offset = TORSION_OFFSET_AMPRO13.valueOf(target.getName() + chiIndex).offset;
377       } catch (IllegalArgumentException ex) {
378         logger.warning(ex.getMessage());
379       }
380       uTors += offset;
381       double criterion = FastMath.exp(-beta * uTors);
382       double rng = rand.nextDouble();
383       report.append(format("    prop: %5.1f %.2g %.2g %.2g\n", theta, uTors, criterion, rng));
384       if (rng < criterion) {
385         report.append("    ^ Accepted!\n");
386         writeSnapshot(snapSuffix, false);
387         trialSet.theta[i] = theta;
388         trialSet.rotamer[i] = newState;
389         trialSet.uDep[i] = uTors;
390         trialSet.uExt[i] = totalEnergy() - uTors; // Expensive!
391         i++;
392         writeSnapshot(snapSuffix, true);
393         if (i < 4 || i > setSize - 1) {
394           report.append(format("       %3s %d:      %5.2f\t%5.2f\t%5.2f\n", snapSuffix, i, theta,
395               trialSet.uDep[i - 1], trialSet.uExt[i - 1]));
396         } else if (i == 4) {
397           report.append("       ...\n");
398         }
399       }
400     }
401     target.revertState(origState);
402     updateAll();
403     return trialSet;
404   }
405 
406   /**
407    * So named, yet inadequate. Final stats on the ctrl vs bias algorithm for chis2,3 of LYS monomer
408    * are: calc "4.23846e+07 / 22422" for the biased run calc "4.21623e+06 / 10000" for the control
409    * run, where numerator = total timer; demon = accepted Possible accelerations are predicated on
410    * the fact that the test above was performed using unbinned ie fully-continuous rotamers and
411    * sampling was done with replacement. Rectifying either of these breaks balance, however, when
412    * used with MD...
413    */
414   private void engageCheap() {
415     report.append(format(" Rosenbluth CBMC Move: %4d  (%s)\n", moveNumber, target));
416 
417     AminoAcid3 name = AminoAcidUtils.AminoAcid3.valueOf(target.getName());
418     double[] chi = RotamerLibrary.measureRotamer(target, false);
419     HashMap<Integer, BackBondedList> map = createBackBondedMap(name);
420 
421     // For each chi, create a test set from Boltzmann distr on torsion energy (Ubond).
422     // Select from among this set based on Boltzmann weight of REMAINING energy (Uext).
423     // The Uext partition function of each test set (wn) becomes a factor of the overall Rosenbluth
424     // (Wn).
425     // ^ NOPE. Instead, Rosenbluth is going to be calculated just once for the whole combined set of
426     // chis.
427     List<Torsion> allTors = new ArrayList<>();
428     for (int i = 0; i < chi.length; i++) {
429       Torsion tors = map.get(i).torsion;
430       allTors.add(tors);
431     }
432     TrialSet newTrialSet = cheapTorsionSet(allTors, testSetSize, "bkn");
433     Wn = newTrialSet.sumExtBolt(); // yields uExt(1) + uExt(2) + ...
434     if (Wn <= 0) {
435       report.append("WARNING: Numerical instability in CMBC.");
436       report.append("  Test set uExt values:  ");
437       for (int i = 0; i < newTrialSet.uExt.length; i++) {
438         report.append(format("%5.2f,  ", newTrialSet.uExt[i]));
439       }
440       report.append("  Discarding move.\n");
441       target.revertState(origState);
442       Wn = -1.0;
443       Wo = 1000;
444       logger.info(report.toString());
445     }
446 
447     // Choose a proposal move from amongst this trial set (bn).
448     double rng = rand.nextDouble(Wn);
449     double running = 0.0;
450     for (int j = 0; j < newTrialSet.uExt.length; j++) {
451       double uExtBolt = FastMath.exp(-beta * newTrialSet.uExt[j]);
452       running += uExtBolt;
453       if (rng < running) {
454         proposedMove = newTrialSet.rotamer[j];
455         proposedChis = newTrialSet.theta;
456         double prob = uExtBolt / Wn * 100;
457         if (printTestSets) {
458           report.append(
459               format("       Chose %d   %7.4f\t  %4.1f%%\n", j + 1, newTrialSet.uExt[j], prob));
460         }
461         break;
462       }
463     }
464     report.append(format("    Wn Total:  %g\n", Wn));
465 
466     // Reprise the above procedure for the old configuration.
467     // The existing conformation forms first member of each test set (wo).
468     double ouDep = 0.0;
469     for (Torsion tors : allTors) {
470       ouDep += tors.energy(false); // original-conf uDep
471     }
472     double ouExt = totalEnergy() - ouDep; // original-conf uExt
473     double ouExtBolt = FastMath.exp(-beta * ouExt);
474     if (printTestSets) {
475       report.append(
476           format("       %3s %d:  %9.5g  %9.5g  %9.5g\n", "bko", 0, ouDep, ouExt, ouExtBolt));
477     }
478     writeSnapshot("bko", true);
479     TrialSet oldTrialSet = cheapTorsionSet(allTors, testSetSize - 1, "bko");
480     Wo = ouExtBolt + oldTrialSet.sumExtBolt();
481 
482     report.append(format("    Wo Total:  %11.4g\n", Wo));
483     report.append(format("    Wn/Wo:     %11.4g", Wn / Wo));
484 
485     RotamerLibrary.applyRotamer(target, proposedMove);
486     proposedChis = RotamerLibrary.measureRotamer(target, false);
487     finalEnergy = totalEnergy();
488     if (finalEnergy < CATASTROPHE_THRESHOLD) {
489       report.append("\nWARNING: Multipole catastrophe in CMBC.\n");
490       report.append("  Discarding move.\n");
491       target.revertState(origState);
492       updateAll();
493       Wn = -1.0;
494       Wo = 1000;
495       logger.info(report.toString());
496     }
497 
498     double criterion = Math.min(1, Wn / Wo);
499     rng = ThreadLocalRandom.current().nextDouble();
500     report.append(format("    rng:    %5.2f\n", rng));
501     if (rng < criterion) {
502       accepted = true;
503       numAccepted++;
504       updateAll();
505       write();
506       report.append(format(" Accepted! %5d    NewEnergy: %.4f    Chi:", numAccepted, finalEnergy));
507       for (double proposedChi : proposedChis) {
508         report.append(format(" %7.2f", proposedChi));
509       }
510       report.append("\n");
511     } else {
512       accepted = false;
513       target.revertState(origState);
514       updateAll();
515       report.append(format(" Denied.   %5d    OldEnergy: %.4f    Chi:", numAccepted, origEnergy));
516       for (double v : chi) {
517         report.append(format(" %7.2f", v));
518       }
519       report.append("\n");
520     }
521 
522     endTime = System.nanoTime();
523     double took = (endTime - startTime) * NS_TO_MS;
524     if (logTimings) {
525       report.append(format("   Timing (ms): %.2f", took));
526     }
527     logger.info(report.toString());
528   }
529 
530   /**
531    * Follows Frenkel/Smit's derivation precisely, which for AMOEBA requires SCF calls in the inner
532    * loops.
533    */
534   private void engageExpensive() {
535     report.append(format(" Rosenbluth CBMC Move: %4d  %s\n", moveNumber, target));
536 
537     AminoAcid3 name = AminoAcidUtils.AminoAcid3.valueOf(target.getName());
538     double[] chi = RotamerLibrary.measureRotamer(target, false);
539     HashMap<Integer, BackBondedList> map = createBackBondedMap(name);
540 
541     // For each chi, create a test set from Boltzmann distr on torsion energy (Ubond).
542     // Select from among this set based on Boltzmann weight of REMAINING energy (Uext).
543     // The Uext partition function of each test set (wn) becomes a factor of the overall Rosenbluth
544     // (Wn).
545     double[] wn = new double[chi.length]; // factors of Wn
546     double[] finalChi = new double[chi.length];
547     for (int i = 0; i < chi.length; i++) {
548       Torsion tors = map.get(i).torsion;
549       //            TrialSet trialSet = boltzmannTorsionSet(tors, i, testSetSize, "bkn");
550       TrialSet trialSet = expensiveTorsionSet(tors, i, testSetSize, "bkn");
551       wn[i] = trialSet.sumExtBolt();
552       if (i == 0) {
553         Wn = wn[i];
554       } else {
555         Wn *= wn[i];
556       }
557       report.append(format(" wn,W running: %.2g %.2g", wn[i], Wn));
558       logger.info(report.toString());
559 
560       // Choose a proposal move from amongst this trial set (bn).
561       double rng = rand.nextDouble(wn[i]);
562       double running = 0.0;
563       for (int j = 0; j < trialSet.uExt.length; j++) {
564         double uExtBolt = FastMath.exp(-beta * trialSet.uExt[j]);
565         running += uExtBolt;
566         if (rng < running) {
567           finalChi[i] = trialSet.theta[j]; // Yes, I mean i then j.
568           double prob = uExtBolt / wn[i] * 100;
569           report.append(
570               format("       Chose %d   %7.4f\t%7.4f\t  %4.1f%%\n", j, trialSet.uExt[j], uExtBolt,
571                   prob));
572           break;
573         }
574       }
575     }
576     report.append(format("    Wn Total:  %g\n", Wn));
577     proposedMove = createRotamer(name, finalChi);
578 
579     // Reprise the above procedure for the old configuration.
580     // The existing conformation forms first member of each test set (wo).
581     // Overall Rosenbluth Wo is product of uExt partition functions.
582     double[] wo = new double[chi.length]; // factors of Wo
583     for (int i = 0; i < chi.length; i++) {
584       Torsion tors = map.get(i).torsion;
585       double ouDep = tors.energy(false); // original-conf uDep
586       double ouExt = totalEnergy() - ouDep; // original-conf uExt
587       double ouExtBolt = FastMath.exp(-beta * ouExt);
588       TrialSet trialSet = expensiveTorsionSet(tors, i, testSetSize - 1, "bko");
589       wo[i] = ouExtBolt + trialSet.sumExtBolt();
590       if (i == 0) {
591         Wo = wo[i];
592       } else {
593         Wo *= wo[i];
594       }
595     }
596     report.append(format("    Wo Total:  %g\n", Wo));
597     report.append(format("    Wn/Wo:     %g\n", Wn / Wo));
598 
599     target.revertState(origState);
600     updateAll();
601     if (verbose) {
602       logger.info(report.toString());
603     }
604   }
605 
606   /**
607    * For validation. Performs Monte Carlo chi moves WITHOUT biasing. Give ALL CHIs a random theta
608    * simultaneously. Accept on the vanilla Metropolis criterion.
609    */
610   private void engageControlAll() {
611     report.append(format(" Rosenbluth Control Move: %4d  %s\n", moveNumber, target));
612     double origEnergy = totalEnergy();
613     double[] origChi = RotamerLibrary.measureRotamer(target, false);
614     double[] newChi = new double[origChi.length];
615     arraycopy(origChi, 0, newChi, 0, origChi.length);
616     for (int i = 0; i < origChi.length; i++) {
617       if (doChi[i]) {
618         double theta = rand.nextDouble(360.0) - 180;
619         newChi[i] = theta;
620       }
621     }
622     proposedChis = newChi;
623     Rotamer newState = createRotamer(target, newChi);
624     RotamerLibrary.applyRotamer(target, newState);
625 
626     finalEnergy = totalEnergy();
627     if (this.finalEnergy < CATASTROPHE_THRESHOLD) {
628       report.append("\nWARNING: Multipole catastrophe in CBMC.\n");
629       report.append("  Discarding move.\n");
630       target.revertState(origState);
631       updateAll();
632       Wn = -1.0;
633       Wo = 1000;
634       logger.info(report.toString());
635     }
636 
637     double dU = finalEnergy - origEnergy;
638     double criterion = FastMath.exp(-beta * dU);
639     double rng = rand.nextDouble();
640     report.append("    move (thetas):    ");
641     for (double value : newChi) {
642       report.append(format("%7.2f ", value));
643     }
644     report.append("\n");
645     report.append(format("    orig, final, dU:  %.2g %.2g %.2g\n", origEnergy, finalEnergy, dU));
646     report.append(format("    crit, rng:        %.2g %.2g\n", criterion, rng));
647     if (rng < criterion) {
648       accepted = true;
649       numAccepted++;
650       report.append(format(" Accepted! %5d    NewEnergy: %.4f    Chi:", numAccepted, finalEnergy));
651       for (double proposedChi : proposedChis) {
652         report.append(format(" %7.2f", proposedChi));
653       }
654       report.append("\n");
655       updateAll();
656       if (!noSnaps) {
657         PDBFilter writer = new PDBFilter(molecularAssembly.getFile(), molecularAssembly, null, null);
658         String filename = molecularAssembly.getFile().getAbsolutePath();
659         if (!filename.contains("_mc")) {
660           filename = FilenameUtils.removeExtension(filename) + "_mc.pdb";
661         }
662         File file = new File(filename);
663         writer.writeFile(file, false);
664       }
665     } else {
666       accepted = false;
667       report.append(format(" Denied.   %5d    NewEnergy: %.4f    Chi:", numAccepted, origEnergy));
668       for (double v : origChi) {
669         report.append(format(" %7.2f", v));
670       }
671       report.append("\n");
672       target.revertState(origState);
673     }
674 
675     updateAll();
676     endTime = System.nanoTime();
677     double took = (endTime - startTime) * NS_TO_MS;
678     if (logTimings) {
679       report.append(format("   Timing (ms): %.2f", took));
680     }
681     logger.info(report.toString());
682   }
683 
684   /**
685    * wasAccepted.
686    *
687    * @return a boolean.
688    */
689   boolean wasAccepted() {
690     return accepted;
691   }
692 
693   private Rotamer createRotamer(AminoAcid3 name, double[] chi) {
694     // Need to add sigma values to construct a new Rotamer with these chis.
695     double[] values = new double[chi.length * 2];
696     for (int k = 0; k < chi.length; k++) {
697       int kk = 2 * k;
698       values[kk] = chi[k];
699       values[kk + 1] = 0.0;
700     }
701     return new Rotamer(name, values);
702   }
703 
704   private Rotamer createRotamer(Residue res, double[] chi) {
705     return createRotamer(AminoAcidUtils.AminoAcid3.valueOf(res.getName()), chi);
706   }
707 
708   private void updateAll() {
709     updateBonds();
710     updateAngles();
711     updateTorsions();
712   }
713 
714   private void updateBonds() {
715     for (Bond bond : target.getBondList()) {
716       bond.update();
717     }
718   }
719 
720   private void updateAngles() {
721     for (Angle angle : target.getAngleList()) {
722       angle.update();
723     }
724   }
725 
726   private void updateTorsions() {
727     for (Torsion torsion : target.getTorsionList()) {
728       torsion.update();
729     }
730   }
731 
732   private double totalEnergy() {
733     double[] x = new double[forceFieldEnergy.getNumberOfVariables() * 3];
734     forceFieldEnergy.setEnergyTermState(Potential.STATE.BOTH);
735     forceFieldEnergy.getCoordinates(x);
736     return forceFieldEnergy.energy(false, verboseEnergies);
737   }
738 
739   /**
740    * Maps the back-bonded terms affected by key atoms in an amino acid. Here, 'key atom' refers to
741    * each new rotamer-torsion-completing atom. e.g. VAL has 1 key atom (CG1), ARG has 4 key atoms
742    * (CG,CD,NE,CZ). 'Back-bonded' means we only map terms that lead toward the backbone.
743    */
744   private HashMap<Integer, BackBondedList> createBackBondedMap(AminoAcid3 name) {
745     HashMap<Integer, BackBondedList> map = new HashMap<>();
746     List<Atom> chain = new ArrayList<>();
747     Atom N = (Atom) target.getAtomNode("N");
748     Atom CA = (Atom) target.getAtomNode("CA");
749     Atom CB = (Atom) target.getAtomNode("CB");
750     List<Atom> keyAtoms = new ArrayList<>();
751     switch (name) {
752       case VAL -> {
753         Atom CG1 = (Atom) target.getAtomNode("CG1");
754         keyAtoms.add(CG1);
755         keyAtoms.add(CB);
756       }
757       case LEU -> {
758         Atom CG = (Atom) target.getAtomNode("CG");
759         Atom CD1 = (Atom) target.getAtomNode("CD1");
760         keyAtoms.add(CG);
761         keyAtoms.add(CD1);
762       }
763       case ILE -> {
764         Atom CD1 = (Atom) target.getAtomNode("CD1");
765         Atom CG1 = (Atom) target.getAtomNode("CG1");
766         keyAtoms.add(CD1);
767         keyAtoms.add(CG1);
768       }
769       case SER -> {
770         Atom OG = (Atom) target.getAtomNode("OG");
771         Atom HG = (Atom) target.getAtomNode("HG");
772         keyAtoms.add(OG);
773         keyAtoms.add(HG);
774       }
775       case THR -> {
776         Atom OG1 = (Atom) target.getAtomNode("OG1");
777         Atom HG1 = (Atom) target.getAtomNode("HG1");
778         keyAtoms.add(OG1);
779         keyAtoms.add(HG1);
780       }
781       case CYX, CYD -> {
782         Atom SG = (Atom) target.getAtomNode("SG");
783         keyAtoms.add(SG);
784       }
785       case PHE -> {
786         Atom CD1 = (Atom) target.getAtomNode("CD1");
787         Atom CG = (Atom) target.getAtomNode("CG");
788         keyAtoms.add(CG);
789       }
790       case PRO -> {
791         // Not allowed yet.
792         Atom CD = (Atom) target.getAtomNode("CD");
793         Atom CG = (Atom) target.getAtomNode("CG");
794         keyAtoms.add(CG);
795         keyAtoms.add(CD);
796       }
797       case TYR -> {
798         Atom CD1 = (Atom) target.getAtomNode("CD1");
799         Atom CE2 = (Atom) target.getAtomNode("CE2");
800         Atom CG = (Atom) target.getAtomNode("CG");
801         Atom CZ = (Atom) target.getAtomNode("CZ");
802         Atom OH = (Atom) target.getAtomNode("OH");
803         Atom HH = (Atom) target.getAtomNode("HH");
804         // SPECIAL CASE: have to create map manualy.
805         Bond b1 = CG.getBond(CB);
806         Angle a1 = CG.getAngle(CB, CA);
807         Torsion t1 = CG.getTorsion(CB, CA, N);
808         Bond b2 = CD1.getBond(CG);
809         Angle a2 = CD1.getAngle(CG, CB);
810         Torsion t2 = CD1.getTorsion(CG, CB, CA);
811         Bond b3 = HH.getBond(OH);
812         Angle a3 = HH.getAngle(OH, CZ);
813         Torsion t3 = HH.getTorsion(OH, CZ, CE2);
814         BackBondedList bbl1 = new BackBondedList(b1, a1, t1);
815         BackBondedList bbl2 = new BackBondedList(b2, a2, t2);
816         BackBondedList bbl3 = new BackBondedList(b3, a3, t3);
817         map.put(0, bbl1);
818         map.put(1, bbl2);
819         map.put(2, bbl3);
820         return map; // Note the return here.
821       }
822       case TYD, TRP -> {
823         Atom CD1 = (Atom) target.getAtomNode("CD1");
824         Atom CG = (Atom) target.getAtomNode("CG");
825         keyAtoms.add(CG);
826         keyAtoms.add(CD1);
827       }
828       case HIS, HID, HIE -> {
829         Atom CG = (Atom) target.getAtomNode("CG");
830         Atom ND1 = (Atom) target.getAtomNode("ND1");
831         keyAtoms.add(CG);
832         keyAtoms.add(ND1);
833       }
834       case ASP -> {
835         Atom CG = (Atom) target.getAtomNode("CG");
836         keyAtoms.add(CG);
837       }
838       case ASH, ASN -> {
839         Atom CG = (Atom) target.getAtomNode("CG");
840         Atom OD1 = (Atom) target.getAtomNode("OD1");
841         keyAtoms.add(CG);
842         keyAtoms.add(OD1);
843       }
844       case GLU, GLH, GLN -> {
845         Atom CG = (Atom) target.getAtomNode("CG");
846         Atom CD = (Atom) target.getAtomNode("CD");
847         Atom OE1 = (Atom) target.getAtomNode("OE1");
848         keyAtoms.add(CG);
849         keyAtoms.add(CD);
850         keyAtoms.add(OE1);
851       }
852       case MET -> {
853         Atom CG = (Atom) target.getAtomNode("CG");
854         Atom CE = (Atom) target.getAtomNode("CE");
855         Atom SD = (Atom) target.getAtomNode("SD");
856         keyAtoms.add(CG);
857         keyAtoms.add(SD);
858         keyAtoms.add(CE);
859       }
860       case LYS, LYD -> {
861         Atom CD = (Atom) target.getAtomNode("CD");
862         Atom CE = (Atom) target.getAtomNode("CE");
863         Atom CG = (Atom) target.getAtomNode("CG");
864         Atom NZ = (Atom) target.getAtomNode("NZ");
865         keyAtoms.add(CG);
866         keyAtoms.add(CD);
867         keyAtoms.add(CE);
868         keyAtoms.add(NZ);
869       }
870       case ARG -> {
871         Atom CD = (Atom) target.getAtomNode("CD");
872         Atom CG = (Atom) target.getAtomNode("CG");
873         Atom CZ = (Atom) target.getAtomNode("CZ");
874         Atom NE = (Atom) target.getAtomNode("NE");
875         keyAtoms.add(CG);
876         keyAtoms.add(CD);
877         keyAtoms.add(NE);
878         keyAtoms.add(CZ);
879       }
880       default -> logger.severe(format("CBMC called on unsupported residue: %s", name));
881     }
882     // Build the chain and assign back-bonded terms.
883     chain.add(N);
884     chain.add(CA);
885     chain.add(CB);
886     chain.addAll(keyAtoms);
887     for (int i = 3; i < chain.size(); i++) {
888       Atom key = chain.get(i);
889       Bond bond = key.getBond(chain.get(i - 1));
890       Angle angle = key.getAngle(chain.get(i - 1), chain.get(i - 2));
891       Torsion torsion = key.getTorsion(chain.get(i - 1), chain.get(i - 2), chain.get(i - 3));
892       BackBondedList bbl = new BackBondedList(bond, angle, torsion);
893       map.put(i - 3, bbl);
894       //            report.append(String.format("    BackBondedMap: %s\t\t%s\n", key, torsion));
895     }
896     return map;
897   }
898 
899   private void writeSnapshot(String suffix, boolean append) {
900     if (snapshotWriter != null && !noSnaps) {
901       snapshotWriter.write(suffix, append);
902     }
903   }
904 
905   /**
906    * Collects data for plot of uTors vs. theta. This is for finding the appropriate offset to add to
907    * each uTors such that the minimum is zero.
908    *
909    * @param allTors All torsions.
910    */
911   private void torsionSampler(List<Torsion> allTors) {
912     updateAll();
913     double[] origChi = measureLysine(target, false);
914     StringBuilder sb = new StringBuilder();
915     sb.append(format(" Torsion Sampling for %s\n", target.getName()));
916     sb.append("   origChi:");
917     for (double value : origChi) {
918       sb.append(format(" %6.2f", value));
919     }
920     sb.append("\n");
921     for (int k = 0; k < origChi.length; k++) {
922       Torsion tors = allTors.get(k);
923       AtomType type1 = tors.getAtomArray()[0].getAtomType();
924       AtomType type2 = tors.getAtomArray()[1].getAtomType();
925       AtomType type3 = tors.getAtomArray()[2].getAtomType();
926       AtomType type4 = tors.getAtomArray()[3].getAtomType();
927       sb.append(
928           format("   %d:    \"(%3d %3d %3s)  (%3d %3d %3s)  (%3d %3d %3s)  (%3d %3d %3s)\"\n", k,
929               type1.type, type1.atomClass, type1.name, type2.type, type2.atomClass, type2.name,
930               type3.type, type3.atomClass, type3.name, type4.type, type4.atomClass, type4.name));
931     }
932     logger.info(sb.toString());
933     sb = new StringBuilder();
934     sb.append(" Resi chi0 chi1 chi2 chi3 List<uTors> uTorsSum uTotal\n");
935     logger.info(sb.toString());
936     sb = new StringBuilder();
937     boolean doChi0 = false, doChi1 = false;
938     boolean doChi2 = true, doChi3 = true;
939     double increment = 1.0;
940     for (double chi0 = -180.0; chi0 < 180.0; chi0 += increment) {
941       for (double chi1 = -180.0; chi1 <= 180.0; chi1 += increment) {
942         for (double chi2 = -180.0; chi2 <= 180.0; chi2 += increment) {
943           for (double chi3 = -180.0; chi3 <= 180.0; chi3 += increment) {
944             sb.append(format(" %3s", target.getName()));
945             double[] newChi = new double[4];
946             if (doChi0) {
947               newChi[0] = chi0;
948             } else {
949               newChi[0] = origChi[0];
950             }
951             if (doChi1) {
952               newChi[1] = chi1;
953             } else {
954               newChi[1] = origChi[1];
955             }
956             if (doChi2) {
957               newChi[2] = chi2;
958             } else {
959               newChi[2] = origChi[2];
960             }
961             if (doChi3) {
962               newChi[3] = chi3;
963             } else {
964               newChi[3] = origChi[3];
965             }
966             Rotamer newState = createRotamer(target, newChi);
967             RotamerLibrary.applyRotamer(target, newState);
968             for (int wut = 0; wut < origChi.length; wut++) {
969               sb.append(format(" %3.0f", newChi[wut]));
970             }
971             writeSnapshot(format("%.0f", chi1), false);
972             double uTorsSum = 0.0;
973             for (Torsion allTor : allTors) {
974               double uTors = allTor.energy(false);
975               sb.append(format(" %5.2f", uTors));
976               uTorsSum += uTors;
977             }
978             sb.append(format(" %5.2f", uTorsSum));
979             double totalE = 1000.0;
980             try {
981               totalE = totalEnergy();
982             } catch (Exception ex) {
983               //
984             }
985             if (totalE > 1000.0) {
986               totalE = 1000.0;
987             }
988             sb.append(format(" %10.4f", totalE));
989             sb.append("\n");
990             logger.info(sb.toString());
991             sb = new StringBuilder();
992             if (!doChi3) {
993               break;
994             }
995           }
996           if (!doChi2) {
997             break;
998           }
999         }
1000         if (!doChi1) {
1001           break;
1002         }
1003       }
1004       if (!doChi0) {
1005         break;
1006       }
1007     }
1008 
1009     if (torsionSampling) {
1010       try {
1011         File output = new File("torsionSampler.log");
1012         BufferedWriter bw = new BufferedWriter(new FileWriter(output));
1013         bw.write(sb.toString());
1014         bw.close();
1015       } catch (IOException ex) {
1016         //
1017       }
1018     }
1019     System.exit(0);
1020   }
1021 
1022   /**
1023    * measureLysine.
1024    *
1025    * @param residue a {@link ffx.potential.bonded.Residue} object.
1026    * @param print a boolean.
1027    * @return an array of {@link double} objects.
1028    */
1029   private double[] measureLysine(Residue residue, boolean print) {
1030     if (!residue.getName().contains("LY") || (
1031         residue.getAminoAcid3() != AminoAcidUtils.AminoAcid3.LYS
1032             && residue.getAminoAcid3() != AminoAcidUtils.AminoAcid3.LYD)) {
1033       logger.severe("Yeah that ain't a lysine.");
1034     }
1035     double[] chi = new double[4];
1036     List<Torsion> torsions = residue.getTorsionList();
1037     Atom N = (Atom) residue.getAtomNode("N");
1038     Atom CA = (Atom) residue.getAtomNode("CA");
1039     Atom CB = (Atom) residue.getAtomNode("CB");
1040     Atom CD = (Atom) residue.getAtomNode("CD");
1041     Atom CE = (Atom) residue.getAtomNode("CE");
1042     Atom CG = (Atom) residue.getAtomNode("CG");
1043     Atom NZ = (Atom) residue.getAtomNode("NZ");
1044     logger.info(
1045         format(" Here's the atoms I found: \n%s\n%s\n%s\n%s\n%s\n%s\n%s", N, CA, CB, CD, CE, CG,
1046             NZ));
1047     logger.info(format(" Num torsions: %d", torsions.size()));
1048     int count = 0;
1049     for (Torsion torsion : torsions) {
1050       torsion.energy(false);
1051       logger.info(format(" Torsion numba %d: %s", count++, torsion));
1052       if (torsion.compare(N, CA, CB, CG)) {
1053         chi[0] = torsion.getValue();
1054         if (print) {
1055           logger.info(torsion.toString());
1056         }
1057       }
1058       if (torsion.compare(CA, CB, CG, CD)) {
1059         chi[1] = torsion.getValue();
1060         if (print) {
1061           logger.info(torsion.toString());
1062         }
1063       }
1064       if (torsion.compare(CB, CG, CD, CE)) {
1065         chi[2] = torsion.getValue();
1066         if (print) {
1067           logger.info(torsion.toString());
1068         }
1069       }
1070       if (torsion.compare(CG, CD, CE, NZ)) {
1071         chi[3] = torsion.getValue();
1072         if (print) {
1073           logger.info(torsion.toString());
1074         }
1075       }
1076     }
1077     return chi;
1078   }
1079 
1080   /** Mode of the RosenbluthChiAllMove instance. */
1081   public enum MODE {
1082     EXPENSIVE, CHEAP, CTRL_ALL
1083   }
1084 
1085   /** Provides lookup values that make true the inequality: uTorsion + offset .ge. 0.0 */
1086   private enum TORSION_OFFSET_AMPRO13 {
1087     LYS0(1.610000), LYD0(1.610000), LYS1(0.939033), LYD1(0.939033), LYS2(1.000000), LYD2(
1088         1.000000), LYS3(0.800000), LYD3(0.800000);
1089 
1090     public final double offset;
1091 
1092     TORSION_OFFSET_AMPRO13(double offset) {
1093       this.offset = offset;
1094     }
1095   }
1096 
1097   private static class BackBondedList {
1098 
1099     public final Bond bond;
1100     public final Angle angle;
1101     public final Torsion torsion;
1102 
1103     public BackBondedList(Bond bond, Angle angle, Torsion tors) {
1104       this.bond = bond;
1105       this.angle = angle;
1106       this.torsion = tors;
1107     }
1108   }
1109 
1110   private class TrialSet {
1111 
1112     public final Rotamer[] rotamer;
1113     public final double[] uDep;
1114     public final double[] uExt;
1115     public final double[] theta;
1116 
1117     public TrialSet(int setSize) {
1118       rotamer = new Rotamer[setSize];
1119       uDep = new double[setSize];
1120       uExt = new double[setSize];
1121       theta = new double[setSize];
1122     }
1123 
1124     public double prodExtBolt() {
1125       double prod = 0.0;
1126       for (double v : uExt) {
1127         prod *= FastMath.exp(-beta * v);
1128       }
1129       return prod;
1130     }
1131 
1132     // We need to SUM over "members of the test set" i.e. ONLY do this for different TRIALS (b1 ...
1133     // bn)
1134     // AND THEN We want to MULTIPLY over "segments in the polymer chain" i.e. ONLY do prod over
1135     // different CHIS
1136     public double sumExtBolt() {
1137       double sum = 0.0;
1138       for (double v : uExt) {
1139         sum += FastMath.exp(-beta * v);
1140       }
1141       return sum;
1142     }
1143   }
1144 
1145   private class SnapshotWriter {
1146 
1147     private final MolecularAssembly mola;
1148     private final PDBFilter filter;
1149     private final boolean interleaving;
1150 
1151     private SnapshotWriter(MolecularAssembly mola, boolean interleaving) {
1152       this.mola = mola;
1153       this.interleaving = interleaving;
1154       this.filter = new PDBFilter(mola.getFile(), mola, null, null);
1155       this.filter.setLogWrites(false);
1156     }
1157 
1158     private void write(String suffix, boolean append) {
1159       String filename =
1160           FilenameUtils.removeExtension(mola.getFile().toString()) + "." + suffix + "-" + moveNumber;
1161       if (interleaving) {
1162         filename = mola.getFile().getAbsolutePath();
1163         if (!filename.contains("dyn")) {
1164           filename = FilenameUtils.removeExtension(filename) + "_dyn.pdb";
1165         }
1166       }
1167       File file = new File(filename);
1168       filter.setLogWrites(false);
1169       filter.writeFile(file, append);
1170     }
1171   }
1172 }