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