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-2026.
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;
39  
40  import ffx.algorithms.cli.AlgorithmsCommand;
41  import ffx.algorithms.cli.ManyBodyOptions;
42  import ffx.algorithms.optimize.RotamerOptimization;
43  import ffx.algorithms.optimize.TitrationManyBody;
44  import ffx.numerics.Potential;
45  import ffx.potential.ForceFieldEnergy;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.bonded.Atom;
48  import ffx.potential.bonded.LambdaInterface;
49  import ffx.potential.bonded.Residue;
50  import ffx.potential.bonded.Rotamer;
51  import ffx.potential.bonded.RotamerLibrary;
52  import ffx.potential.cli.AlchemicalOptions;
53  import ffx.potential.parameters.ForceField;
54  import ffx.potential.parsers.PDBFilter;
55  import ffx.utilities.FFXBinding;
56  import ffx.xray.DiffractionData;
57  import ffx.xray.RefinementEnergy;
58  import ffx.xray.refine.RefinementMode;
59  import ffx.xray.cli.XrayOptions;
60  import org.apache.commons.configuration2.CompositeConfiguration;
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.io.FileWriter;
68  import java.io.IOException;
69  import java.util.ArrayList;
70  import java.util.Arrays;
71  import java.util.HashSet;
72  import java.util.List;
73  import java.util.Set;
74  import java.util.stream.Collectors;
75  
76  import static ffx.potential.bonded.NamingUtils.renameAtomsToPDBStandard;
77  import static java.lang.String.format;
78  
79  /**
80   * The GenZ script for calculating free energy changes.
81   * <br>
82   * Usage:
83   * <br>
84   * ffxc xray.GenZ [options] &lt;filename&gt;
85   */
86  @Command(description = " Run GenZ function for free energy change.", name = "xray.GenZ")
87  public class GenZ extends AlgorithmsCommand {
88  
89    @Mixin
90    private ManyBodyOptions manyBodyOptions;
91  
92    @Mixin
93    private AlchemicalOptions alchemicalOptions;
94  
95    @Mixin
96    private XrayOptions xrayOptions;
97  
98    @Option(names = {"--rEE", "--ro-ensembleEnergy"}, paramLabel = "0.0",
99        description = "Keep permutations within ensemble Energy kcal/mol from the GMEC.")
100   private String ensembleEnergy = "0.0";
101 
102   @Option(names = {"--pF", "--printFiles"}, paramLabel = "false",
103       description = "Write to an energy restart file and ensemble file.")
104   private boolean printFiles = false;
105 
106   @Option(names = {"--pKa"}, paramLabel = "false",
107       description = "Calculating protonation populations for pKa shift.")
108   private boolean pKa = false;
109 
110   /**
111    * One or more filenames.
112    */
113   @Parameters(arity = "1..*", paramLabel = "files", description = "PDB and Real Space input files.")
114   private List<String> filenames;
115 
116   private RefinementEnergy refinementEnergy;
117   private ForceFieldEnergy potentialEnergy;
118   private MolecularAssembly[] molecularAssemblies;
119   private ForceField forceField;
120   private TitrationManyBody titrationManyBody;
121   private List<Residue> selectedResidues;
122   private MolecularAssembly[] conformerAssemblies = new MolecularAssembly[3];
123 
124   /**
125    * GenZ Constructor.
126    */
127   public GenZ() {
128     super();
129   }
130 
131   /**
132    * GenZ constructor that sets the command line arguments.
133    *
134    * @param args Command line arguments.
135    */
136   public GenZ(String[] args) {
137     super(args);
138   }
139 
140   /**
141    * GenZ Constructor.
142    *
143    * @param binding The Binding to use.
144    */
145   public GenZ(FFXBinding binding) {
146     super(binding);
147   }
148 
149   /**
150    * {@inheritDoc}
151    */
152   @Override
153   public GenZ run() {
154     if (!init()) {
155       return this;
156     }
157 
158     // Atomic clashes are expected and will be handled using direct induced dipoles.
159     System.setProperty("sor-scf-fallback", "false");
160     System.setProperty("direct-scf-fallback", "true");
161 
162     xrayOptions.init();
163     double titrationPH = manyBodyOptions.getTitrationPH();
164     double inclusionCutoff = manyBodyOptions.getInclusionCutoff();
165     int mutatingResidue = manyBodyOptions.getInterestedResidue();
166     boolean onlyTitration = manyBodyOptions.getOnlyTitration();
167     double pHRestraint = manyBodyOptions.getPHRestraint();
168     if (manyBodyOptions.getTitration()) {
169       System.setProperty("manybody-titration", "true");
170     }
171 
172     // Many-Body expansion of the X-ray target converges much more quickly with the NEA.
173     String nea = System.getProperty("native-environment-approximation", "true");
174     System.setProperty("native-environment-approximation", nea);
175 
176     boolean lambdaTerm = alchemicalOptions.hasSoftcore();
177     if (lambdaTerm) {
178       // Turn on softcore van der Waals
179       System.setProperty("lambdaterm", "true");
180       // Turn of alchemical electrostatics
181       System.setProperty("elec-lambdaterm", "false");
182       // Turn on intra-molecular softcore
183       System.setProperty("intramolecular-softcore", "true");
184     }
185     System.setProperty("ro-ensembleEnergy", ensembleEnergy);
186 
187     String modelFilename;
188     if (filenames != null && !filenames.isEmpty()) {
189       molecularAssemblies = algorithmFunctions.openAll(filenames.get(0));
190       activeAssembly = molecularAssemblies[0];
191       modelFilename = filenames.get(0);
192     } else if (activeAssembly == null) {
193       logger.info(helpString());
194       return this;
195     } else {
196       molecularAssemblies = new MolecularAssembly[]{activeAssembly};
197       modelFilename = activeAssembly.getFile().getAbsolutePath();
198     }
199 
200     CompositeConfiguration properties = activeAssembly.getProperties();
201     activeAssembly.getPotentialEnergy().setPrintOnFailure(false, false);
202     potentialEnergy = activeAssembly.getPotentialEnergy();
203     forceField = activeAssembly.getForceField();
204 
205     if (forceField == null) {
206       logger.info("This force field is null");
207     }
208 
209     String filename = filenames.get(0);
210 
211     List<Residue> titrateResidues = new ArrayList<>();
212 
213     // Prepare variables for saving out the highest population rotamers (optimal rotamers)
214     Set<Atom> excludeAtoms = new HashSet<>();
215     boolean isTitrating = false;
216 
217     // The refinement mode must be coordinates.
218     if (xrayOptions.refinementMode != RefinementMode.COORDINATES) {
219       logger.info(" Refinement mode set to COORDINATES.");
220       xrayOptions.refinementMode = RefinementMode.COORDINATES;
221     }
222 
223     // Collect residues to optimize.
224     List<Residue> residues = manyBodyOptions.collectResidues(activeAssembly);
225     if (residues == null || residues.isEmpty()) {
226       logger.info(" There are no residues in the active system to optimize.");
227       return this;
228     }
229 
230     String[] titratableResidues = {"HIS", "HIE", "HID", "GLU", "GLH", "ASP", "ASH", "LYS", "LYD", "CYS", "CYD"};
231     List<String> titratableResiudesList = Arrays.asList(titratableResidues);
232     double boltzmannWeights;
233     double offsets;
234     double[][] populationArray = new double[1][55];
235     double[][] titrateBoltzmann;
236     double[] protonationBoltzmannSums;
237     double totalBoltzmann = 0;
238     List<Residue> residueList = activeAssembly.getResidueList();
239 
240     String listResidues = "";
241     // Select residues with alpha carbons within the inclusion cutoff or
242     // Select only the titrating residues or the titrating residues and those within the inclusion cutoff
243     if (inclusionCutoff != -1 || onlyTitration) {
244       listResidues = manyBodyOptions.selectInclusionResidues(residueList, mutatingResidue, onlyTitration, inclusionCutoff);
245     }
246 
247     List<Integer> residueNumber = new ArrayList<>();
248     for (Residue residue : residueList) {
249       residueNumber.add(residue.getResidueNumber());
250     }
251 
252     // Application of rotamers uses side-chain atom naming from the PDB.
253     if (properties.getBoolean("standardizeAtomNames", false)) {
254       renameAtomsToPDBStandard(activeAssembly);
255     }
256 
257     // Handle rotamer optimization with titration.
258     if (manyBodyOptions.getTitration()) {
259       logger.info("\n Adding titration hydrogen to : " + filenames.get(0) + "\n");
260 
261       // Collect residue numbers.
262       List<Integer> resNumberList = new ArrayList<>();
263       for (Residue residue : residues) {
264         resNumberList.add(residue.getResidueNumber());
265       }
266 
267       // Create new MolecularAssembly with additional protons and update the ForceFieldEnergy
268       titrationManyBody = new TitrationManyBody(filename, activeAssembly.getForceField(),
269           resNumberList, titrationPH, manyBodyOptions);
270       activeAssembly = titrationManyBody.getProtonatedAssembly();
271       potentialEnergy = activeAssembly.getPotentialEnergy();
272     }
273 
274     // Load parsed X-ray properties.
275     xrayOptions.setProperties(parseResult, properties);
276     molecularAssemblies = new MolecularAssembly[]{activeAssembly};
277     // Set up the diffraction data, which could be multiple files.
278     DiffractionData diffractionData = xrayOptions.getDiffractionData(filenames, molecularAssemblies, properties);
279     refinementEnergy = xrayOptions.toXrayEnergy(diffractionData);
280     refinementEnergy.setScaling(null);
281 
282     // Selecting residues
283     if (!pKa || onlyTitration) {
284       manyBodyOptions.setListResidues(listResidues);
285     }
286 
287     if (lambdaTerm) {
288       alchemicalOptions.setFirstSystemAlchemistry(activeAssembly);
289       LambdaInterface lambdaInterface = (LambdaInterface) potentialEnergy;
290       double lambda = alchemicalOptions.getInitialLambda();
291       logger.info(format(" Setting ManyBody softcore lambda to: %5.3f", lambda));
292       lambdaInterface.setLambda(lambda);
293     }
294 
295     RotamerOptimization rotamerOptimization = new RotamerOptimization(activeAssembly, refinementEnergy, algorithmListener);
296     manyBodyOptions.initRotamerOptimization(rotamerOptimization, activeAssembly);
297     rotamerOptimization.setPrintFiles(printFiles);
298     rotamerOptimization.setWriteEnergyRestart(printFiles);
299     rotamerOptimization.setPHRestraint(pHRestraint);
300     rotamerOptimization.setpH(titrationPH);
301     double[] x = new double[refinementEnergy.getNumberOfVariables()];
302     x = refinementEnergy.getCoordinates(x);
303     double e = refinementEnergy.energy(x, true);
304     logger.info(format("\n Initial target energy: %16.8f ", e));
305 
306     selectedResidues = rotamerOptimization.getResidues();
307     rotamerOptimization.initFraction(selectedResidues);
308 
309     RotamerLibrary.measureRotamers(selectedResidues, false);
310 
311     rotamerOptimization.optimize(manyBodyOptions.getAlgorithm(selectedResidues.size()));
312 
313     int[] currentRotamers = new int[selectedResidues.size()];
314 
315     // Calculate possible permutations for assembly
316     try {
317       rotamerOptimization.getFractions(selectedResidues.toArray(new Residue[0]), 0, currentRotamers);
318       if (pKa) {
319         rotamerOptimization.getProtonationPopulations(selectedResidues.toArray(new Residue[0]));
320       }
321     } catch (Exception ex) {
322       logger.severe(" Error calculating rotamer fractions: " + ex.getMessage());
323       return this;
324     }
325 
326     // Collect the Bolztmann weights and calculated offset of each assembly
327     boltzmannWeights = rotamerOptimization.getTotalBoltzmann();
328     offsets = rotamerOptimization.getRefEnergy();
329 
330     // Calculate the populations for the all residue rotamers
331     populationArray = rotamerOptimization.getFraction();
332 
333     // optimalRotamers = rotamerOptimization.getOptimumRotamers()
334     // if (manyBodyOptions.getTitration()) {
335     //     isTitrating = titrationManyBody.excludeExcessAtoms(excludeAtoms, optimalRotamers, selectedResidues)
336     // }
337 
338     try (FileWriter fileWriter = new FileWriter("populations.txt")) {
339       int residueIndex = 0;
340       for (Residue residue : selectedResidues) {
341         fileWriter.write("\n");
342         Rotamer[] rotamers = residue.getRotamers();
343         for (int rotIndex = 0; rotIndex < rotamers.length; rotIndex++) {
344           String rotPop = format("%.6f", populationArray[residueIndex][rotIndex]);
345           fileWriter.write(residue.getName() + residue.getResidueNumber() + "\t" +
346               rotamers[rotIndex].toString() + "\t" + rotPop + "\n");
347         }
348         residueIndex += 1;
349       }
350     } catch (IOException ioException) {
351       logger.warning("Error writing populations file: " + ioException.getMessage());
352     }
353 
354     System.out.println("\n Successfully wrote to the populations file.");
355 
356     int[][] conformers;
357     try {
358       conformers = rotamerOptimization.getConformers();
359     } catch (Exception ex) {
360       logger.severe(" Error getting conformers: " + ex.getMessage());
361       return this;
362     }
363 
364     char[] altLocs = new char[]{'C', 'B', 'A'};
365     List<String> residueChainNum = new ArrayList<>();
366     for (Residue residue : activeAssembly.getResidueList()) {
367       char chain = residue.getChainID();
368       int resNum = residue.getResidueNumber();
369       residueChainNum.add(String.valueOf(chain) + resNum);
370     }
371 
372     File structureFile = new File(filename);
373     // List<String> rotNames = new ArrayList<>()
374     int[] optimalRotamers = new int[selectedResidues.size()];
375     int assemblyIndex = 0;
376     String[] rotNames = new String[selectedResidues.size()];
377     for (int confIndex = 2; confIndex > -1; confIndex--) {
378       List<Residue> conformerResidueList = new ArrayList<>();
379       MolecularAssembly conformerAssembly = algorithmFunctions.open(filename);
380       if (manyBodyOptions.getTitration()) {
381         logger.info("\n Adding titration hydrogen to : " + filenames.get(0) + "\n");
382 
383         // Collect residue numbers.
384         List<Integer> resNumberList = new ArrayList<>();
385         for (Residue residue : residues) {
386           resNumberList.add(residue.getResidueNumber());
387         }
388 
389         // Create new MolecularAssembly with additional protons and update the ForceFieldEnergy
390         titrationManyBody = new TitrationManyBody(filename, activeAssembly.getForceField(),
391             resNumberList, titrationPH, manyBodyOptions);
392         conformerAssembly = titrationManyBody.getProtonatedAssembly();
393         potentialEnergy = conformerAssembly.getPotentialEnergy();
394       }
395       for (int resIndex = 0; resIndex < selectedResidues.size(); resIndex++) {
396         Residue residueSelect = selectedResidues.get(resIndex);
397         String resChainNum = String.valueOf(residueSelect.getChainID()) + residueSelect.getResidueNumber();
398         int index = residueChainNum.indexOf(resChainNum);
399         Residue residue = conformerAssembly.getResidueList().get(index);
400         conformerResidueList.add(residue);
401         residue.setRotamers(manyBodyOptions.getRotamerLibrary(true));
402         Rotamer[] rotamers = residue.getRotamers();
403         int rotIndex = conformers[resIndex][confIndex];
404         if (populationArray[resIndex][rotIndex] != 0) {
405           optimalRotamers[resIndex] = rotIndex;
406           RotamerLibrary.applyRotamer(residue, rotamers[rotIndex]);
407           double occupancy = populationArray[resIndex][rotIndex];
408           boolean diffStates = false;
409           for (int i = 2; i > -1; i--) {
410             int rotamerInd = conformers[resIndex][i];
411             String rotName = rotamers[rotamerInd].getName();
412             double occupancyTest = populationArray[resIndex][rotamerInd];
413             if (i == 2 && rotNames[resIndex] != null) {
414               rotNames[resIndex] = rotName;
415             } else if (i < 2 && occupancyTest != 0 && !rotNames[resIndex].contains(rotName)) {
416               diffStates = true;
417               String newString = rotNames[resIndex] + rotName;
418               logger.info(newString);
419               rotNames[resIndex] = newString;
420             } else if (i == 2) {
421               rotNames[resIndex] = rotName;
422             }
423           }
424           for (Atom atom : residue.getAtomList()) {
425             if (!residue.getBackboneAtoms().contains(atom) || diffStates) {
426               if (occupancy != 1) {
427                 atom.setAltLoc(altLocs[confIndex]);
428                 atom.setOccupancy(occupancy);
429               } else {
430                 atom.setOccupancy(occupancy);
431                 atom.setAltLoc(' ');
432               }
433             } else {
434               atom.setOccupancy(1.0);
435               atom.setAltLoc(' ');
436             }
437           }
438         }
439       }
440 
441       conformerAssemblies[assemblyIndex] = conformerAssembly;
442       if (manyBodyOptions.getTitration()) {
443         titrationManyBody.excludeExcessAtoms(excludeAtoms, optimalRotamers, conformerAssemblies[assemblyIndex], conformerResidueList);
444         excludeAtoms.addAll(titrationManyBody.getExcludeAtoms());
445       }
446       assemblyIndex++;
447       // Calculate the protonation populations
448       if (pKa) {
449         rotamerOptimization.getProtonationPopulations(selectedResidues.toArray(new Residue[0]));
450       }
451     }
452 
453     // Print information from the fraction protonated calculations
454     try (FileWriter popFileWriter = new FileWriter("populations.txt")) {
455       int residueIndex = 0;
456       for (Residue residue : selectedResidues) {
457         popFileWriter.write("\n");
458         protonationBoltzmannSums = new double[selectedResidues.size()];
459         // Set sums for to protonated, deprotonated, and tautomer states of titratable residues
460         Rotamer[] rotamers = residue.getRotamers();
461         for (int rotIndex = 0; rotIndex < rotamers.length; rotIndex++) {
462           String rotPop = format("%.6f", populationArray[residueIndex][rotIndex]);
463           popFileWriter.write(residue.getName() + residue.getResidueNumber() + "\t" +
464               rotamers[rotIndex].toString() + "\t" + rotPop + "\n");
465         }
466         residueIndex += 1;
467       }
468     } catch (IOException ioException) {
469       logger.warning("Error writing populations file: " + ioException.getMessage());
470     }
471 
472     System.out.println("\n Successfully wrote to the populations file.");
473 
474     PDBFilter pdbFilter = new PDBFilter(structureFile, Arrays.asList(conformerAssemblies), forceField, properties);
475     pdbFilter.writeFile(structureFile, false, excludeAtoms, true, true);
476 
477     System.setProperty("standardizeAtomNames", "false");
478 
479     return this;
480   }
481 
482   @Override
483   public List<Potential> getPotentials() {
484     if (conformerAssemblies == null) {
485       return new ArrayList<>();
486     } else {
487       return Arrays.stream(conformerAssemblies)
488           .filter(a -> a != null)
489           .map(MolecularAssembly::getPotentialEnergy)
490           .filter(e -> e != null)
491           .collect(Collectors.toList());
492     }
493   }
494 }