View Javadoc
1   //******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  //******************************************************************************
38  
39  package ffx.algorithms.optimize;
40  
41  import com.google.common.collect.Lists;
42  import com.google.common.collect.MinMaxPriorityQueue;
43  import ffx.numerics.math.HilbertCurveTransforms;
44  import ffx.potential.AssemblyState;
45  import ffx.potential.ForceFieldEnergy;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.bonded.Atom;
48  import ffx.potential.bonded.Bond;
49  import ffx.potential.bonded.Molecule;
50  import org.apache.commons.lang3.ArrayUtils;
51  import org.apache.commons.math3.util.FastMath;
52  
53  import javax.annotation.Nullable;
54  import java.util.ArrayList;
55  import java.util.Arrays;
56  import java.util.Collections;
57  import java.util.List;
58  import java.util.logging.Logger;
59  
60  import static ffx.potential.utils.Superpose.applyRotation;
61  import static java.lang.String.format;
62  import static org.apache.commons.math3.util.FastMath.cos;
63  import static org.apache.commons.math3.util.FastMath.sin;
64  import static org.apache.commons.math3.util.FastMath.toRadians;
65  
66  /**
67   * TorsionSearch class for performing a torsion scan on a molecule in a molecular assembly. It
68   * provides options to run a static comparison of each bond on it's own, or run the full
69   * exponential number of configurations. Options to run in parallel with multiple workers
70   * exists. It is also set up so that multiple workers can run on different molecules if runWorker
71   * is not used.
72   *
73   * @author Matthew J. Speranza
74   * @author Arron J. Nessler
75   */
76  public class TorsionSearch {
77    private static final Logger logger = Logger.getLogger(TorsionSearch.class.getName());
78  
79    private final MolecularAssembly molecularAssembly;
80    private final ForceFieldEnergy forceFieldEnergy;
81    private final double[] x;
82    private final boolean run;
83  
84    private final List<Bond> torsionalBonds;
85    private final List<Atom[]> atomGroups;
86  
87    private final int nTorsionsPerBond;
88    private final int nBits;
89    private int nTorsionalBonds;
90    private final int returnedStates;
91    private long numConfigs;
92    private long numIndices;
93    private long end;
94    private double minEnergy = Double.MAX_VALUE;
95    private final List<Double> energies = new ArrayList<>();
96    private final List<Long> hilbertIndices = new ArrayList<>();
97    private final List<AssemblyState> states = new ArrayList<>();
98    private MinMaxPriorityQueue<StateContainer> queue = null;
99  
100   private long[] workerAssignments;
101   private long indicesPerAssignment;
102   private boolean runWorker = false;
103 
104   public TorsionSearch(MolecularAssembly molecularAssembly,
105                        Molecule molecule,
106                        int nTorsionsPerBond,
107                        int returnedStates
108   ) {
109     this.molecularAssembly = molecularAssembly;
110     if (ArrayUtils.contains(molecularAssembly.getMoleculeArray(), molecule)) {
111       run = true;
112     } else {
113       logger.warning("Molecule is not part of the assembly. Torsion scan will not run.");
114       run = false;
115     }
116     this.forceFieldEnergy = molecularAssembly.getPotentialEnergy();
117     this.x = new double[forceFieldEnergy.getNumberOfVariables()];
118     this.nTorsionsPerBond = nTorsionsPerBond; // Powers of two recommended
119     this.nBits = (int) FastMath.ceil(FastMath.log(2, nTorsionsPerBond));
120     this.torsionalBonds = getTorsionalBonds(molecule);
121     this.atomGroups = getRotationGroups(torsionalBonds);
122     this.nTorsionalBonds = torsionalBonds.size();
123     this.numConfigs = (long) FastMath.pow(this.nTorsionsPerBond, torsionalBonds.size());
124     this.numIndices = (long) FastMath.pow(2, this.nBits * torsionalBonds.size());
125     if (returnedStates != -1) {
126       this.returnedStates = returnedStates;
127     } else {
128       this.returnedStates = (int) numConfigs;
129     }
130     this.end = numIndices;
131   }
132 
133   /**
134    * Scanning through nTorsionsPerBond^nTorsionsPerBond (nTorsionsPerBond x nTorsionsPerBond x nTorsionsPerBond x ...)
135    * array. Uses a hilbert curve to get to each point in the discrete space with minimal rotations
136    * between each state without recursion. Hilbert curve implementation based on OpenMM's
137    * c++ implementation. Runs through ALL indices with this worker. Use other methods to run just
138    * one workers indices assigned by the buildWorker() method.
139    */
140   public void spinTorsions() {
141     logger.info("\n ----------------- Starting torsion scan -----------------");
142     logger.info(format(" Number of configurations: %d", numConfigs));
143     logger.info(format(" Number of indices: %d", numIndices));
144     this.spinTorsions(0, this.end);
145   }
146 
147   /**
148    * Similar to above, but with a limited number of hilbert indices to run through with
149    * this worker alone.
150    */
151   public void spinTorsions(long start, long end) {
152     logger.info("\n ----------------- Starting torsion scan -----------------");
153     logger.info(format(" Number of configurations: %d", numConfigs));
154     logger.info(format(" Number of indices: %d", numIndices));
155     this.spinTorsions(start, end, true);
156   }
157 
158   /**
159    * Similar to above, but with a limited number of hilbert indices to run through with
160    * this worker alone. Can also choose whether to update the lists of energies, indices, and states.
161    * Private to prevent user from running with false, since this will not update the lists, which
162    * will waste time & resources if the lists are never updated.
163    */
164   private void spinTorsions(long start, long end, boolean updateLists) {
165     if (!run) {
166       logger.warning("Torsion spin returning early since molecule not part of molecular assembly.");
167       return;
168     }
169 
170     // Initialize the coordinates to all zeros --> torsions depend on initial configuration
171     long[] currentState = new long[nTorsionalBonds];
172 
173     // Create a priority queue to store the lowest energy states
174     if (queue == null) {
175       queue = MinMaxPriorityQueue
176           .maximumSize(returnedStates)
177           .create();
178     }
179 
180     int progress = 0; // Worker jobs and specified start/end jobs cannot finish early -> can't know how many will exist
181     while (start <= end && progress < numConfigs) {
182       long[] newState = HilbertCurveTransforms.hilbertIndexToCoordinates(nTorsionalBonds, nBits, start);
183       if (ArrayUtils.contains(newState, nTorsionsPerBond)) {
184         start++;
185         continue;
186       }
187       //logger.info(format(" Hilbert Index: %d; Coordinate State: " + Arrays.toString(newState), start));
188       // Permute from currentState to newState
189       changeState(currentState, newState, nTorsionsPerBond, torsionalBonds, atomGroups);
190       // Update coordinates
191       forceFieldEnergy.getCoordinates(x);
192       // Calculate energy
193       double energy = forceFieldEnergy.energy(x);
194       // Log minimum energy
195       if (energy < minEnergy) {
196         minEnergy = energy;
197         logger.info(format("\n New minimum energy: %12.5f", minEnergy));
198         logger.info(format(" Hilbert Index: %d; Coordinate State: " + Arrays.toString(newState), start));
199       }
200       // Add to queue
201       queue.add(new StateContainer(new AssemblyState(molecularAssembly), energy, start));
202       currentState = newState;
203       start++;
204       progress++;
205     }
206     // Revert back to original state
207     changeState(currentState, new long[nTorsionalBonds], nTorsionsPerBond, torsionalBonds, atomGroups);
208     if (progress == numConfigs) {
209       logger.info("\n Completed all configurations before end index.");
210     }
211     if (updateLists) {
212       this.updateInfoLists();
213     }
214   }
215 
216   /**
217    * Static analysis of torsional bonds. Optionally remove bonds that cause large energy changes
218    * outside of a specified threshold. Only one worker should run this method on the same structure.
219    *
220    * @param numRemove            Number of bonds to remove if they exceed the threshold.
221    * @param eliminationThreshold Threshold for removing bonds and logging as high energy.
222    */
223   public void staticAnalysis(int numRemove, double eliminationThreshold) {
224     if (!run) {
225       logger.warning("Static analysis returning early since molecule not part of molecular assembly.");
226       return;
227     }
228     if (queue == null) {
229       queue = MinMaxPriorityQueue
230           .maximumSize(returnedStates)
231           .create();
232     }
233     eliminationThreshold = FastMath.abs(eliminationThreshold);
234     // Update coordinates
235     forceFieldEnergy.getCoordinates(x);
236     // Calculate energy as a reference
237     double initialE = forceFieldEnergy.energy(x);
238     ArrayList<Integer> remove = new ArrayList<>();
239     long[] state = new long[nTorsionalBonds];
240     long[] oldState = new long[nTorsionalBonds];
241     // Loop through all torsional bonds up to the number of torsions per bond finding energies and resetting
242     for (int i = 0; i < nTorsionalBonds; i++) {
243       for (int j = i == 0 ? 0 : 1; j < nTorsionsPerBond; j++) {
244         state[i] = j;
245         changeState(oldState, state, nTorsionsPerBond, torsionalBonds, atomGroups);
246         // Update coordinates
247         forceFieldEnergy.getCoordinates(x);
248         // Calculate energy
249         double newEnergy = forceFieldEnergy.energy(x);
250         if (newEnergy - initialE > eliminationThreshold && !remove.contains(i)) {
251           remove.add(i);
252         } else {
253           long index = HilbertCurveTransforms.coordinatesToHilbertIndex(nTorsionalBonds, nBits, state);
254           queue.add(new StateContainer(new AssemblyState(molecularAssembly), newEnergy, index));
255         }
256         changeState(state, oldState, nTorsionsPerBond, torsionalBonds, atomGroups);
257       }
258       state[i] = 0;
259     }
260     remove.sort(Collections.reverseOrder());
261     logger.info("\n " + remove.size() + " bonds that cause large energy increase: " + remove);
262     if (remove.size() > numRemove) {
263       remove = Lists.newArrayList(remove.subList(0, numRemove));
264     }
265     for (int i = remove.size() - 1; i >= 0; i--) {
266       logger.info(" Removing bond: " + torsionalBonds.get(remove.get(i)));
267       logger.info(" Bond index: " + remove.get(i));
268       torsionalBonds.set(remove.get(i), null);
269       atomGroups.set(remove.get(i), null);
270     }
271     torsionalBonds.removeAll(Collections.singleton(null));
272     atomGroups.removeAll(Collections.singleton(null));
273     this.nTorsionalBonds = torsionalBonds.size();
274     // Update parameters based on new set of bonds
275     this.end = (long) FastMath.pow(2, nBits * nTorsionalBonds);
276     this.numConfigs = (long) FastMath.pow(nTorsionsPerBond, nTorsionalBonds);
277     this.numIndices = this.end;
278     logger.info(" Finished static analysis.");
279     logger.info(format(" Number of configurations after elimination: %d", numConfigs));
280     logger.info(format(" Number of indices after elimination: %d", numIndices));
281     this.updateInfoLists();
282   }
283 
284   /**
285    * Builds the worker assignments for each rank. Should be run before spinTorsions.
286    *
287    * @param rank
288    * @param worldSize
289    * @return whether the worker was built successfully
290    */
291   public boolean buildWorker(int rank, int worldSize) {
292     if (!run) {
293       logger.warning("Build worker returning early since molecule not part of molecular assembly.");
294       return false;
295     }
296     if (rank >= worldSize) {
297       logger.warning(" Rank is greater than world size.");
298       return false;
299     }
300     runWorker = true;
301     // Assign each worker the same number of indices
302     this.workerAssignments = new long[worldSize];
303     long jobsPerWorker = numIndices / worldSize;
304     this.indicesPerAssignment = jobsPerWorker / worldSize;
305     logger.info("\n Jobs per worker: " + jobsPerWorker);
306     logger.info(" Jobs per worker split: " + indicesPerAssignment);
307     // Assign starting indexes to each worker
308     for (int i = 0; i < worldSize; i++) {
309       workerAssignments[i] = i * jobsPerWorker + rank * indicesPerAssignment;
310     }
311     logger.info(" Worker " + rank + " assigned indices: " + Arrays.toString(workerAssignments));
312     return true;
313   }
314 
315   /**
316    * Runs this worker with the indices assigned to it by the buildWorker() method.
317    */
318   public void runWorker() {
319     if (!run) {
320       logger.warning("Worker returning early since molecule not part of molecular assembly.");
321       return;
322     } else if (!runWorker) {
323       logger.warning("Worker returning early since worker not built or is invalid.");
324       return;
325     }
326     logger.info("\n ----------------- Starting torsion scan -----------------");
327     logger.info(format("\n Number of configurations before worker starts: %d", numConfigs));
328     logger.info(format(" Number of indices before worker starts: %d", numIndices));
329     for (int i = 0; i < workerAssignments.length; i++) {
330       logger.info(format(" Worker torsion assignment %3d of %3d: %12d to %12d.", i + 1, workerAssignments.length,
331           workerAssignments[i], workerAssignments[i] + indicesPerAssignment - 1));
332       // Only update lists on last assignment
333       this.spinTorsions(workerAssignments[i], workerAssignments[i] + indicesPerAssignment - 1,
334           i == workerAssignments.length - 1);
335     }
336   }
337 
338   /**
339    * List of energies for each state in order of lowest to highest energy.
340    */
341   public List<Double> getEnergies() {
342     return energies;
343   }
344 
345   /**
346    * List of hilbert indices for each state in order of lowest to highest energy.
347    */
348   public List<Long> getHilbertIndices() {
349     return hilbertIndices;
350   }
351 
352   /**
353    * List of states in order of lowest to highest energy.
354    */
355   public List<AssemblyState> getStates() {
356     return states;
357   }
358 
359   /**
360    * @return the end index of the system
361    */
362   public long getEnd() {
363     return end;
364   }
365 
366   /**
367    * Updates state, energy, and hilbert index lists after a run of one of the public methods.
368    */
369   private void updateInfoLists() {
370     if (!states.isEmpty()) {
371       // Refill limited-size priority queue with previous information
372       for (int i = 0; i < states.size(); i++) {
373         queue.add(new StateContainer(states.get(i), energies.get(i), hilbertIndices.get(i)));
374       }
375       // Clear lists
376       states.clear();
377       energies.clear();
378       hilbertIndices.clear();
379     }
380     // Refill lists
381     while (!queue.isEmpty()) {
382       StateContainer toBeSaved = queue.removeFirst();
383       states.add(toBeSaved.getState());
384       energies.add(toBeSaved.getEnergy());
385       hilbertIndices.add(toBeSaved.getIndex());
386     }
387   }
388 
389   /**
390    * Finds torisonal bonds in a molecule ignoring methyl groups, hydrogens, and up to 6-membered rings.
391    *
392    * @param molecule molecule of interest
393    * @return list of torsional bonds
394    */
395   private static List<Bond> getTorsionalBonds(Molecule molecule) {
396     List<Bond> torsionalBonds = new ArrayList<>();
397     for (Bond bond : molecule.getBondList()) {
398       Atom a1 = bond.getAtom(0);
399       Atom a2 = bond.getAtom(1);
400       List<Bond> bond1 = a1.getBonds();
401       int b1 = bond1.size();
402       List<Bond> bond2 = a2.getBonds();
403       int b2 = bond2.size();
404       // Ignore single bonds (ex. alcohol groups)
405       if (b1 > 1 && b2 > 1) {
406         // Ignore methyl groups
407         if (a1.getAtomicNumber() == 6 && a1.getNumberOfBondedHydrogen() == 3 || a2.getAtomicNumber() == 6 && a2.getNumberOfBondedHydrogen() == 3) {
408           continue;
409         }
410         // Ignore ring atoms
411         if (a1.isRing(a2)) {
412           continue;
413         }
414         torsionalBonds.add(bond);
415         logger.info(" Bond " + bond + " is a torsional bond.");
416       }
417     }
418     return torsionalBonds;
419   }
420 
421   /**
422    * Get the smallest group of atoms to rotate about a bond.
423    *
424    * @param bonds Torsional bonds
425    * @return List of atoms to rotate about each bond in the given list
426    */
427   private static List<Atom[]> getRotationGroups(List<Bond> bonds) {
428     List<Atom[]> rotationGroups = new ArrayList<>();
429     for (Bond bond : bonds) {
430       Atom a1 = bond.getAtom(0);
431       Atom a2 = bond.getAtom(1);
432       // We should have two atoms with a spinnable bond
433       List<Atom> a1List = new ArrayList<>();
434       List<Atom> a2List = new ArrayList<>();
435       // Search for rotation groups
436       searchTorsions(a1, a1List, a2);
437       searchTorsions(a2, a2List, a1);
438       // Convert List to array
439       Atom[] a1Array = new Atom[a1List.size()];
440       Atom[] a2Array = new Atom[a2List.size()];
441       a1List.toArray(a1Array);
442       a2List.toArray(a2Array);
443       // Add smaller list to rotation groups
444       if (a1List.size() > a2List.size()) {
445         rotationGroups.add(a2Array);
446       } else {
447         rotationGroups.add(a1Array);
448       }
449     }
450     return rotationGroups;
451   }
452 
453   /**
454    * Rotate an atom about another.
455    *
456    * @param u     a unit vector to rotate {@link ffx.potential.bonded.Atom} object about.
457    * @param a2    a {@link ffx.potential.bonded.Atom} object to create axis of rotation.
458    * @param theta Amount to rotate by in degrees.
459    */
460   private static void rotateAbout(double[] u, Atom a2, double theta) {
461 
462     theta = toRadians(theta);
463 
464     // Define quaternion from axis-angle
465     double[] quaternion = new double[4];
466     quaternion[0] = cos(theta / 2);
467     quaternion[1] = u[0] * sin(theta / 2);
468     quaternion[2] = u[1] * sin(theta / 2);
469     quaternion[3] = u[2] * sin(theta / 2);
470     // Normalize quaternion
471     double quaternionNorm = 1 / Math.sqrt(quaternion[0] * quaternion[0] + quaternion[1] * quaternion[1]
472         + quaternion[2] * quaternion[2] + quaternion[3] * quaternion[3]);
473     for (int i = 0; i < 4; i++) {
474       quaternion[i] *= quaternionNorm;
475     }
476     // Useful storage
477     double q1q1 = quaternion[1] * quaternion[1];
478     double q2q2 = quaternion[2] * quaternion[2];
479     double q3q3 = quaternion[3] * quaternion[3];
480     double q0q1 = quaternion[0] * quaternion[1];
481     double q0q2 = quaternion[0] * quaternion[2];
482     double q0q3 = quaternion[0] * quaternion[3];
483     double q1q2 = quaternion[1] * quaternion[2];
484     double q1q3 = quaternion[1] * quaternion[3];
485     double q2q3 = quaternion[2] * quaternion[3];
486     // Quaternion rotation matrix
487     double[][] rotation2 = new double[3][3];
488     rotation2[0][0] = 1 - 2 * (q2q2 + q3q3);
489     rotation2[0][1] = 2 * (q1q2 - q0q3);
490     rotation2[0][2] = 2 * (q1q3 + q0q2);
491     rotation2[1][0] = 2 * (q1q2 + q0q3);
492     rotation2[1][1] = 1 - 2 * (q1q1 + q3q3);
493     rotation2[1][2] = 2 * (q2q3 - q0q1);
494     rotation2[2][0] = 2 * (q1q3 - q0q2);
495     rotation2[2][1] = 2 * (q2q3 + q0q1);
496     rotation2[2][2] = 1 - 2 * (q1q1 + q2q2);
497 
498     double[] a2XYZ = new double[3];
499     a2.getXYZ(a2XYZ);
500     applyRotation(a2XYZ, rotation2);
501     a2.setXYZ(a2XYZ);
502   }
503 
504   /**
505    * Identify atoms that should be rotated.
506    *
507    * @param seed    an {@link ffx.potential.bonded.Atom} object to rotate about.
508    * @param atoms   a list of {@link ffx.potential.bonded.Atom} objects to rotate.
509    * @param notAtom Avoid this atom (wrong side of bond).
510    */
511   private static void searchTorsions(@Nullable Atom seed, List<Atom> atoms, Atom notAtom) {
512     if (seed == null) {
513       return;
514     }
515     atoms.add(seed);
516     for (Bond b : seed.getBonds()) {
517       Atom nextAtom = b.get1_2(seed);
518       if (nextAtom == notAtom || atoms.contains(nextAtom)) { //nextAtom.getParent() != null ||
519         continue;
520       }
521       searchTorsions(nextAtom, atoms, notAtom);
522     }
523   }
524 
525   /**
526    * Change the state of the molecule (rotations) to match newState based on changes from the oldState.
527    *
528    * @param oldState  current positions
529    * @param newState  new positions
530    * @param nTorsions number of torsions per bond
531    * @param bonds     list of bonds that the states define
532    * @param atoms     list of atoms to rotate with each bond
533    */
534   private static void changeState(long[] oldState, long[] newState, int nTorsions,
535                                   List<Bond> bonds, List<Atom[]> atoms) {
536     for (int i = 0; i < oldState.length; i++) {
537       if (oldState[i] != newState[i]) {
538         // Add change required to go from oldState to newState to change array
539         int change = (int) (newState[i] - oldState[i]);
540         // Apply change at this index to atoms
541         int turnDegrees = (change * (360 / nTorsions));
542         // Get vector from atom to atom of bond i
543         double[] u = new double[3];
544         double[] translation = new double[3];
545         double[] a1 = bonds.get(i).getAtom(0).getXYZ(new double[3]);
546         double[] a2 = bonds.get(i).getAtom(1).getXYZ(new double[3]);
547         if (atoms.get(i)[0] == bonds.get(i).getAtom(0)) { // if a1 had the smaller rotation group
548           for (int j = 0; j < 3; j++) {
549             // Rotation about this vector is same as the same rotation about the opposite vector?
550             u[j] = a1[j] - a2[j]; // Vector defined as line segment from a2 to a1
551             translation[j] = -a1[j];
552           }
553           // Unit vector
554           double unit = 1 / Math.sqrt(u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);
555           for (int j = 0; j < 3; j++) {
556             u[j] *= unit;
557           }
558           for (int j = 0; j < atoms.get(i).length; j++) {
559             atoms.get(i)[j].move(translation);
560             rotateAbout(u, atoms.get(i)[j], turnDegrees);
561             atoms.get(i)[j].move(a1);
562           }
563         } else {
564           for (int j = 0; j < 3; j++) {
565             u[j] = a2[j] - a1[j]; // Vector defined as line segment from a1 to a2
566             translation[j] = -a2[j];
567           }
568           double unit = 1 / Math.sqrt(u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);
569           for (int j = 0; j < 3; j++) {
570             u[j] *= unit;
571           }
572           for (int j = 0; j < atoms.get(i).length; j++) {
573             atoms.get(i)[j].move(translation);
574             rotateAbout(u, atoms.get(i)[j], turnDegrees);
575             atoms.get(i)[j].move(a2); // This line different then above
576           }
577         }
578       }
579     }
580   }
581 
582   /**
583    * Implements StateContainer to store the coordinates of a state and its energy
584    */
585   private record StateContainer(AssemblyState state, double e,
586                                 long hilbertIndex) implements Comparable<StateContainer> {
587     AssemblyState getState() {
588       return state;
589     }
590 
591     double getEnergy() {
592       return e;
593     }
594 
595     long getIndex() {
596       return hilbertIndex;
597     }
598 
599     @Override
600     public int compareTo(StateContainer o) {
601       return Double.compare(e, o.getEnergy());
602     }
603   }
604 }