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.potential.parameters;
39  
40  import ffx.utilities.FFXProperty;
41  import org.w3c.dom.Document;
42  import org.w3c.dom.Element;
43  
44  import javax.annotation.Nullable;
45  import java.util.Arrays;
46  import java.util.Comparator;
47  import java.util.HashMap;
48  import java.util.Map;
49  import java.util.logging.Level;
50  import java.util.logging.Logger;
51  
52  import static ffx.potential.parameters.ForceField.ForceFieldType.IMPROPER;
53  import static ffx.potential.parameters.ForceField.ForceFieldType.TORSION;
54  import static ffx.utilities.Constants.DEGREES_PER_RADIAN;
55  import static ffx.utilities.Constants.KCAL_TO_KJ;
56  import static ffx.utilities.PropertyGroup.EnergyUnitConversion;
57  import static ffx.utilities.PropertyGroup.PotentialFunctionParameter;
58  import static java.lang.Double.parseDouble;
59  import static java.lang.Integer.parseInt;
60  import static java.lang.String.format;
61  import static java.util.Arrays.copyOf;
62  import static org.apache.commons.math3.util.FastMath.cos;
63  import static org.apache.commons.math3.util.FastMath.sin;
64  import static org.apache.commons.math3.util.FastMath.toRadians;
65  
66  /**
67   * The TorsionType class defines a torsional angle.
68   *
69   * @author Michael J. Schnieders
70   * @since 1.0
71   */
72  @FFXProperty(name = "improper", clazz = String.class, propertyGroup = PotentialFunctionParameter, description = """
73      [4 integers and 2 reals]"
74      Provides the values for a single CHARMM-style improper dihedral angle parameter.
75      The integer modifiers give the atom class numbers for the four kinds of atoms involved in the torsion which is to be defined.
76      The real number modifiers give the force constant value for the deviation from the target improper torsional angle, and the target value for the torsional angle, respectively.
77      The default units for the improper force constant are kcal/mole/radian^2, but this can be controlled via the impropunit keyword.
78      """)
79  @FFXProperty(name = "torsion", clazz = String.class, propertyGroup = PotentialFunctionParameter, description = """
80      [4 integers and up to 6 real/real/integer triples]
81      Provides the values for a single torsional angle parameter.
82      The first four integer modifiers give the atom class numbers for the atoms involved in the torsional angle to be defined.
83      Each of the remaining triples of real/real/integer modifiers give the amplitude,
84      phase offset in degrees and periodicity of a particular torsional function term, respectively.
85      Periodicities through 6-fold are allowed for torsional parameters.
86      """)
87  public final class TorsionType extends BaseType implements Comparator<String> {
88  
89    private static final Logger logger = Logger.getLogger(TorsionType.class.getName());
90    /**
91     * Atom classes that for this Torsion angle.
92     */
93    public final int[] atomClasses;
94    /**
95     * Number of terms in the Fourier series.
96     */
97    public final int terms;
98    /**
99     * Amplitudes of the Fourier series.
100    */
101   public final double[] amplitude;
102   /**
103    * Phases of the Fourier series in degrees.
104    */
105   public final double[] phase;
106   /**
107    * Cosine of the phase angle.
108    */
109   public final double[] cosine;
110   /**
111    * Sine of the phase angle.
112    */
113   public final double[] sine;
114   /**
115    * Periodicity of the Fourier series.
116    */
117   public final int[] periodicity;
118   /**
119    * The torsion mode in use.
120    */
121   private final TorsionMode torsionMode;
122 
123   public static final double DEFAULT_TORSION_UNIT = 1.0;
124   /**
125    * Unit conversion.
126    */
127   @FFXProperty(name = "torsionunit", propertyGroup = EnergyUnitConversion, defaultValue = "1.0", description = """
128       Sets the scale factor needed to convert the energy value computed by the torsional angle potential into units of kcal/mole.
129       The correct value is force field dependent and typically provided in the header of the master force field parameter file.
130       """)
131   public double torsionUnit = DEFAULT_TORSION_UNIT;
132 
133   /**
134    * TorsionType Constructor.
135    *
136    * @param atomClasses Atom classes.
137    * @param amplitude   Amplitudes of the Fourier series.
138    * @param phase       Phases of the Fourier series in degrees.
139    * @param periodicity Periodicity of the Fourier series.
140    */
141   public TorsionType(int[] atomClasses, double[] amplitude, double[] phase, int[] periodicity) {
142     this(atomClasses, amplitude, phase, periodicity, TorsionMode.NORMAL);
143   }
144 
145   /**
146    * TorsionType Constructor.
147    *
148    * @param atomClasses Atom classes.
149    * @param amplitude   Amplitudes of the Fourier series.
150    * @param phase       Phases of the Fourier series in degrees.
151    * @param periodicity Periodicity of the Fourier series.
152    * @param torsionMode Define the TorsionMode for this TorsionType.
153    */
154   public TorsionType(int[] atomClasses, double[] amplitude, double[] phase, int[] periodicity,
155                      TorsionMode torsionMode) {
156     super(TORSION, sortKey(atomClasses));
157     this.atomClasses = atomClasses;
158     int max = 1;
159     for (int i1 : periodicity) {
160       if (i1 > max) {
161         max = i1;
162       }
163     }
164     terms = max;
165     this.amplitude = new double[terms];
166     this.phase = new double[terms];
167     this.periodicity = new int[terms];
168     for (int i = 0; i < terms; i++) {
169       this.periodicity[i] = i + 1;
170     }
171     for (int i = 0; i < amplitude.length; i++) {
172       int j = periodicity[i] - 1;
173       if (j >= 0 && j < terms) {
174         this.amplitude[j] = amplitude[i];
175         this.phase[j] = phase[i];
176       }
177     }
178 
179     cosine = new double[terms];
180     sine = new double[terms];
181     for (int i = 0; i < terms; i++) {
182       double angle = toRadians(this.phase[i]);
183       cosine[i] = cos(angle);
184       sine[i] = sin(angle);
185     }
186 
187     this.torsionMode = torsionMode;
188     if (torsionMode == TorsionMode.IMPROPER) {
189       forceFieldType = IMPROPER;
190     }
191   }
192 
193   /**
194    * Average two TorsionTypes.
195    *
196    * @param torsionType1 First TorsionType.
197    * @param torsionType2 Second TorsionType.
198    * @param atomClasses  Atom classes for the new TorsionType.
199    * @return A new TorsionType that is the average of the two input types, or null if the inputs are invalid.
200    */
201   public static TorsionType average(@Nullable TorsionType torsionType1,
202                                     @Nullable TorsionType torsionType2,
203                                     @Nullable int[] atomClasses) {
204     if (torsionType1 == null || torsionType2 == null || atomClasses == null) {
205       return null;
206     }
207 
208     int len = torsionType1.amplitude.length;
209     if (len != torsionType2.amplitude.length) {
210       return null;
211     }
212     double[] amplitude = new double[len];
213     double[] phase = new double[len];
214     int[] periodicity = new int[len];
215     for (int i = 0; i < len; i++) {
216       amplitude[i] = (torsionType1.amplitude[i] + torsionType2.amplitude[i]) / 2.0;
217       phase[i] = (torsionType1.phase[i] + torsionType2.phase[i]) / 2.0;
218       periodicity[i] = (torsionType1.periodicity[i] + torsionType2.periodicity[i]) / 2;
219     }
220 
221     return new TorsionType(atomClasses, amplitude, phase, periodicity);
222   }
223 
224   /**
225    * Construct a TorsionType from an input string.
226    *
227    * @param input  The overall input String.
228    * @param tokens The input String tokenized.
229    * @return a TorsionType instance.
230    */
231   public static TorsionType parse(String input, String[] tokens) {
232     if (tokens.length < 5) {
233       logger.log(Level.WARNING, "Invalid TORSION type:\n{0}", input);
234     } else {
235       try {
236         int[] atomClasses = new int[4];
237         atomClasses[0] = parseInt(tokens[1]);
238         atomClasses[1] = parseInt(tokens[2]);
239         atomClasses[2] = parseInt(tokens[3]);
240         atomClasses[3] = parseInt(tokens[4]);
241         int terms = (tokens.length - 5) / 3;
242         double[] amplitude = new double[terms];
243         double[] phase = new double[terms];
244         int[] periodicity = new int[terms];
245         int index = 5;
246         for (int i = 0; i < terms; i++) {
247           amplitude[i] = parseDouble(tokens[index++]);
248           phase[i] = parseDouble(tokens[index++]);
249           periodicity[i] = parseInt(tokens[index++]);
250         }
251         return new TorsionType(atomClasses, amplitude, phase, periodicity);
252       } catch (NumberFormatException e) {
253         String message = "NumberFormatException parsing TORSION type:\n" + input + "\n";
254         logger.log(Level.SEVERE, message, e);
255       } catch (Exception e) {
256         String message = "Exception parsing TORSION type:\n" + input + "\n";
257         logger.log(Level.SEVERE, message, e);
258       }
259     }
260     return null;
261   }
262 
263   /**
264    * Construct a TorsionType with <code>TorsionMode.IMPROPER</code> from an input string.
265    *
266    * @param input  The overall input String.
267    * @param tokens The input String tokenized.
268    * @return a TorsionType instance.
269    */
270   public static TorsionType parseImproper(String input, String[] tokens) {
271     if (tokens.length < 5) {
272       logger.log(Level.WARNING, "Invalid IMPROPER type:\n{0}", input);
273     } else {
274       try {
275         int[] atomClasses = new int[4];
276         atomClasses[0] = parseInt(tokens[1]);
277         atomClasses[1] = parseInt(tokens[2]);
278         atomClasses[2] = parseInt(tokens[3]);
279         atomClasses[3] = parseInt(tokens[4]);
280         double[] amplitude = new double[1];
281         double[] phase = new double[1];
282         int[] periodicity = new int[1];
283         int index = 5;
284         amplitude[0] = parseDouble(tokens[index++]);
285         phase[0] = parseDouble(tokens[index]);
286         periodicity[0] = 1;
287         return new TorsionType(atomClasses, amplitude, phase, periodicity, TorsionMode.IMPROPER);
288       } catch (NumberFormatException e) {
289         String message = "Exception parsing IMPROPER type:\n" + input + "\n";
290         logger.log(Level.SEVERE, message, e);
291       }
292     }
293     return null;
294   }
295 
296   /**
297    * This method sorts the atom classes for the torsion.
298    *
299    * @param c atomClasses
300    * @return lookup key
301    * @since 1.0
302    */
303   public static String sortKey(int[] c) {
304     if (c == null || c.length != 4) {
305       return null;
306     }
307     if (c[1] < c[2]) {
308       // Do nothing.
309     } else if (c[2] < c[1]) {
310       // Reverse the order.
311       int temp = c[0];
312       c[0] = c[3];
313       c[3] = temp;
314       temp = c[1];
315       c[1] = c[2];
316       c[2] = temp;
317     } else if (c[1] == c[2]) {
318       if (c[0] > c[3]) {
319         // Reverse the order.
320         int temp = c[0];
321         c[0] = c[3];
322         c[3] = temp;
323       }
324     } else if (c[0] <= c[3]) {
325       // Do nothing.
326     } else {
327       // Reverse the order.
328       int temp = c[0];
329       c[0] = c[3];
330       c[3] = temp;
331       temp = c[1];
332       c[1] = c[2];
333       c[2] = temp;
334     }
335 
336     return c[0] + " " + c[1] + " " + c[2] + " " + c[3];
337   }
338 
339   /**
340    * {@inheritDoc}
341    *
342    * @since 1.0
343    */
344   @Override
345   public int compare(String s1, String s2) {
346     String[] keys1 = s1.split(" ");
347     String[] keys2 = s2.split(" ");
348     int[] c1 = new int[4];
349     int[] c2 = new int[4];
350 
351     for (int i = 0; i < 4; i++) {
352       c1[i] = parseInt(keys1[i]);
353       c2[i] = parseInt(keys2[i]);
354     }
355 
356     if (c1[1] < c2[1]) {
357       return -1;
358     } else if (c1[1] > c2[1]) {
359       return 1;
360     } else if (c1[2] < c2[2]) {
361       return -1;
362     } else if (c1[2] > c2[2]) {
363       return 1;
364     } else if (c1[0] < c2[0]) {
365       return -1;
366     } else if (c1[0] > c2[0]) {
367       return 1;
368     } else if (c1[3] < c2[3]) {
369       return -1;
370     } else if (c1[3] > c2[3]) {
371       return 1;
372     }
373 
374     return 0;
375   }
376 
377   /**
378    * {@inheritDoc}
379    *
380    * <p>Override the default <code>equals</code> method.
381    *
382    * @since 1.0
383    */
384   @Override
385   public boolean equals(Object o) {
386     if (this == o) {
387       return true;
388     }
389     if (o == null || getClass() != o.getClass()) {
390       return false;
391     }
392     TorsionType torsionType = (TorsionType) o;
393     return Arrays.equals(atomClasses, torsionType.atomClasses);
394   }
395 
396   /**
397    * {@inheritDoc}
398    *
399    * <p>Implementation of the <code>hashCode</code> method.
400    *
401    * @since 1.0
402    */
403   @Override
404   public int hashCode() {
405     return Arrays.hashCode(atomClasses);
406   }
407 
408   /**
409    * Increment the atom classes by a specified amount.
410    *
411    * @param increment The increment to add to the atom classes.
412    */
413   public void incrementClasses(int increment) {
414     for (int i = 0; i < atomClasses.length; i++) {
415       if (atomClasses[i] != 0) {
416         atomClasses[i] += increment;
417       }
418     }
419     setKey(sortKey(atomClasses));
420   }
421 
422   /**
423    * Remap new atom classes to known internal ones.
424    *
425    * @param typeMap a lookup between new atom types and known atom types.
426    * @return a {@link ffx.potential.parameters.TorsionType} object.
427    */
428   public TorsionType patchClasses(HashMap<AtomType, AtomType> typeMap) {
429     int count = 0;
430     int len = atomClasses.length;
431 
432     // Look for new TorsionTypes that contain 1 to 3 mapped atom classes.
433     for (AtomType newType : typeMap.keySet()) {
434       for (int atomClass : atomClasses) {
435         if (atomClass == newType.atomClass) {
436           count++;
437         }
438       }
439     }
440 
441     // If found, create a new TorsionType that bridges to known classes.
442     if (count == 1 || count == 2 || count == 3) {
443       int[] newClasses = copyOf(atomClasses, len);
444       for (AtomType newType : typeMap.keySet()) {
445         for (int i = 0; i < len; i++) {
446           if (atomClasses[i] == newType.atomClass) {
447             AtomType knownType = typeMap.get(newType);
448             newClasses[i] = knownType.atomClass;
449           }
450         }
451       }
452       return new TorsionType(newClasses, amplitude, phase, periodicity);
453     }
454     return null;
455   }
456 
457   /**
458    * {@inheritDoc}
459    *
460    * <p>Nicely formatted Torsion angle.
461    *
462    * @since 1.0
463    */
464   @Override
465   public String toString() {
466     StringBuilder torsionBuffer;
467     if (torsionMode == TorsionMode.IMPROPER) {
468       torsionBuffer = new StringBuilder("improper");
469     } else {
470       torsionBuffer = new StringBuilder("torsion");
471     }
472     for (int i : atomClasses) {
473       torsionBuffer.append(format(" %5d", i));
474     }
475 
476     boolean nonZero = false;
477     for (double v : amplitude) {
478       if (v != 0.0) {
479         nonZero = true;
480         break;
481       }
482     }
483 
484     for (int i = 0; i < amplitude.length; i++) {
485       if (amplitude[i] == 0.0 && (i > 0 || nonZero)) {
486         continue;
487       }
488       if (torsionMode == TorsionMode.NORMAL) {
489         torsionBuffer.append(format(" %7.3f %7.3f %1d", amplitude[i], phase[i], periodicity[i]));
490       } else {
491         torsionBuffer.append(format(" %7.3f %7.3f", amplitude[i], phase[i]));
492       }
493     }
494     return torsionBuffer.toString();
495   }
496 
497 
498   /**
499    * Create a PeriodicTorsionForce Element.
500    *
501    * @param doc        the Document instance.
502    * @param forceField the ForceField that contains the Torsion types.
503    * @return the PeriodicTorsionForce instance.
504    */
505   public static Element getXMLForce(Document doc, ForceField forceField) {
506     Map<String, TorsionType> types = forceField.getTorsionTypes();
507     if (!types.values().isEmpty()) {
508       Element node = doc.createElement("PeriodicTorsionForce");
509       double torsionUnit = forceField.getDouble("torsionunit", DEFAULT_TORSION_UNIT);
510       for (TorsionType torsionType : types.values()) {
511         node.appendChild(torsionType.toXML(doc, torsionUnit));
512       }
513       return node;
514     }
515     return null;
516   }
517 
518   /**
519    * Write TorsionType (Proper) to OpenMM XML format.
520    *
521    * @param doc      the Document instance.
522    * @param torsUnit scale torsion force constants by this factor.
523    * @return the Proper torsion Element.
524    */
525   public Element toXML(Document doc, Double torsUnit) {
526     Element node = doc.createElement("Proper");
527     // TODO specify proper?
528     node.setAttribute("class1", format("%d", atomClasses[0]));
529     node.setAttribute("class2", format("%d", atomClasses[1]));
530     node.setAttribute("class3", format("%d", atomClasses[2]));
531     node.setAttribute("class4", format("%d", atomClasses[3]));
532     for (int i = 0; i < amplitude.length; i++) {
533       // Convert from Kcal/mol to KJ/mol
534       node.setAttribute(format("k%d", i + 1), format("%f", amplitude[i] * KCAL_TO_KJ * torsUnit));
535       // Convert from degrees to radians.
536       node.setAttribute(format("phase%d", i + 1), format("%f", phase[i] / DEGREES_PER_RADIAN));
537       node.setAttribute(format("periodicity%d", i + 1), format("%d", periodicity[i]));
538     }
539     return node;
540   }
541 
542   /**
543    * Torsion modes include Normal or In-Plane
544    */
545   public enum TorsionMode {
546     NORMAL, IMPROPER
547   }
548 }