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.xray.commands.test;
39  
40  import edu.rit.pj.Comm;
41  import ffx.algorithms.cli.AlgorithmsCommand;
42  import ffx.algorithms.cli.DynamicsOptions;
43  import ffx.algorithms.cli.LambdaParticleOptions;
44  import ffx.algorithms.dynamics.MolecularDynamics;
45  import ffx.algorithms.dynamics.integrators.IntegratorEnum;
46  import ffx.algorithms.dynamics.thermostats.ThermostatEnum;
47  import ffx.algorithms.thermodynamics.HistogramData;
48  import ffx.algorithms.thermodynamics.LambdaData;
49  import ffx.algorithms.thermodynamics.OrthogonalSpaceTempering;
50  import ffx.algorithms.thermodynamics.OrthogonalSpaceTempering.OptimizationParameters;
51  import ffx.numerics.Potential;
52  import ffx.potential.ForceFieldEnergy;
53  import ffx.potential.MolecularAssembly;
54  import ffx.potential.bonded.Atom;
55  import ffx.potential.bonded.MSNode;
56  import ffx.realspace.cli.RealSpaceOptions;
57  import ffx.utilities.FFXBinding;
58  import ffx.xray.DiffractionData;
59  import ffx.xray.RefinementEnergy;
60  import ffx.xray.RefinementMinimize.RefinementMode;
61  import ffx.xray.cli.XrayOptions;
62  import org.apache.commons.configuration2.CompositeConfiguration;
63  import org.apache.commons.io.FilenameUtils;
64  import picocli.CommandLine.Command;
65  import picocli.CommandLine.Mixin;
66  import picocli.CommandLine.Option;
67  import picocli.CommandLine.Parameters;
68  
69  import java.io.File;
70  import java.util.Collections;
71  import java.util.List;
72  import java.util.logging.Logger;
73  
74  /**
75   * The Alchemical Changes script.
76   * <br>
77   * Usage:
78   * <br>
79   * ffxc xray.Alchemical [options] &lt;filename&gt;
80   */
81  @Command(description = " Simulated annealing on an X-ray target.", name = "xray.test.Alchemical")
82  public class Alchemical extends AlgorithmsCommand {
83  
84    @Mixin
85    private DynamicsOptions dynamicsOptions;
86  
87    @Mixin
88    private LambdaParticleOptions lambdaParticleOptions;
89  
90    @Mixin
91    private XrayOptions xrayOptions;
92  
93    private static final Logger logger = Logger.getLogger(RealSpaceOptions.class.getName());
94  
95    /**
96     * -I or --onlyIons sets whether or not ion positions are optimized (default is false; must set at least one of either '-W' or '-I') (only one type of ion is chosen).
97     */
98    @Option(names = {"-I", "--onlyIons"}, paramLabel = "false",
99        description = "Set to only optimize ions (of a single type).")
100   private boolean onlyIons = false;
101 
102   /**
103    * --itype or --iontype Specify which ion to run optimization on. If none is specified, default behavior chooses the first ion found in the PDB file.
104    */
105   @Option(names = {"--itype", "--iontype"}, paramLabel = "null",
106       description = "Specify which ion to run optimization on. If none is specified, default behavior chooses the first ion found in the PDB file.")
107   private String[] ionType = null;
108 
109   /**
110    * -N or --neutralize Adds more of the selected ion in order to neutralize the crystal's charge.
111    */
112   @Option(names = {"-N", "--neutralize"}, paramLabel = "false",
113       description = "Neutralize the crystal's charge by adding more of the selected ion")
114   private boolean neutralize = false;
115 
116   /**
117    * -W or --onlyWater sets whether or not water positions are optimized (default is false; must set at least one of either '-W' or '-I').
118    */
119   @Option(names = {"-W", "--onlyWater"}, paramLabel = "false",
120       description = "Set to only optimize water.")
121   private boolean onlyWater = false;
122 
123   /**
124    * The refinement mode to use.
125    */
126   private RefinementMode refinementMode = RefinementMode.COORDINATES;
127 
128   /**
129    * One or more filenames.
130    */
131   @Parameters(arity = "1..*", paramLabel = "files", description = "PDB and Real Space input files.")
132   private List<String> filenames;
133 
134   // Value of Lambda.
135   private double lambda = 1.0;
136 
137   // ThermostatEnum [ ADIABATIC, BERENDSEN, BUSSI ]
138   private ThermostatEnum thermostat = ThermostatEnum.ADIABATIC;
139 
140   // IntegratorEnum [ BEEMAN, RESPA, STOCHASTIC ]
141   private IntegratorEnum integrator = IntegratorEnum.STOCHASTIC;
142 
143   // File type of coordinate snapshots to write out.
144   private String fileType = "PDB";
145 
146   // OST
147   private boolean runOST = true;
148 
149   // Reset velocities (ignored if a restart file is given)
150   private boolean initVelocities = true;
151 
152   private OrthogonalSpaceTempering orthogonalSpaceTempering;
153 
154   /**
155    * Alchemical constructor.
156    */
157   public Alchemical() {
158     super();
159   }
160 
161   /**
162    * Alchemical constructor that sets the command line arguments.
163    * @param args Command line arguments.
164    */
165   public Alchemical(String[] args) {
166     super(args);
167   }
168 
169   /**
170    * Alchemical constructor.
171    * @param binding The Binding to use.
172    */
173   public Alchemical(FFXBinding binding) {
174     super(binding);
175   }
176 
177   @Override
178   public Alchemical run() {
179 
180     if (!init()) {
181       return this;
182     }
183     dynamicsOptions.init();
184     xrayOptions.init();
185     System.setProperty("lambdaterm", "true");
186 
187     String modelfilename;
188     MolecularAssembly[] assemblies;
189     if (filenames != null && filenames.size() > 0) {
190       assemblies = algorithmFunctions.openAll(filenames.get(0));
191       activeAssembly = assemblies[0];
192       modelfilename = filenames.get(0);
193     } else if (activeAssembly == null) {
194       logger.info(helpString());
195       return this;
196     } else {
197       modelfilename = activeAssembly.getFile().getAbsolutePath();
198       assemblies = new MolecularAssembly[]{activeAssembly};
199     }
200 
201     logger.info("\n Running Alchemical Changes on " + modelfilename);
202 
203     File structureFile = new File(FilenameUtils.normalize(modelfilename));
204     structureFile = new File(structureFile.getAbsolutePath());
205     String baseFilename = FilenameUtils.removeExtension(structureFile.getName());
206     File histogramRestart = new File(baseFilename + ".his");
207     File lambdaRestart = new File(baseFilename + ".lam");
208     File dyn = new File(baseFilename + ".dyn");
209 
210     Comm world = Comm.world();
211     int size = world.size();
212     int rank = 0;
213     double[] energyArray = new double[world.size()];
214     for (int i = 0; i < world.size(); i++) {
215       energyArray[i] = Double.MAX_VALUE;
216     }
217 
218     // For a multi-process job, try to get the restart files from rank sub-directories.
219     if (size > 1) {
220       rank = world.rank();
221       File rankDirectory = new File(
222           structureFile.getParent() + File.separator + Integer.toString(rank));
223       if (!rankDirectory.exists()) {
224         rankDirectory.mkdir();
225       }
226       lambdaRestart = new File(rankDirectory.getPath() + File.separator + baseFilename + ".lam");
227       dyn = new File(rankDirectory.getPath() + File.separator + baseFilename + ".dyn");
228       structureFile = new File(rankDirectory.getPath() + File.separator + structureFile.getName());
229     }
230     if (!dyn.exists()) {
231       dyn = null;
232     }
233 
234     // Set built atoms active/use flags to true (false for other atoms).
235     Atom[] atoms = activeAssembly.getAtomArray();
236 
237     // Get a reference to the first system's ForceFieldEnergy.
238     ForceFieldEnergy forceFieldEnergy = activeAssembly.getPotentialEnergy();
239     forceFieldEnergy.setPrintOnFailure(false, false);
240 
241     // Configure all atoms to:
242     // 1) be used in the potential
243     // 2) be inactive (i.e. cannot move)
244     // 3) not be controlled by the lambda state variable.
245     for (int i = 0; i <= atoms.length; i++) {
246       Atom ai = atoms[i - 1];
247       ai.setUse(true);
248       ai.setActive(false);
249       ai.setApplyLambda(false);
250     }
251 
252     double crystalCharge = activeAssembly.getCharge(true);
253     logger.info(" Overall crystal charge: " + crystalCharge);
254     List<MSNode> ions = assemblies[0].getIons();
255     List<MSNode> water = assemblies[0].getWater();
256 
257     // Consider the option of creating a composite lambda gradient from vapor phase to crystal phase
258     if (!onlyWater) {
259       logger.info("Doing ions.");
260       if (ions == null || ions.size() == 0) {
261         logger.info("\n Please add an ion to the PDB file to scan with.");
262         return this;
263       }
264       for (MSNode msNode : ions) {
265         // Check if ionType is specified and matches this ion's atom names
266         boolean ionSelected = false;
267         if (ionType != null) {
268           logger.info("Selecting ion.");
269           List<Atom> atomList = msNode.getAtomList();
270           if (!atomList.isEmpty()) {
271             String atomName = atomList.get(0).getName();
272             for (String targetIonType : ionType) {
273               if (atomName.equals(targetIonType)) {
274                 ionSelected = true;
275                 break;
276               }
277             }
278           }
279         }
280 
281         if (ionSelected) {
282           logger.info("Ion has been selected.");
283           for (Atom atom : msNode.getAtomList()) {
284             System.out.println("Activating ions");
285             atom.setUse(true);
286             atom.setActive(true);
287             atom.setApplyLambda(true);
288             logger.info(" Alchemical atom: " + atom.toString());
289           }
290         } else {
291           logger.info("Ion has not been selected.");
292           if (neutralize) {
293             logger.info("Neutralizing crystal.");
294             double ionCharge = 0;
295             for (Atom atom : msNode.getAtomList()) {
296               ionCharge += atom.getMultipoleType().getCharge();
297             }
298             logger.info("Ion charge is: " + Double.toString(ionCharge));
299             int numIons = (int) (-1 * (Math.ceil(crystalCharge / ionCharge)));
300             if (numIons > 0) {
301               List<Atom> atomList = msNode.getAtomList();
302               String atomName = atomList.isEmpty() ? "" : atomList.get(0).getName();
303               logger.info(numIons + " " + atomName
304                   + " ions needed to neutralize the crystal.");
305               ionType = new String[]{atomName};
306               for (Atom atom : msNode.getAtomList()) {
307                 atom.setUse(true);
308                 atom.setActive(true);
309                 atom.setApplyLambda(true);
310                 logger.info(" Alchemical atom: " + atom.toString());
311               }
312             }
313           } else {
314             List<Atom> atomList = msNode.getAtomList();
315             String atomName = atomList.isEmpty() ? "" : atomList.get(0).getName();
316             ionType = new String[]{atomName};
317             for (Atom atom : msNode.getAtomList()) {
318               atom.setUse(true);
319               atom.setActive(true);
320               atom.setApplyLambda(true);
321               logger.info(" Alchemical atom: " + atom.toString());
322             }
323           }
324         }
325       }
326     }
327 
328     // Lambdize water for position optimization, if this option was set to true
329     if (onlyWater) {
330       for (MSNode msNode : water) {
331         for (Atom atom : msNode.getAtomList()) {
332           // Scan with the last ion in the file.
333           atom.setUse(true);
334           atom.setActive(true);
335           atom.setApplyLambda(true);
336           logger.info(" Water atom:      " + atom.toString());
337         }
338       }
339     }
340 
341     // Load parsed X-ray properties.
342     CompositeConfiguration properties = assemblies[0].getProperties();
343     xrayOptions.setProperties(parseResult, properties);
344 
345     DiffractionData diffractionData = xrayOptions.getDiffractionData(filenames, assemblies, properties);
346     RefinementEnergy refinementEnergy = xrayOptions.toXrayEnergy(diffractionData);
347 
348     double[] x = new double[refinementEnergy.getNumberOfVariables()];
349     x = refinementEnergy.getCoordinates(x);
350     refinementEnergy.energy(x, true);
351 
352     refinementEnergy.setLambda(lambda);
353 
354     CompositeConfiguration props = assemblies[0].getProperties();
355 
356     HistogramData histogramData = HistogramData.readHistogram(histogramRestart);
357     if (lambdaRestart == null) {
358       String filename = histogramRestart.toString().replaceFirst("\\.his$", ".lam");
359       lambdaRestart = new File(filename);
360     }
361     LambdaData lambdaData = LambdaData.readLambdaData(lambdaRestart);
362 
363     OrthogonalSpaceTempering orthogonalSpaceTempering = new OrthogonalSpaceTempering(refinementEnergy,
364         refinementEnergy, histogramData, lambdaData, props, dynamicsOptions, lambdaParticleOptions, algorithmListener);
365 
366     orthogonalSpaceTempering.setLambda(lambda);
367     orthogonalSpaceTempering.getOptimizationParameters().setOptimization(true, activeAssembly);
368     // Create the MolecularDynamics instance.
369     MolecularDynamics molDyn = new MolecularDynamics(assemblies[0],
370         orthogonalSpaceTempering, algorithmListener, thermostat, integrator);
371 
372     algorithmFunctions.energy(assemblies[0]);
373 
374     molDyn.dynamic(dynamicsOptions.getSteps(), dynamicsOptions.getDt(), dynamicsOptions.getReport(),
375         dynamicsOptions.getWrite(), dynamicsOptions.getTemperature(), true,
376         fileType, dynamicsOptions.getWrite(), dyn);
377 
378     logger.info(" Searching for low energy coordinates");
379     OptimizationParameters opt = orthogonalSpaceTempering.getOptimizationParameters();
380     double[] lowEnergyCoordinates = opt.getOptimumCoordinates();
381     double currentOSTOptimum = opt.getOptimumEnergy();
382     if (lowEnergyCoordinates != null) {
383       forceFieldEnergy.setCoordinates(lowEnergyCoordinates);
384     } else {
385       logger.info(" OST stage did not succeed in finding a minimum.");
386     }
387 
388     return this;
389   }
390 
391   @Override
392   public List<Potential> getPotentials() {
393     return orthogonalSpaceTempering == null ? Collections.emptyList() :
394         Collections.singletonList(orthogonalSpaceTempering);
395   }
396 }