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