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-2024.
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.misc;
39  
40  import static java.lang.String.format;
41  
42  import ffx.algorithms.AlgorithmListener;
43  import ffx.numerics.Potential;
44  import ffx.potential.MolecularAssembly;
45  import ffx.potential.bonded.AminoAcidUtils;
46  import ffx.potential.bonded.Atom;
47  import ffx.potential.bonded.Residue;
48  import ffx.potential.bonded.AminoAcidUtils.AminoAcid3;
49  import ffx.potential.bonded.NucleicAcidUtils.NucleicAcid3;
50  import ffx.potential.bonded.Rotamer;
51  import ffx.potential.bonded.RotamerLibrary;
52  import ffx.potential.parsers.PDBFilter;
53  import java.io.BufferedWriter;
54  import java.io.File;
55  import java.io.FileWriter;
56  import java.io.IOException;
57  import java.util.Arrays;
58  import java.util.logging.Level;
59  import java.util.logging.Logger;
60  import java.util.regex.Matcher;
61  import java.util.regex.Pattern;
62  
63  /**
64   * The GenerateRotamers class helps generate a rotamer library (particularly for nonstandard amino
65   * acids) for a Residue.
66   *
67   * @author Jacob M. Litman
68   * @author Michael J. Schnieders
69   */
70  public class GenerateRotamers {
71  
72    private static final Logger logger = Logger.getLogger(GenerateRotamers.class.getName());
73    private static final Pattern atRangePatt = Pattern.compile("(\\d+)-(\\d+)");
74    private final Residue residue;
75    private final int nChi;
76    private final double[] currentChi;
77    private final File outFile;
78    private final MolecularAssembly molecularAssembly;
79    private final AlgorithmListener listener;
80    private final RotamerLibrary library;
81    private final Potential potential;
82    private double[] x;
83    private AminoAcid3 baselineAAres;
84    private Rotamer[] baselineRotamers;
85    private double incr = 10.0; // Degrees to rotate each torsion by.
86    private boolean aroundLibrary = false;
87    private double width = 180.0; // Left 180 + right 180 = 360 degree coverage.
88    private int startDepth = 0;
89    private int endDepth;
90    private boolean print = false; // If true, log energy at each torsion.
91    private int nEval = 0;
92    private boolean writeVideo = false;
93    private File videoFile;
94    private PDBFilter videoFilter;
95  
96    /**
97     * Intended to create rotamer sets for nonstandard amino acids.
98     *
99     * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
100    * @param potential a {@link ffx.numerics.Potential} object.
101    * @param residue a {@link ffx.potential.bonded.Residue} object.
102    * @param file Output file
103    * @param nChi Number of rotameric torsions in the residue
104    * @param listener a {@link ffx.algorithms.AlgorithmListener} object.
105    */
106   public GenerateRotamers(MolecularAssembly molecularAssembly, Potential potential, Residue residue,
107       File file, int nChi, AlgorithmListener listener) {
108     this(molecularAssembly, potential, residue, file, nChi, listener,
109         RotamerLibrary.getDefaultLibrary());
110   }
111 
112   /**
113    * Intended to create rotamer sets for nonstandard amino acids.
114    *
115    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
116    * @param potential a {@link ffx.numerics.Potential} object.
117    * @param residue a {@link ffx.potential.bonded.Residue} object.
118    * @param file Output file
119    * @param nChi Number of rotameric torsions in the residue
120    * @param listener a {@link ffx.algorithms.AlgorithmListener} object.
121    * @param library Rotamer library to use
122    */
123   public GenerateRotamers(MolecularAssembly molecularAssembly, Potential potential, Residue residue,
124       File file, int nChi, AlgorithmListener listener, RotamerLibrary library) {
125     this.residue = residue;
126     this.nChi = nChi;
127     endDepth = nChi - 1;
128     this.potential = potential;
129     this.molecularAssembly = molecularAssembly;
130     this.listener = listener;
131     this.currentChi = new double[nChi];
132     this.library = library;
133     Arrays.fill(currentChi, 0.0);
134     File outputFile = file;
135     if (outputFile.exists()) {
136       String outName = outputFile.getName();
137       for (int i = 1; i < 1000; i++) {
138         outputFile = new File(format("%s_%d", outName, i));
139         if (!outputFile.exists()) {
140           break;
141         }
142       }
143       if (outputFile.exists()) {
144         logger.severe(format(" Could not version file %s", outName));
145       }
146     }
147     outFile = outputFile;
148     this.baselineAAres = residue.getAminoAcid3();
149     baselineRotamers = library.getRotamers(baselineAAres);
150   }
151 
152   /**
153    * Accessory method for more simplistic saving of specific torsion states.
154    *
155    * @param torSets an array of {@link java.lang.String} objects.
156    */
157   public void applyAndSaveTorsions(String[] torSets) {
158     for (String torSet : torSets) {
159       String[] torsions = torSet.split(",");
160       double[] values = new double[nChi * 2];
161       Arrays.fill(values, 0.0);
162       for (int i = 0; i < (Math.min(torsions.length, nChi)); i++) {
163         double chival = Double.parseDouble(torsions[i]);
164         currentChi[i] = chival;
165         values[2 * i] = chival;
166       }
167       Rotamer newRot = generateRotamer(values);
168       RotamerLibrary.applyRotamer(residue, newRot);
169       writeSnapshot();
170     }
171   }
172 
173   /**
174    * Sets a standard amino acid to be the baseline for rotamer generation. For example, use TYR as a
175    * baseline for phospho-tyrosine.
176    *
177    * @param aa3 a {@link AminoAcid3} object.
178    */
179   public void setBaselineAARes(AminoAcid3 aa3) {
180     this.baselineAAres = aa3;
181     if (aa3 == AminoAcidUtils.AminoAcid3.UNK) {
182       boolean orig = library.getUsingOrigCoordsRotamer();
183       library.setUseOrigCoordsRotamer(false);
184       baselineRotamers = residue.setRotamers(library);
185       library.setUseOrigCoordsRotamer(orig);
186     } else {
187       baselineRotamers = library.getRotamers(aa3);
188     }
189     aroundLibrary = true;
190   }
191 
192   /**
193    * Set which torsions to work on. Negative end values set the final depth to be the total number of
194    * torsions.
195    *
196    * @param start The first torsion to work on.
197    * @param end The last torsion to work on.
198    */
199   public void setDepth(int start, int end) {
200     startDepth = start;
201     if (end < 1 || end > nChi) {
202       endDepth = nChi - 1;
203     } else {
204       endDepth = end;
205     }
206   }
207 
208   /**
209    * Inactivates electrostatics for atom sets defined by 'start-end,start-end,...'.
210    *
211    * @param electrostatics Input string
212    */
213   public void setElectrostatics(String electrostatics) {
214     if (electrostatics != null) {
215       String[] tokens = electrostatics.split(",");
216       Atom[] atoms = molecularAssembly.getAtomArray();
217       for (String tok : tokens) {
218         Matcher m = atRangePatt.matcher(tok);
219         if (m.matches()) {
220           int begin = Integer.parseInt(m.group(1));
221           int end = Integer.parseInt(m.group(2));
222           logger.info(format(" Inactivating electrostatics for atoms %d-%d", begin, end));
223           for (int i = begin; i <= end; i++) {
224             Atom ai = atoms[i - 1];
225             ai.setElectrostatics(false);
226             ai.print(Level.FINE);
227           }
228         } else {
229           logger.info(format(" Discarding electrostatics input %s", tok));
230         }
231       }
232     }
233   }
234 
235   /**
236    * Inactivates atom sets defined by 'start-end,start-end,...'.
237    *
238    * @param iatoms Input string
239    */
240   public void setInactiveAtoms(String iatoms) {
241     if (iatoms != null) {
242       String[] tokens = iatoms.split(",");
243       Atom[] atoms = molecularAssembly.getAtomArray();
244       for (String tok : tokens) {
245         Matcher m = atRangePatt.matcher(tok);
246         if (m.matches()) {
247           int begin = Integer.parseInt(m.group(1));
248           int end = Integer.parseInt(m.group(2));
249           logger.info(format(" Inactivating atoms %d-%d", begin, end));
250           for (int i = begin; i <= end; i++) {
251             Atom ai = atoms[i - 1];
252             ai.setUse(false);
253             ai.print(Level.FINE);
254           }
255         } else {
256           logger.info(format(" Discarding inactive atoms input %s", tok));
257         }
258       }
259     }
260   }
261 
262   /**
263    * Sets the angle to change torsions by.
264    *
265    * @param incr a double.
266    */
267   public void setIncrement(double incr) {
268     this.incr = incr;
269   }
270 
271   /**
272    * Sets algorithm to log all torsions/energies (not just to file).
273    *
274    * @param print a boolean.
275    */
276   public void setPrint(boolean print) {
277     this.print = print;
278   }
279 
280   /**
281    * Sets the width around each torsion to search (+/-, so 10 degree width will search a 20 degree
282    * arc).
283    *
284    * @param width a double.
285    */
286   public void setSearchWidth(double width) {
287     this.width = width;
288   }
289 
290   /**
291    * Null file indicates to not write a video.
292    *
293    * @param videoFile Filename for video or null
294    */
295   public void setVideo(String videoFile) {
296     if (videoFile != null) {
297       File vidFile = new File(videoFile);
298       if (vidFile.exists()) {
299         for (int i = 0; i < 1000; i++) {
300           vidFile = new File(format("%s_%d", videoFile, i));
301           if (!vidFile.exists()) {
302             this.videoFile = vidFile;
303             writeVideo = true;
304             videoFilter = new PDBFilter(this.videoFile, molecularAssembly,
305                 molecularAssembly.getForceField(), null);
306             videoFilter.setLogWrites(false);
307             break;
308           }
309         }
310         if (vidFile.exists()) {
311           logger.warning(format(" Could not version video file %s", videoFile));
312         }
313       } else {
314         this.videoFile = vidFile;
315         writeVideo = true;
316         videoFilter = new PDBFilter(this.videoFile, molecularAssembly,
317             molecularAssembly.getForceField(), null);
318         videoFilter.setLogWrites(false);
319       }
320     } else {
321       writeVideo = false;
322     }
323   }
324 
325   /** Main driver method; spins torsions, evaluates energy, and prints to file. */
326   public void tryRotamers() {
327     try (BufferedWriter bw = new BufferedWriter(new FileWriter(outFile))) {
328       if (aroundLibrary) {
329         for (Rotamer rotamer : baselineRotamers) {
330           applyLibRotamer(rotamer);
331           turnChi(startDepth, bw);
332         }
333       } else {
334         RotamerLibrary.measureRotamer(residue, currentChi, false);
335         turnChi(startDepth, bw);
336       }
337     } catch (IOException ex) {
338       logger.warning(format(" IO exception in rotamer generation: %s", ex));
339     }
340   }
341 
342   /**
343    * Applies a library rotamer, filling in the end with 0s as necessary. For example, PTR requires
344    * more torsions than TYR, so one cannot simply apply a TYR rotamer to a PTR residue.
345    *
346    * @param rotamer The rotamer to apply.
347    */
348   private void applyLibRotamer(Rotamer rotamer) {
349     double[] rotValues = new double[nChi * 2];
350     Arrays.fill(rotValues, 0.0);
351     Arrays.fill(currentChi, 0.0);
352     int fillTo = Math.min(startDepth, (rotamer.length - 1));
353     for (int i = 0; i < fillTo; i++) {
354       int ii = 2 * i;
355       rotValues[ii] = rotamer.angles[i];
356       currentChi[i] = rotamer.angles[i];
357       rotValues[ii + 1] = rotamer.sigmas[i];
358     }
359     Rotamer newRot = generateRotamer(rotValues);
360     RotamerLibrary.applyRotamer(residue, newRot);
361   }
362 
363   /**
364    * Generates an aa/na/unk Rotamer for the selected residue given values.
365    *
366    * @param values The values to use.
367    * @return The Rotamer.
368    */
369   private Rotamer generateRotamer(double[] values) {
370     switch (residue.getResidueType()) {
371       case AA -> {
372         AminoAcid3 aa3 = residue.getAminoAcid3();
373         return new Rotamer(aa3, values);
374       }
375       case NA -> {
376         NucleicAcid3 na3 = residue.getNucleicAcid3();
377         return new Rotamer(na3, values);
378       }
379       default -> {
380         return new Rotamer(values);
381       }
382     }
383   }
384 
385   /**
386    * Recursive method for turning the torsions.
387    *
388    * @param depth Current depth of the recursion
389    * @param bw The BufferedWriter to write to.
390    * @throws IOException If there is an IO exception.
391    */
392   private void turnChi(int depth, BufferedWriter bw) throws IOException {
393     double chi = currentChi[depth];
394     for (double i = chi - width; i <= chi + width; i += incr) {
395       currentChi[depth] = i;
396       if (depth == endDepth) {
397         evaluateChi(bw);
398         if (listener != null) {
399           listener.algorithmUpdate(molecularAssembly);
400         }
401         if (writeVideo) {
402           writeSnapshot();
403         }
404       } else {
405         turnChi(depth + 1, bw);
406       }
407     }
408     currentChi[depth] = chi; // Reset the chi value to where it was.
409   }
410 
411   private void writeSnapshot() {
412     try (BufferedWriter bw = new BufferedWriter(new FileWriter(videoFile, true))) {
413       bw.write(format("MODEL %d", nEval));
414       bw.newLine();
415       bw.write(format("REMARK 301 TORSIONS %s", formatChi()));
416       bw.newLine();
417       bw.flush();
418       videoFilter.writeFile(videoFile, true);
419       bw.write("ENDMDL");
420       bw.newLine();
421       bw.flush();
422     } catch (IOException ex) {
423       logger.warning(
424           format(" Exception writing to video file %s: " + "%s", videoFile.getName(), ex));
425     }
426   }
427 
428   /**
429    * Called at a leaf of the recursion.
430    *
431    * @param bw The BufferedWriter to write to.
432    * @throws IOException If there is an IO exception.
433    */
434   private void evaluateChi(BufferedWriter bw) throws IOException {
435     try {
436       applyChi();
437       double e = currentEnergy();
438       String result = format("%s,%12f", formatChi(), e);
439       ++nEval;
440       if (print) {
441         logger.info(format(" Evaluation %10d %s, energy %10.5f kcal/mol", nEval, formatChi(), e));
442       }
443       bw.write(result);
444       bw.newLine();
445       if (nEval % 1000 == 0) {
446         logger.info(format(" %12.7e states evaluated", (double) nEval));
447       }
448     } catch (ArithmeticException ex) {
449       logger.info(format(" Force field exception at chi %s", formatChi()));
450     }
451   }
452 
453   /** Converts the chi array to a Rotamer and applies it. */
454   private void applyChi() {
455     double[] rotValues = new double[nChi * 2];
456     for (int i = 0; i < nChi; i++) {
457       int ii = 2 * i;
458       rotValues[ii] = currentChi[i];
459       rotValues[ii + 1] = 0.0;
460     }
461     RotamerLibrary.applyRotamer(residue, generateRotamer(rotValues));
462   }
463 
464   /**
465    * Returns a formatted String with chi values.
466    *
467    * @return A formatted String with chi values.
468    */
469   private String formatChi() {
470     StringBuilder sb = new StringBuilder(format("%8f", currentChi[0]));
471     for (int i = 1; i < nChi; i++) {
472       // sb.append(",").append(currentChi[i]);
473       sb.append(format(",%8f", currentChi[i]));
474     }
475     return sb.toString();
476   }
477 
478   /**
479    * Calculates the energy at the current state.
480    *
481    * @return Energy of the current state.
482    */
483   private double currentEnergy() throws ArithmeticException {
484     if (x == null) {
485       int nVar = potential.getNumberOfVariables();
486       x = new double[nVar * 3];
487     }
488     potential.getCoordinates(x);
489     return potential.energy(x);
490   }
491 }