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