View Javadoc
1   //******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  //******************************************************************************
38  package ffx.algorithms.commands.test;
39  
40  import ffx.algorithms.cli.AlgorithmsCommand;
41  import ffx.algorithms.optimize.Minimize;
42  import ffx.crystal.Crystal;
43  import ffx.numerics.Potential;
44  import ffx.potential.ForceFieldEnergy;
45  import ffx.potential.MolecularAssembly;
46  import ffx.potential.bonded.Atom;
47  import ffx.utilities.Constants;
48  import ffx.utilities.FFXBinding;
49  import org.apache.commons.io.FilenameUtils;
50  import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
51  import picocli.CommandLine.Command;
52  import picocli.CommandLine.Option;
53  import picocli.CommandLine.Parameters;
54  
55  import java.io.File;
56  import java.util.Arrays;
57  import java.util.Random;
58  
59  /**
60   * The CrystalSearch script searches for minimum energy polymorphs for a given space group.
61   * <br>
62   * Usage:
63   * <br>
64   * ffxc test.CrystalSearch [options] &lt;filename&gt;
65   */
66  @Command(description = " Search for minimum energy polymoprhs for a given space group.", name = "test.CrystalSearch")
67  public class CrystalSearch extends AlgorithmsCommand {
68  
69    /**
70     * -e or --eps Minimization convergence criteria (Kcal/mol/Ang).
71     */
72    @Option(names = {"-e", "--eps"}, paramLabel = "1.0", defaultValue = "1.0",
73        description = "Minimization convergence criteria (Kcal/mol/Ang).")
74    private double eps;
75  
76    /**
77     * -t or --trials Number of random trials.
78     */
79    @Option(names = {"-t", "--trials"}, paramLabel = "10", defaultValue = "10",
80        description = "Number of random trials.")
81    private int nTrials;
82  
83    /**
84     * A PDB or XYZ filename.
85     */
86    @Parameters(arity = "1", paramLabel = "file",
87        description = "A PDB or XYZ input file.")
88    private String filename;
89  
90    /**
91     * CrystalSearch Constructor.
92     */
93    public CrystalSearch() {
94      super();
95    }
96  
97    /**
98     * CrystalSearch Constructor.
99     * @param binding The Binding to use.
100    */
101   public CrystalSearch(FFXBinding binding) {
102     super(binding);
103   }
104 
105   /**
106    * CrystalSearch constructor that sets the command line arguments.
107    * @param args Command line arguments.
108    */
109   public CrystalSearch(String[] args) {
110     super(args);
111   }
112 
113   /**
114    * Function to provide random doubles within a specified range.
115    *
116    * @param maxShift Maximum shift value
117    * @param minShift Minimum shift value
118    * @param rando Random number generator
119    * @return Random double between minShift and maxShift
120    */
121   private static double getRandomNumber(double maxShift, double minShift, Random rando) {
122     return minShift + (maxShift - minShift) * rando.nextDouble();
123   }
124 
125   /**
126    * Function to get atomic coordinates after minimization.
127    *
128    * @param atoms Array of atoms
129    * @param numberOfAtoms Number of atoms
130    * @return 2D array of atomic coordinates
131    */
132   private static double[][] getAtomicCoordinates(Atom[] atoms, int numberOfAtoms) {
133     double[] coords = new double[3];
134     double[][] atomCoords = new double[numberOfAtoms][3];
135     for (int j = 0; j < numberOfAtoms; j++) {
136       coords[0] = atoms[j].getX();
137       coords[1] = atoms[j].getY();
138       coords[2] = atoms[j].getZ();
139       for (int k = 0; k < 3; k++) {
140         atomCoords[j][k] = coords[k];
141       }
142     }
143     return atomCoords;
144   }
145 
146   /**
147    * Calculate crystal density.
148    *
149    * @param mass Molecular mass
150    * @param nSymm Number of symmetry operations
151    * @param unitCell Crystal unit cell
152    * @return Density in g/cm³
153    */
154   private static double density(double mass, int nSymm, Crystal unitCell) {
155     return (mass * nSymm / Constants.AVOGADRO) * (1.0e24 / unitCell.volume);
156   }
157 
158   /**
159    * Execute the script.
160    */
161   @Override
162   public CrystalSearch run() {
163 
164     if (!init()) {
165       return this;
166     }
167 
168     // Load the MolecularAssembly.
169     MolecularAssembly molecularAssembly = getActiveAssembly(filename);
170     if (molecularAssembly == null) {
171       logger.info(helpString());
172       return this;
173     }
174 
175     Crystal crystal = molecularAssembly.getCrystal().getUnitCell();
176 
177     logger.info("\n Searching for " + filename);
178     logger.info(" RMS gradient convergence criteria: " + eps);
179 
180     ForceFieldEnergy forceFieldEnergy = molecularAssembly.getPotentialEnergy();
181     Minimize minimize = new Minimize(molecularAssembly, null);
182     Atom[] atoms = molecularAssembly.getAtomArray();
183     int nSymm = 4;
184 
185     //Allocate memory for Translate array
186     double[] translate = new double[3];
187 
188     //Allocate memory for COM
189     double[] com = new double[3];
190 
191     // Used to label trial number
192     int counter = 1;
193 
194     // Avogadro #
195     final double avogadro = Constants.AVOGADRO;
196 
197     // Determine the number of atoms in molecule
198     int nAtoms = atoms.length;
199 
200     // Allocate memory for mass of atoms
201     // Variable used in determining crystal density
202     double mass = 0.0;
203 
204     // Create object "random" used to generate randome doubles
205     Random random = new Random();
206 
207     // Allocate memory for the atom coordinates after translation, rotation, and minimization
208     double[][] newAtoms = new double[nAtoms][3];
209 
210     // Allocate temporary memory for the axis coordinates for each atom
211     double[] finalCoords = new double[3];
212 
213     // Allocate memory for the best atom coordinates from all conducted trials
214     double[][] bestAtoms = new double[nAtoms][3];
215 
216     double[][] originalAtoms = new double[nAtoms][3];
217 
218     double[] bestCrystalParameters = new double[6];
219 
220     // Allocate memory for unit cell axis lengths and angles
221     double a = 0.0;
222     double b = 0.0;
223     double c = 0.0;
224     double alpha = 0.0;
225     double beta = 0.0;
226     double gamma = 0.0;
227 
228     // Use "a" length to set apropriate translation vectors
229     double max = 0.5;
230     double min = -max;
231 
232     //Allocate memory for variable used in comparision with density of crystal
233     double minDensity = 0.8;
234     double maxDensity = 1.3;
235 
236     // Boolean used to escape while loop
237     boolean likelyDensity = false;
238 
239     // Translate the center of mass of the molecule to the origin
240     // This allows for proper rotation of molecule and only needs to be conducted once.
241 
242     // Assign the center of mass as an array to 0.0
243     com[0] = 0.0;
244     com[1] = 0.0;
245     com[2] = 0.0;
246 
247     // Loop over each atom to find the average X, Y, and Z values
248     for (Atom atom : atoms) {
249       com[0] = com[0] + atom.getX();
250       com[1] = com[1] + atom.getY();
251       com[2] = com[2] + atom.getZ();
252     }
253 
254     // Divide average coordinate value by the number of atoms
255     com[0] = com[0] / nAtoms;
256     com[1] = com[1] / nAtoms;
257     com[2] = com[2] / nAtoms;
258 
259     // Calculate the translation vector for the center of mass
260     crystal.toPrimaryCell(com, translate);
261     translate[0] = translate[0] - com[0];
262     translate[1] = translate[1] - com[1];
263     translate[2] = translate[2] - com[2];
264 
265     // Move each atom and calculate the total mass of the molecule
266     for (Atom atom : atoms) {
267       atom.move(translate);
268       mass += atom.getMass();
269     }
270 
271     double energy2 = Double.MAX_VALUE;
272 
273     // For each trial apply a rotation and translation.
274     for (int i = 0; i < nTrials; i++) {
275 
276       // Randomly assign lattice parameters for crystal
277       while (!likelyDensity) {
278         a = getRandomNumber(14, 8, random);
279         b = getRandomNumber(14, 8, random);
280         c = getRandomNumber(14, 8, random);
281 
282         // Monoclinic Space group settings require alpha and gamma = 90
283         alpha = 90;
284         beta = getRandomNumber(150, 140, random);
285         gamma = 90;
286 
287         crystal.changeUnitCellParameters(a, b, c, alpha, beta, gamma);
288         double den = density(mass, nSymm, crystal);
289         if (den > minDensity && den < maxDensity) {
290           likelyDensity = true;
291         }
292       }
293 
294       logger.info("---------------------- Trial Number: " + counter + " ----------------------");
295       counter++;
296       logger.info("Cell parameters: " + a + ' ' + b + ' ' + c + ' ' + beta);
297 
298       // Set likelyDensity to false so that on next trial run it will generate new unit
299       // cell parameters.
300       likelyDensity = false;
301 
302       // Apply the proposed boundary condition.
303       forceFieldEnergy.setCrystal(crystal);
304 
305       // Apply rotation algorithm
306       // Generate random Quaternion. In order to be random it can not be evenly distributed along the angle due to spherical rotation pattern.
307       double s = random.nextDouble();
308       double σ1 = Math.sqrt(1 - s);
309       double σ2 = Math.sqrt(s);
310       double θ1 = 2 * Math.PI * random.nextDouble();
311       double θ2 = 2 * Math.PI * random.nextDouble();
312       double w = Math.cos(θ2) * σ2;
313       double x = Math.sin(θ1) * σ1;
314       double y = Math.cos(θ1) * σ1;
315       double z = Math.sin(θ2) * σ2;
316 
317       // Create object for rotation of molecule based on quaternion
318       Rotation rotation = new Rotation(w, x, y, z, true);
319 
320       // Apply rotation to atoms
321       for (int j = 0; j < nAtoms; j++) {
322         atoms[j].rotate(rotation.getMatrix());
323       }
324 
325       // Generate a random TRANSLATION vector i times for the a, b, and c axis
326 
327       double d = getRandomNumber(max, min, random);
328       double e = getRandomNumber(max, min, random);
329       double f = getRandomNumber(max, min, random);
330 
331       double[] translation = new double[]{d, e, f};
332       logger.info(" Translation vector: " + Arrays.toString(translation));
333       // Apply the translation to each atom in the molecule
334       for (int k = 0; k < nAtoms; k++) {
335         atoms[k].move(translation);
336       }
337 
338       // Minimize the current atom configuration
339       Potential g = minimize.minimize(eps);
340 
341       // Gather the new minimized atom coordinates
342       newAtoms = getAtomicCoordinates(atoms, nAtoms);
343 
344       // Determine the energy associated with the current configuration
345       double energy = forceFieldEnergy.energy(false, true);
346 
347       // If first trial hold onto energy regardless of value for future comparison
348       if (i == 0) {
349         energy2 = energy;
350         for (int l = 0; l < nAtoms; l++) {
351           for (int m = 0; m < 3; m++) {
352             originalAtoms[l][m] = newAtoms[l][m];
353           }
354         }
355       } else if (energy < energy2) {
356         // If new energy is lower than previous energy, store new energy.
357         energy2 = energy;
358         for (int l = 0; l < nAtoms; l++) {
359           for (int m = 0; m < 3; m++) {
360             bestAtoms[l][m] = newAtoms[l][m];
361           }
362         }
363         bestCrystalParameters = new double[]{a, b, c, alpha, beta, gamma};
364       }
365       // After running 1,000 trials coords were like 180, 93,100 so i believe the tranlations were summed.
366       for (int n = 0; n < nAtoms; n++) {
367         finalCoords[0] = originalAtoms[n][0];
368         finalCoords[1] = originalAtoms[n][1];
369         finalCoords[2] = originalAtoms[n][2];
370         atoms[n].moveTo(finalCoords[0], finalCoords[1], finalCoords[2]);
371       }
372       // If the energy is not lower than previously stored energy and its the last trial
373       // then we need to retrieve the lowest energy snapshot
374       if (i == nTrials - 1) {
375         for (int n = 0; n < nAtoms; n++) {
376           finalCoords[0] = bestAtoms[n][0];
377           finalCoords[1] = bestAtoms[n][1];
378           finalCoords[2] = bestAtoms[n][2];
379           atoms[n].moveTo(finalCoords[0], finalCoords[1], finalCoords[2]);
380         }
381         crystal.changeUnitCellParameters(bestCrystalParameters[0], bestCrystalParameters[1],
382             bestCrystalParameters[2], bestCrystalParameters[3], bestCrystalParameters[4],
383             bestCrystalParameters[5]);
384         forceFieldEnergy.setCrystal(crystal);
385         logger.info("Final energy is " + energy2);
386       }
387     }
388 
389     // Save lowest energy snapshot
390     String ext = FilenameUtils.getExtension(filename);
391     filename = FilenameUtils.removeExtension(filename);
392 
393     if (ext.toUpperCase().contains("XYZ")) {
394       algorithmFunctions.saveAsXYZ(molecularAssembly, new File(filename + ".xyz"));
395     } else {
396       algorithmFunctions.saveAsPDB(molecularAssembly, new File(filename + ".pdb"));
397     }
398 
399     return this;
400   }
401 }