View Javadoc
1   package ffx.algorithms.optimize;
2   
3   import com.google.common.collect.MinMaxPriorityQueue;
4   import ffx.algorithms.AlgorithmListener;
5   import ffx.potential.AssemblyState;
6   import ffx.potential.ForceFieldEnergy;
7   import ffx.potential.MolecularAssembly;
8   import ffx.potential.bonded.Atom;
9   import ffx.potential.bonded.Bond;
10  import ffx.potential.bonded.Molecule;
11  import ffx.potential.bonded.RestrainDistance;
12  import ffx.potential.nonbonded.pme.Polarization;
13  import ffx.potential.parameters.BondType;
14  import ffx.potential.parsers.XYZFilter;
15  
16  import java.io.File;
17  import java.util.AbstractQueue;
18  import java.util.ArrayList;
19  import java.util.concurrent.ArrayBlockingQueue;
20  import java.util.logging.Logger;
21  
22  import static ffx.potential.utils.Superpose.applyRotation;
23  import static java.lang.String.format;
24  import static org.apache.commons.math3.util.FastMath.cos;
25  import static org.apache.commons.math3.util.FastMath.sin;
26  
27  /**
28   * This class is for a configuration optimization of two small systems. The search
29   * consists of aligning heavy (except carbon) and hydrogen atoms at a h-bond distance
30   * with their center of masses behind them all on the z-axis in hopes that a global
31   * conformational minima will be found by allowing for strong h-bonds to occur. After
32   * aligning, an optional static torsion scan and NONE,DIRECT,MUTUAL minimizations are
33   * performed. A binding energy can be calculated with this by:
34   *
35   * ddEbind = dEbind (of two systems)
36   *         - dEbind (of one system w/ itself)
37   *         - dEbind (of the other system w/ itself)
38   */
39  public class ConformationScan {
40  
41      private static final Logger logger = Logger.getLogger(ConformationScan.class.getName());
42  
43      private final MolecularAssembly mola;
44      private final ForceFieldEnergy forceFieldEnergy;
45      private final AssemblyState initState;
46      private double[] x;
47      private AlgorithmListener algorithmListener;
48  
49      private final Molecule[] s1;
50      private final Molecule[] s2;
51      private final Atom[] s1Atoms;
52      private final Atom[] s2Atoms;
53      private final ArrayList<Atom> s1TargetAtoms;
54      private final ArrayList<Atom> s2TargetAtoms;
55  
56      private AbstractQueue<StateContainer> statesQueue;
57  
58      private final double eps;
59      private final int maxIter;
60      private double hBondDist;
61      private double flatBottomRadius;
62      private final boolean tScan;
63      private final boolean excludeH;
64      private final boolean minimize;
65  
66      private double m1MinEnergy;
67      private double m2MinEnergy;
68      private double totalMonomerMinimizedEnergy;
69  
70      private double minimumEnergy;
71      private double averageEnergy;
72      private double averageEnergyNoOutlier;
73      private double stdOfEnergies;
74      private double stdOfEnergiesNoOutlier;
75  
76      public ConformationScan(
77              MolecularAssembly molecularAssembly,
78              Molecule[] systemOne,
79              Molecule[] systemTwo,
80              double minimizeEps,
81              int minimizeMaxIterations,
82              boolean staticTorsionScan,
83              boolean excludeExtraHydrogen,
84              boolean minimize
85      )
86      {
87          mola = molecularAssembly;
88          forceFieldEnergy = molecularAssembly.getPotentialEnergy();
89          initState = new AssemblyState(mola);
90          s1 = systemOne;
91          s2 = systemTwo;
92          eps = minimizeEps;
93          maxIter = minimizeMaxIterations;
94          tScan = staticTorsionScan;
95          excludeH = excludeExtraHydrogen;
96          this.minimize = minimize;
97          s1Atoms = getAtomsFromMoleculeArray(s1);
98          s2Atoms = getAtomsFromMoleculeArray(s2);
99          s1TargetAtoms = new ArrayList<>();
100         s2TargetAtoms = new ArrayList<>();
101         setTargetAtoms(mola.getAtomArray()); // TODO: Adjust this to handle systems not just molecules
102         statesQueue = MinMaxPriorityQueue.maximumSize(s1TargetAtoms.size() * s2TargetAtoms.size())
103                 .create();
104         if(!minimize) { // If minimization is not performed, then the queue will be in order of the loop
105             statesQueue = new ArrayBlockingQueue<>(s1TargetAtoms.size() * s2TargetAtoms.size() + 1);
106         }
107     }
108 
109     public void scan(){
110         systemEnergies(); // get energy of each system by itself for binding e calculation
111         double[] zAxis = new double[]{0,0,1};
112         // Loop through interactions between the two molecules --> Not necessarily symmetric but close?
113         int loopCounter = 1;
114         for(Atom a: s1TargetAtoms){
115             for(Atom b: s2TargetAtoms){
116                 zAxis[2] = 1;
117                 initState.revertState(); // all trials need to be initialized from the initial states
118                 alignSystemCOMtoAtomVecWithAxis(a, zAxis, s1Atoms);
119                 zAxis[2] = -1;
120                 logger.info("\n ----- Trial " + loopCounter + " out of " +
121                         (s2TargetAtoms.size() * s1TargetAtoms.size()) + " -----");
122                 loopCounter++;
123                 alignSystemCOMtoAtomVecWithAxis(b, zAxis, s2Atoms);
124                 // Move the second molecule to the perfect h-bond distance
125                 hBondDist = 2.0;
126                 double[] hBondVector = new double[]{0, 0, a.getZ() - b.getZ() + hBondDist};
127                 logger.info(" Initial H-bond distance: " + hBondVector[2]);
128                 // Finds and moves to minimial vector (no polarization to avoid failures)
129                 forceFieldEnergy.getPmeNode().setPolarization(Polarization.NONE);
130                 hBondVector[2] += minimizeVector(hBondVector, -15, 15)[2];
131                 forceFieldEnergy.getPmeNode().setPolarization(Polarization.MUTUAL);
132                 logger.info(" Best H-bond distance: " + hBondVector[2]);
133                 hBondDist = hBondVector[2] - (a.getZ() - b.getZ());
134                 flatBottomRadius = Math.abs(hBondDist / 2.0);
135                 logger.info(" Flat bottom radius: " + flatBottomRadius);
136                 // Minimize the energy of the system subject to a harmonic restraint on the distance
137                 // between the two atoms. Keep the state if minimization works.
138                 try {
139                     mola.update();
140                     forceFieldEnergy.getCoordinates(x);
141                     int status = 0;
142                     if(minimize) {
143                         status = minimizeSystem(a, b);
144                     }
145                     if(status != -1) {
146                         forceFieldEnergy.getCoordinates(x);
147                         double e = forceFieldEnergy.energy(x, true) - totalMonomerMinimizedEnergy;
148                         logger.info("\n Binding energy of trial " + (loopCounter - 1) + ": " + e);
149                         statesQueue.add(new StateContainer(new AssemblyState(mola), e));
150                     } else {
151                         logger.warning(" Minimization failed. No state will be saved.");
152                         //statesQueue.add(new StateContainer(new AssemblyState(mola), -1)); Use for debugging
153                     }
154                 } catch (Exception ignored) {
155                     logger.warning(" Minimization failed. No state will be saved.");
156                     //statesQueue.add(new StateContainer(new AssemblyState(mola), -1)); Use for debugging
157                     //e.printStackTrace()
158                 }
159             }
160         }
161         logger.info("\n ------------------------- End of Trials -------------------------");
162         if(statesQueue.peek() != null) {
163             minimumEnergy = statesQueue.peek().getEnergy();
164         } else{
165             logger.warning(" No states were saved. No minimum energy found.");
166             minimumEnergy = Double.NaN;
167             return;
168         }
169         calculateMeanStd();
170     }
171 
172     public void logAllEnergyInformation(){
173         logger.info(format(" Minimum energy of monomer 1:                 %12.5f", m1MinEnergy));
174         logger.info(format(" Minimum energy of monomer 2:                 %12.5f", m2MinEnergy));
175         logger.info(format(" Sum of minimum monomers:                     %12.5f", totalMonomerMinimizedEnergy));
176         logger.info(format(" Minimum binding energy of system:            %12.5f", minimumEnergy));
177         logger.info(format(" Average binding energy:                      %12.5f +- %6.3f", averageEnergy, stdOfEnergies));
178         logger.info(format(" Average binding energy (no outlier):         %12.5f +- %6.3f", averageEnergyNoOutlier, stdOfEnergiesNoOutlier));
179     }
180     public boolean writeStructuresToXYZ(File outputFile){
181         XYZFilter xyzFilter = new XYZFilter(outputFile, mola, mola.getForceField(), mola.getForceField().getProperties());
182         logger.info("\n Writing structures to " + outputFile.getAbsolutePath());
183         int count = 1;
184         StateContainer[] states = new StateContainer[statesQueue.size()];
185         if(minimize){
186             // Pull states out in order
187             int index = 0;
188             while(!statesQueue.isEmpty() && index < states.length){
189                 states[index] = statesQueue.poll();
190                 index++;
191             }
192             if(index < states.length){
193                 logger.warning(" Not all states will be saved. ");
194                 return false;
195             }
196         }
197         else{
198             statesQueue.toArray(states);
199         }
200         if(states.length == 0){
201             logger.warning(" No states were saved. No structures will be written.");
202             return false;
203         }
204         for(StateContainer s: states){
205             AssemblyState state = s.getState();
206             double energy = s.getEnergy();
207             logger.info(" Writing configuration #" + count + " with energy: " + energy);
208             state.revertState();
209             xyzFilter.writeFile(outputFile, count != 1);
210             count++;
211         }
212         return true;
213     }
214 
215     public static ArrayList<Double> logBindingEnergyCalculation(ConformationScan m1, ConformationScan m2, ConformationScan dimer){
216         ArrayList<Double> bindingEnergies = new ArrayList<>();
217         logger.info("\n ------------------------- Binding Energy -------------------------");
218         logger.info(" ddE (of binding) = dE (of coformer dimer binding) - dE (average of both monomer dimer bindings summed)");
219         logger.info("\n Calculation using minimized energies: ");
220         double averageMonomerEnergy = (m1.getMinimumEnergy() + m2.getMinimumEnergy()) / 2;
221         double bindingE = dimer.getMinimumEnergy() - averageMonomerEnergy;
222         logger.info(format(" Minimal Structure    ddE = %8.3f - %8.3f = %12.5f",
223                 dimer.getMinimumEnergy(),
224                 averageMonomerEnergy,
225                 bindingE));
226         bindingEnergies.add(bindingE);
227         logger.info("\n Calculation using average energies: ");
228         averageMonomerEnergy = (m1.getAverageEnergy() + m2.getAverageEnergy()) / 2;
229         bindingE = dimer.getAverageEnergy() - averageMonomerEnergy;
230         logger.info(format(" Average              ddE = %8.3f - %8.3f = %12.5f",
231                 dimer.getAverageEnergy(),
232                 averageMonomerEnergy,
233                 bindingE));
234         bindingEnergies.add(bindingE);
235         logger.info("\n Calculation using average energies (no outliers): ");
236         averageMonomerEnergy = (m1.getAverageEnergyNoOutlier() + m2.getAverageEnergyNoOutlier()) / 2;
237         bindingE = dimer.getAverageEnergyNoOutlier() - averageMonomerEnergy;
238         logger.info(format(" Average (no outlier) ddE = %8.3f - %8.3f = %12.5f",
239                 dimer.getAverageEnergyNoOutlier(),
240                 averageMonomerEnergy,
241                 bindingE));
242         bindingEnergies.add(bindingE);
243         logger.info("\n N.B. -- Monomer energies are calculated using the minimized structures in all cases.");
244         logger.info(" ------------------------- End of Binding Energy -------------------------\n");
245         return bindingEnergies;
246     }
247 
248     public ArrayList<AssemblyState> getStates(){
249         ArrayList<AssemblyState> states = new ArrayList<>();
250         for(StateContainer s: statesQueue){
251             states.add(s.getState());
252         }
253         return states;
254     }
255     public ArrayList<AssemblyState> getStatesWithinEnergy(double energy){
256         ArrayList<AssemblyState> states = new ArrayList<>();
257         StateContainer minState = statesQueue.peek();
258         if(minState != null){
259             double minEnergy = minState.getEnergy();
260             for(StateContainer s: statesQueue){
261                 if(s.getEnergy() - minEnergy < energy){
262                     states.add(s.getState());
263                 }
264             }
265         }
266         return states;
267     }
268     public ArrayList<AssemblyState> getStatesFilteredByRMSD(double rmsdCutoff){return null;}
269     public ArrayList<AssemblyState> getStatesAroundAverage(double averageE, double radialCutoff){return null;}
270     public ArrayList<Double> getEnergies(){
271         ArrayList<Double> energies = new ArrayList<>();
272         for(StateContainer s: statesQueue){
273             energies.add(s.getEnergy());
274         }
275         return energies;
276     }
277     public ArrayList<Double> getEnergiesWithinEnergy(double energy){
278         ArrayList<Double> energies = new ArrayList<>();
279         StateContainer minState = statesQueue.peek();
280         if(minState != null){
281             double minEnergy = minState.getEnergy();
282             for(StateContainer s: statesQueue){
283                 if(s.getEnergy() - minEnergy < energy){
284                     energies.add(s.getEnergy());
285                 }
286             }
287         }
288         return energies;
289     }
290     public double getM1MinEnergy() {return m1MinEnergy;}
291     public double getM2MinEnergy() {return m2MinEnergy;}
292     public double getTotalMonomerMinimizedEnergy() {return totalMonomerMinimizedEnergy;}
293     public double getMinimumEnergy() {return minimumEnergy;}
294     public double getAverageEnergy() {return averageEnergy;}
295     public double getAverageEnergyNoOutlier() {return averageEnergyNoOutlier;}
296     public double getStdOfEnergies() {return stdOfEnergies;}
297     public double getStdOfEnergiesNoOutlier() {return stdOfEnergiesNoOutlier;}
298     private double[] minimizeVector(double[] hBondVector, int lowBound, int highBound) {
299         highBound += 1; // To include the highBound
300         // Grid search from lowBound to highBound w/ 1 ang steps
301         double[] coarsePotentialSurface = new double[Math.abs(highBound - lowBound)];
302         double[] zSearched = new double[Math.abs(highBound - lowBound)]; // Relative to hBondVector[2]
303         double[] coarseVector = new double[3];
304         int minIndex = -1;
305         double minE = Double.MAX_VALUE;
306         coarseVector[2] = hBondVector[2] + lowBound; // Bounds depend on the hBondVector[2] value
307         for(int i = 0; i < Math.abs(highBound - lowBound); i++){
308             zSearched[i] = i == 0 ? lowBound : zSearched[i - 1] + 1;
309             // Move mol2 to new position
310             for(Atom a: s2Atoms){
311                 a.move(coarseVector);
312             }
313             // Calculate the energy of the system
314             forceFieldEnergy.getCoordinates(x);
315             coarsePotentialSurface[i] = forceFieldEnergy.energy(x, false);
316             if(coarsePotentialSurface[i] < minE){
317                 minE = coarsePotentialSurface[i];
318                 minIndex = i;
319             }
320             coarseVector[2] = 1;
321         }
322         // Return to hBond position using difference between hBondVector[2] and zSearched[zSearched.length - 1] to start
323         // relative search
324         for (Atom s2Atom : s2Atoms) {
325             s2Atom.move(new double[]{0, 0, hBondVector[2] - zSearched[zSearched.length - 1]});
326         }
327         //logger.info("Z space: " + Arrays.toString(zSearched));
328         //logger.info("Coarse potential surface: " + Arrays.toString(coarsePotentialSurface));
329 
330         // Refine the minimum with binary search
331         double[] refinedVector = new double[3]; // new c position relative to previous c
332         double a;
333         double aPotential;
334         if (minIndex == 0) {
335             a = zSearched[minIndex + 1];
336             aPotential = coarsePotentialSurface[minIndex + 1];
337         } else if (minIndex == coarsePotentialSurface.length - 1) {
338             a = zSearched[minIndex - 1];
339             aPotential = coarsePotentialSurface[minIndex - 1];
340         } else {
341             a = coarsePotentialSurface[minIndex - 1] < coarsePotentialSurface[minIndex + 1] ? // relative to hbond
342                     zSearched[minIndex - 1] : zSearched[minIndex + 1];
343             aPotential = Math.min(coarsePotentialSurface[minIndex - 1], coarsePotentialSurface[minIndex + 1]);
344         }
345         double b = zSearched[minIndex]; // relative to hbond
346         double bPotential = coarsePotentialSurface[minIndex];
347         double c = 0; // Store position of previous c
348         double convergence = 1e-5;
349         while(Math.abs(aPotential - bPotential) > convergence){
350             refinedVector[2] = (a + b) / 2 - c;
351             // Move mol2 to new position between a and b
352             for(Atom a1: s2Atoms){
353                 a1.move(refinedVector);
354             }
355             // Calculate the energy of the system
356             forceFieldEnergy.getCoordinates(x);
357             double e = forceFieldEnergy.energy(x, false);
358             // Set up next step
359             minE = Math.min(e, minE);
360             if(aPotential > bPotential && e < aPotential){
361                 a = (a+b)/2;
362                 aPotential = e;
363                 c = a;
364             } else if(e < bPotential){
365                 b = (a+b)/2;
366                 bPotential = e;
367                 c = b;
368             } else{
369                 // Move to a or b
370                 c = (a + b) / 2;
371                 if(aPotential < bPotential){
372                     refinedVector[2] = a - c;
373                     for(Atom a1: s2Atoms){
374                         a1.move(refinedVector);
375                     }
376                 } else{
377                     refinedVector[2] = b - c;
378                     for(Atom a1: s2Atoms){
379                         a1.move(refinedVector);
380                     }
381                 }
382                 break; // e > bPotential and e > aPotential --> numerical stability err or P.E.S. is not parabolic
383             }
384         }
385         refinedVector[2] = aPotential < bPotential ? a : b;
386         //logger.info("Hbond vector: " + Arrays.toString(hBondVector));
387         //logger.info("Refined vector (relative to hbond): " + Arrays.toString(refinedVector));
388         return refinedVector; // relative to hbond
389     }
390 
391     // This used to be for minimization but caused issues
392     private void systemEnergies(){
393         // Monomer one energy
394         for(Atom a: s2Atoms){ a.setUse(false); }
395         logger.info("\n --------- System 1 Energy Breakdown --------- ");
396         double monomerEnergy = forceFieldEnergy.energy(x, true);
397         for(Atom a: s2Atoms){ a.setUse(true); }
398 
399         // Monomer two energy
400         for(Atom a: s1Atoms){ a.setUse(false); }
401         logger.info("\n --------- System 2 Energy Breakdown --------- ");
402         double monomerEnergy2 = forceFieldEnergy.energy(x, true);
403         for(Atom a: s1Atoms){ a.setUse(true); }
404 
405         // Log potentials
406         logger.info(format("\n %-29s%12.7f kcal/mol", "System energy 1:", monomerEnergy));
407         logger.info(format(" %-29s%12.7f kcal/mol", "System energy 2:", monomerEnergy2));
408 
409         m1MinEnergy = monomerEnergy;
410         m2MinEnergy = monomerEnergy2;
411         totalMonomerMinimizedEnergy = monomerEnergy + monomerEnergy2;
412     }
413 
414     /**
415      * This function minimizes a single molecule using static tscan and a minimization engine
416      * @param m
417      * @return
418      */
419     private int minimizeMolecule(Molecule m){
420         if (this.tScan){
421             staticScanMolecule(m);
422         }
423         Minimize minimize = new Minimize(this.mola, this.forceFieldEnergy, this.algorithmListener);
424         minimize.minimize(this.eps, this.maxIter).getCoordinates(this.x);
425         return minimize.getStatus();
426     }
427 
428     private void staticScanMolecule(Molecule m){
429         TorsionSearch torsionSearch = new TorsionSearch(this.mola, m, 32, 1);
430         torsionSearch.staticAnalysis(0, 100);
431         if(!torsionSearch.getStates().isEmpty()){
432             AssemblyState minState = torsionSearch.getStates().get(0);
433             minState.revertState();
434         }
435     }
436     private int minimizeSystem(Atom a, Atom b) throws Exception{
437         if(tScan){
438             // Molecules feel each other
439             logger.info("\n --------- System 1 Static Torsion Scan --------- ");
440             forceFieldEnergy.getCoordinates(x);
441             double tscanE = forceFieldEnergy.energy(x, false);
442             for(Molecule m: s1) {
443                 staticScanMolecule(m);
444             }
445             forceFieldEnergy.getCoordinates(x);
446             double tscanEAfter = forceFieldEnergy.energy(x, false);
447             logger.info("\n Energy before static torsion scan of system 1: " + tscanE);
448             logger.info(" Energy after static torsion scan of system 1: " + tscanEAfter);
449 
450             logger.info("\n --------- System 2 Static Torsion Scan --------- ");
451             forceFieldEnergy.getCoordinates(x);
452             tscanE = forceFieldEnergy.energy(x, false);
453             for(Molecule m: s2) {
454                 staticScanMolecule(m);
455             }
456             forceFieldEnergy.getCoordinates(x);
457             tscanEAfter = forceFieldEnergy.energy(x, false);
458             logger.info("\n Energy before static torsion scan of system 2: " + tscanE);
459             logger.info(" Energy after static torsion scan of system 2: " + tscanEAfter);
460         }
461         forceFieldEnergy.getCoordinates(x);
462         double e = forceFieldEnergy.energy(x, true);
463         RestrainDistance restrainDistance = getRestraintBond(a, b, e);
464         Minimize minEngine = new Minimize(mola, forceFieldEnergy, null);
465         try {
466             minEngine.minimize(this.eps, this.maxIter);
467         } catch (Exception ex){
468             // Delete restraintBond no matter what
469             a.getBonds().remove(restrainDistance);
470             b.getBonds().remove(restrainDistance);
471             a.update();
472             b.update();
473             mola.getBondList().remove(restrainDistance);
474             mola.update();
475             return -1;
476         }
477         // Delete restraintBond
478         a.getBonds().remove(restrainDistance);
479         b.getBonds().remove(restrainDistance);
480         a.update();
481         b.update();
482         mola.getBondList().remove(restrainDistance);
483         mola.update();
484         return minEngine.getStatus();
485     }
486 
487     private RestrainDistance getRestraintBond(Atom a, Atom b, double e) throws Exception {
488         if (e > 1000000){
489             throw new Exception(" Energy too high to minimize.");
490         }
491         // Set up restraintBond
492         BondType restraint = new BondType(new int[]{a.getAtomicNumber(), b.getAtomicNumber()},
493                 100.0,
494                 this.hBondDist,
495                 BondType.BondFunction.FLAT_BOTTOM_QUARTIC,
496                 this.flatBottomRadius);
497         RestrainDistance restrainDistance = new RestrainDistance(a, b,
498                 null,
499                 false,
500                 0.0, 0.0,
501                 null);
502         restrainDistance.setBondType(restraint);
503         return restrainDistance;
504     }
505 
506     private void setTargetAtoms(Atom[] atoms){
507         for(Atom a: atoms){
508             if(a.getAtomType().atomicNumber == 7|| a.getAtomType().atomicNumber == 8
509                     || a.getAtomType().atomicNumber == 9 || a.getAtomType().atomicNumber == 15
510                     || a.getAtomType().atomicNumber == 16 || a.getAtomType().atomicNumber == 17){ // N,O,F,P,S,Cl
511                 if(a.getMoleculeNumber() == mola.getMoleculeNumbers()[0]) {
512                     s1TargetAtoms.add(a);
513                 } else{
514                     s2TargetAtoms.add(a);
515                 }
516 
517                 // Searching for bonded h's only if we are excluding H's that aren't bonded to electronegative atoms
518                 if(excludeH) {
519                     for (Bond b : a.getBonds()) {
520                         int num = b.get1_2(a).getAtomType().atomicNumber;
521                         if (num == 1) {
522                             if (a.getMoleculeNumber() == mola.getMoleculeNumbers()[0]) {
523                                 s1TargetAtoms.add(a);
524                             } else {
525                                 s2TargetAtoms.add(a);
526                             }
527                         }
528                     }
529                 }
530             }
531             else if(a.getAtomType().atomicNumber == 1 && !excludeH){ // Add all H's
532                 if(a.getMoleculeNumber() == mola.getMoleculeNumbers()[0]) {
533                     s1TargetAtoms.add(a);
534                 } else{
535                     s2TargetAtoms.add(a);
536                 }
537             }
538         }
539 
540     }
541     private void calculateMeanStd(){
542         ArrayList<Double> energies = this.getEnergies();
543         double highOutlierCutoff = Double.MAX_VALUE;
544         double lowOutlierCutoff = Double.MIN_VALUE;
545         // Calculate the interquartile range
546         if(energies.size() > 4) {
547             double iqr = energies.get(energies.size() / 4) - energies.get(3 * energies.size() / 4);
548             double q3 = energies.get(3 * energies.size() / 4);
549             double q1 = energies.get(energies.size() / 4);
550             highOutlierCutoff = q3 + 1.5 * iqr;
551             lowOutlierCutoff = q1 - 1.5 * iqr;
552         }
553 
554         // Calculate the means
555         int count = 0;
556         double sum = 0.0;
557         double sumNoOutlier = 0.0;
558         for(double e: energies){
559             if(e < highOutlierCutoff && e > lowOutlierCutoff){
560                 sumNoOutlier += e;
561                 count++;
562             }
563             sum += e;
564         }
565         averageEnergy = sum / energies.size();
566         averageEnergyNoOutlier = sumNoOutlier / count;
567 
568         // Calculate stds
569         double sumOfSquares = 0.0;
570         double sumOfSquaresNoOutlier = 0.0;
571         for(double e: energies){
572             sumOfSquares += (e - averageEnergy) * (e - averageEnergy);
573             if(e < highOutlierCutoff && e > lowOutlierCutoff){
574                 sumOfSquaresNoOutlier += (e - averageEnergyNoOutlier) * (e - averageEnergyNoOutlier);
575             }
576         }
577         stdOfEnergies = Math.sqrt(sumOfSquares / energies.size());
578         stdOfEnergiesNoOutlier = Math.sqrt(sumOfSquaresNoOutlier / count);
579     }
580 
581     private static void alignSystemCOMtoAtomVecWithAxis(Atom a, double[] axis, Atom[] mAtoms){
582         // Get center of mass of moleculeOneAtoms
583         double[] moleculeOneCOM = getCOM(mAtoms);
584         for (Atom mAtom : mAtoms) {
585             mAtom.move(new double[]{-moleculeOneCOM[0], -moleculeOneCOM[1], -moleculeOneCOM[2]});
586         }
587         // Get coordinates of a
588         double[] aCoords = a.getXYZ().copy().get();
589         // Get rotation matrix to align dipole moment with z-axis
590         double[][] rotation = getRotationBetween(aCoords, axis);
591         // Get a reference to the moleculeOneAtoms
592         double[] moleculeAtomicPositions = new double[mAtoms.length * 3];
593         for(int i = 0; i < mAtoms.length; i++){
594             moleculeAtomicPositions[i*3] = mAtoms[i].getX();
595             moleculeAtomicPositions[i*3 + 1] = mAtoms[i].getY();
596             moleculeAtomicPositions[i*3 + 2] = mAtoms[i].getZ();
597         }
598         applyRotation(moleculeAtomicPositions, rotation);
599         // Move atoms into rotated positions
600         for(int i = 0; i < mAtoms.length; i++){
601             mAtoms[i].setXYZ(new double[] {moleculeAtomicPositions[i*3],
602                     moleculeAtomicPositions[i*3 + 1],
603                     moleculeAtomicPositions[i*3 + 2]});
604         }
605     }
606 
607     /**
608      * Gets the center of mass of a set of atoms
609      * @param atoms
610      * @return x,y,z coordinates of center of mass
611      */
612     private static double[] getCOM(Atom[] atoms){
613         // Get center of mass of moleculeOneAtoms
614         double[] COM = new double[3];
615         double totalMass = 0.0;
616         for(Atom s: atoms){
617             double[] pos = s.getXYZ().get();
618             COM[0] += pos[0] * s.getMass();
619             COM[1] += pos[1] * s.getMass();
620             COM[2] += pos[2] * s.getMass();
621             totalMass += s.getMass();
622         }
623         totalMass = 1 / totalMass;
624         COM[0] *= totalMass;
625         COM[1] *= totalMass;
626         COM[2] *= totalMass;
627 
628         return COM;
629     }
630 
631     /**
632      * Gets the rotation matrix between two vectors
633      * @param v1
634      * @param v2
635      * @return rotation matrix
636      */
637     private static double[][] getRotationBetween(double[] v1, double[] v2){
638         // Normalize v1 and v2
639         double v1Norm = 1/ Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2]);
640         double v2Norm = 1 / Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2]);
641         for(int i = 0; i < 3; i++){
642             v1[i] *= v1Norm;
643             v2[i] *= v2Norm;
644         }
645         // Cross product between targetVector and z-axis
646         double[] crossProduct = new double[3];
647         crossProduct[0] = v1[1] * v2[2] - v1[2] * v2[1];
648         crossProduct[1] = v1[2] * v2[0] - v1[0] * v2[2];
649         crossProduct[2] = v1[0] * v2[1] - v1[1] * v2[0];
650         // Normalize cross product
651         double crossProductNorm = 1 / Math.sqrt(crossProduct[0] * crossProduct[0] + crossProduct[1] * crossProduct[1] + crossProduct[2] * crossProduct[2]);
652         for(int i = 0; i < 3; i++){
653             crossProduct[i] *= crossProductNorm;
654         }
655         // Dot product between v1 and z-axis
656         double dotProduct = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
657         // Angle between v1 and z-axis
658         double theta = Math.acos(dotProduct);
659         // Define quaternion from axis-angle
660         double[] quaternion = new double[4];
661         quaternion[0] = cos(theta/2);
662         quaternion[1] = crossProduct[0] * sin(theta/2);
663         quaternion[2] = crossProduct[1] * sin(theta/2);
664         quaternion[3] = crossProduct[2] * sin(theta/2);
665         // Normalize quaternion
666         double quaternionNorm = 1 / Math.sqrt(quaternion[0] * quaternion[0] + quaternion[1] * quaternion[1]
667                 + quaternion[2] * quaternion[2] + quaternion[3] * quaternion[3]);
668         for(int i = 0; i < 4; i++){ quaternion[i] *= quaternionNorm; }
669         // Useful storage
670         double q1q1 = quaternion[1] * quaternion[1];
671         double q2q2 = quaternion[2] * quaternion[2];
672         double q3q3 = quaternion[3] * quaternion[3];
673         double q0q1 = quaternion[0] * quaternion[1];
674         double q0q2 = quaternion[0] * quaternion[2];
675         double q0q3 = quaternion[0] * quaternion[3];
676         double q1q2 = quaternion[1] * quaternion[2];
677         double q1q3 = quaternion[1] * quaternion[3];
678         double q2q3 = quaternion[2] * quaternion[3];
679         // Quaternion rotation matrix
680         double[][] rotation = new double[3][3];
681         rotation[0][0] = 1 - 2 * (q2q2 + q3q3);
682         rotation[0][1] = 2 * (q1q2 - q0q3);
683         rotation[0][2] = 2 * (q1q3 + q0q2);
684         rotation[1][0] = 2 * (q1q2 + q0q3);
685         rotation[1][1] = 1 - 2 * (q1q1 + q3q3);
686         rotation[1][2] = 2 * (q2q3 - q0q1);
687         rotation[2][0] = 2 * (q1q3 - q0q2);
688         rotation[2][1] = 2 * (q2q3 + q0q1);
689         rotation[2][2] = 1 - 2 * (q1q1 + q2q2);
690 
691         return rotation;
692     }
693 
694     private static Atom[] getAtomsFromMoleculeArray(Molecule[] system){
695         ArrayList<Atom> atoms = new ArrayList<>();
696         for(Molecule m: system){
697             atoms.addAll(m.getAtomList());
698         }
699         return atoms.toArray(new Atom[0]);
700     }
701 
702 
703     /**
704      * Implements StateContainer to store the coordinates of a state and its energy
705      */
706     private record StateContainer(AssemblyState state, double e) implements Comparable<StateContainer>
707     {
708         AssemblyState getState() {
709             return state;
710         }
711 
712         double getEnergy() {
713             return e;
714         }
715 
716         @Override
717         public int compareTo(StateContainer o) {
718             return Double.compare(e, o.getEnergy());
719         }
720     }
721 }