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