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